← back to Dw Launches

public/index.html

2203 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DW Launches — Social Composer</title>
<style>
  /* ─── Design tokens ─────────────────────────────────────────── */
  :root{
    /* surfaces */
    --bg:#0a0b0d;
    --panel:#111318;
    --panel2:#181b22;
    --panel3:#1e222b;
    --line:#252932;
    --line-soft:#1e2129;

    /* ink */
    --ink:#e8e4dc;
    --ink-2:#b0aaa0;
    --muted:#6e7480;

    /* brand palette */
    --gold:#c2a064;
    --gold-dim:#8a6e3e;
    --gold-faint:rgba(194,160,100,.08);
    --gold-faint2:rgba(194,160,100,.14);

    /* status */
    --danger:#c0505e;
    --danger-faint:rgba(192,80,94,.12);
    --warn:#b8923a;
    --warn-faint:rgba(184,146,58,.12);
    --ok:#3a8a60;
    --ok-faint:rgba(58,138,96,.12);
    --info:#3a6aa0;
    --info-faint:rgba(58,106,160,.12);

    /* interaction */
    --focus:rgba(194,160,100,.35);

    --radius:10px;
    --radius-sm:6px;
    --radius-lg:14px;

    --font:"Helvetica Neue",Helvetica,Arial,sans-serif;
    --serif:"Georgia","Times New Roman",serif;

    /* spacing scale */
    --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px;
    --sp-6:24px; --sp-7:28px; --sp-8:32px;
  }

  /* ─── Reset ─────────────────────────────────────────────────── */
  *{box-sizing:border-box;margin:0;padding:0}
  body{
    background:var(--bg);
    color:var(--ink);
    font-family:var(--font);
    font-size:13.5px;
    line-height:1.5;
    -webkit-font-smoothing:antialiased;
  }

  /* ─── Topbar ─────────────────────────────────────────────────── */
  header.topbar{
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding:0 28px;
    height:56px;
    border-bottom:1px solid var(--line);
    background:var(--panel);
    position:sticky;
    top:0;
    z-index:20;
  }

  /* Wordmark */
  .brand{
    display:flex;
    flex-direction:column;
    gap:0;
    line-height:1;
  }
  .brand-eyebrow{
    font-family:var(--font);
    font-size:9.5px;
    letter-spacing:3px;
    text-transform:uppercase;
    color:var(--muted);
    margin-bottom:3px;
  }
  .brand-name{
    font-family:var(--serif);
    font-size:22px;
    font-weight:400;
    letter-spacing:.8px;
    color:var(--ink);
    line-height:1;
  }
  .brand-name .dot{color:var(--gold)}

  /* Topbar nav */
  nav.tabs{display:flex;gap:2px}
  nav.tabs button{
    background:transparent;
    border:none;
    color:var(--muted);
    padding:8px 16px;
    border-radius:var(--radius-sm);
    cursor:pointer;
    font-size:12.5px;
    font-family:var(--font);
    letter-spacing:.3px;
    transition:color .15s, background .15s;
  }
  nav.tabs button:hover{color:var(--ink-2);background:var(--panel2)}
  nav.tabs button.active{
    color:var(--ink);
    background:var(--panel2);
    box-shadow:inset 0 -2px 0 var(--gold);
  }

  /* ─── Layout ─────────────────────────────────────────────────── */
  .wrap{
    display:flex;
    gap:20px;
    padding:20px 28px;
    max-width:1520px;
    margin:0 auto;
  }
  .col{flex:1;min-width:0}

  /* ─── Cards ─────────────────────────────────────────────────── */
  .card{
    background:var(--panel);
    border:1px solid var(--line);
    border-radius:var(--radius-lg);
    padding:20px;
    margin-bottom:14px;
  }

  /* Section label — editorial, not bureaucratic */
  .section-label{
    display:flex;
    align-items:baseline;
    gap:10px;
    margin-bottom:16px;
    padding-bottom:12px;
    border-bottom:1px solid var(--line-soft);
  }
  .section-label .sl-step{
    font-family:var(--font);
    font-size:10px;
    font-weight:600;
    letter-spacing:1.5px;
    text-transform:uppercase;
    color:var(--gold-dim);
    min-width:16px;
  }
  .section-label .sl-title{
    font-family:var(--serif);
    font-size:14px;
    font-weight:400;
    letter-spacing:.3px;
    color:var(--ink);
  }
  /* Legacy h3 inside .card — keep for any missed cards */
  .card h3{
    font-family:var(--font);
    font-size:10px;
    letter-spacing:1.5px;
    text-transform:uppercase;
    color:var(--gold-dim);
    margin:0 0 14px;
    font-weight:600;
  }

  /* ─── Channel rail ───────────────────────────────────────────── */
  .chanrail{display:flex;gap:8px;flex-wrap:wrap}
  .chip{
    display:flex;
    align-items:center;
    gap:8px;
    padding:7px 13px 7px 9px;
    border:1px solid var(--line);
    border-radius:999px;
    cursor:pointer;
    user-select:none;
    background:var(--panel2);
    color:var(--ink-2);
    font-size:12.5px;
    transition:border-color .15s, color .15s, background .15s;
  }
  .chip:hover:not(.disabled){
    border-color:var(--gold-dim);
    color:var(--ink);
    background:var(--panel3);
  }
  /* Brand-color dot replaces full candy avatar */
  .chip .av{
    width:8px;
    height:8px;
    border-radius:50%;
    flex-shrink:0;
  }
  .chip.sel{
    border-color:var(--gold);
    color:var(--ink);
    background:var(--gold-faint2);
  }
  .chip.sel .av{
    /* Slightly brightened dot on selected */
    box-shadow:0 0 0 2px rgba(0,0,0,.4);
  }
  .chip.disabled{opacity:.35;cursor:not-allowed}
  .chip .connect{
    font-size:9.5px;
    color:var(--warn);
    border:1px solid var(--warn-faint);
    border-radius:4px;
    padding:1px 5px;
    background:var(--warn-faint);
  }

  /* ─── Form elements ──────────────────────────────────────────── */
  textarea,
  input[type=text],
  input[type=datetime-local],
  select{
    width:100%;
    background:var(--panel2);
    border:1px solid var(--line);
    color:var(--ink);
    border-radius:var(--radius-sm);
    padding:9px 11px;
    font-family:inherit;
    font-size:13px;
    line-height:1.5;
    transition:border-color .15s, box-shadow .15s;
    -webkit-appearance:none;
    appearance:none;
  }
  textarea:focus,
  input[type=text]:focus,
  input[type=datetime-local]:focus,
  select:focus{
    outline:none;
    border-color:var(--gold-dim);
    box-shadow:0 0 0 3px var(--focus);
  }
  textarea{min-height:128px;resize:vertical;line-height:1.6}
  select{cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236e7480'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}

  label{
    display:block;
    font-size:11px;
    color:var(--muted);
    margin:12px 0 4px;
    letter-spacing:.4px;
    text-transform:uppercase;
  }
  label:first-child{margin-top:0}

  .row{display:flex;gap:10px}
  .row>*{flex:1}

  /* ─── Buttons ────────────────────────────────────────────────── */
  button.btn{
    border:none;
    border-radius:var(--radius-sm);
    padding:9px 18px;
    font-weight:500;
    cursor:pointer;
    font-size:13px;
    font-family:inherit;
    letter-spacing:.2px;
    transition:opacity .15s, background .15s, box-shadow .15s;
  }
  /* Primary — gold CTA (Save draft) */
  button.btn:not(.ghost):not(.danger):not(.btn-submit):not(.btn-approve):not(.btn-publish){
    background:var(--gold-faint2);
    color:var(--gold);
    border:1px solid var(--gold-dim);
  }
  button.btn:not(.ghost):not(.danger):not(.btn-submit):not(.btn-approve):not(.btn-publish):hover{
    background:rgba(194,160,100,.22);
  }
  /* Ghost */
  button.ghost{
    background:transparent;
    border:1px solid var(--line);
    color:var(--ink-2);
  }
  button.ghost:hover{border-color:var(--ink-2);color:var(--ink)}

  /* Submit for approval */
  button.btn-submit{
    background:var(--panel2);
    border:1px solid var(--gold-dim);
    color:var(--gold);
  }
  button.btn-submit:hover{background:var(--gold-faint2)}

  /* Steve sign-off */
  button.btn-approve{
    background:var(--panel2);
    border:1px solid var(--line);
    color:var(--ink-2);
    font-style:italic;
  }
  button.btn-approve:hover{border-color:var(--gold-dim);color:var(--ink)}

  /* Publish — solid gold when enabled, visually suppressed when gated */
  button.btn-publish{
    background:var(--gold);
    color:#0d0b06;
    font-weight:600;
    letter-spacing:.3px;
  }
  button.btn-publish:hover:not(:disabled){
    background:#d4b070;
    box-shadow:0 2px 12px rgba(194,160,100,.25);
  }
  button.btn-publish:disabled{
    background:var(--panel3);
    border:1px solid var(--line);
    color:var(--muted);
    cursor:not-allowed;
    font-weight:400;
    letter-spacing:0;
  }

  button.danger{background:var(--danger);color:#fff;border:none}
  button.danger:hover{opacity:.88}
  button:disabled{cursor:not-allowed}

  /* ─── Per-channel tabs ───────────────────────────────────────── */
  .ctabs{
    display:flex;
    gap:3px;
    flex-wrap:wrap;
    margin-bottom:14px;
    padding-bottom:12px;
    border-bottom:1px solid var(--line-soft);
  }
  .ctabs button{
    padding:5px 12px;
    font-size:12px;
    border-radius:var(--radius-sm);
    border:1px solid transparent;
    background:transparent;
    color:var(--muted);
    cursor:pointer;
    font-family:inherit;
    transition:color .15s, background .15s, border-color .15s;
  }
  .ctabs button:hover{color:var(--ink-2);background:var(--panel2)}
  .ctabs button.active{
    color:var(--ink);
    background:var(--panel2);
    border-color:var(--line);
    box-shadow:inset 0 -2px 0 var(--gold);
  }

  /* ─── Media ──────────────────────────────────────────────────── */
  .media-drop{
    border:1px dashed var(--line);
    border-radius:var(--radius);
    padding:18px 14px;
    text-align:center;
    color:var(--muted);
    font-size:12px;
    cursor:pointer;
    transition:border-color .15s, color .15s, background .15s;
    margin-top:4px;
  }
  .media-drop:hover{border-color:var(--gold-dim);color:var(--ink-2);background:var(--gold-faint)}
  .media-drop.over{border-color:var(--gold);color:var(--ink);background:var(--gold-faint2)}
  .media-list{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
  .media-item{
    position:relative;
    width:76px;
    height:76px;
    border-radius:var(--radius-sm);
    background:var(--panel2);
    border:1px solid var(--line);
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:9.5px;
    color:var(--muted);
    overflow:hidden;
    text-align:center;
    padding:4px;
    transition:border-color .15s;
  }
  .media-item:hover{border-color:var(--gold-dim)}
  .media-item img{width:100%;height:100%;object-fit:cover;border-radius:3px}
  .media-item .x{
    position:absolute;
    top:3px;right:4px;
    cursor:pointer;
    color:var(--danger);
    font-weight:700;
    font-size:13px;
    line-height:1;
    opacity:0;
    transition:opacity .12s;
  }
  .media-item:hover .x{opacity:1}
  .media-item .tag{
    position:absolute;
    bottom:3px;left:3px;
    font-size:8px;
    background:rgba(0,0,0,.65);
    padding:1px 4px;
    border-radius:3px;
    letter-spacing:.3px;
    text-transform:uppercase;
  }

  /* ─── Media Library view ─────────────────────────────────────── */
  .ml-toolbar{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);flex-wrap:wrap}
  .ml-sources{display:flex;gap:2px;flex-wrap:wrap}
  .ml-sources button{
    background:none;border:1px solid transparent;color:var(--ink-3);
    font:inherit;font-size:13px;padding:5px 12px;border-radius:7px;cursor:pointer;
  }
  .ml-sources button:hover{color:var(--ink-2);background:var(--panel2)}
  .ml-sources button.active{color:var(--ink);background:var(--gold-faint);border-color:var(--gold-dim)}
  .ml-sources button .ct{opacity:.6;font-size:11px;margin-left:5px}
  .ml-search{
    background:var(--panel2);border:1px solid var(--line);border-radius:8px;
    color:var(--ink);font:inherit;font-size:13px;padding:7px 12px;min-width:240px;
  }
  .ml-search:focus{outline:none;border-color:var(--gold-dim)}
  .ml-note{padding:10px 22px;font-size:12px}
  .ml-grid{
    display:grid;gap:12px;padding:6px 22px 22px;
    grid-template-columns:repeat(auto-fill,minmax(var(--mlmin,150px),1fr));
  }
  .ml-card{
    position:relative;border:1px solid var(--line);border-radius:9px;overflow:hidden;
    background:var(--panel2);display:flex;flex-direction:column;
  }
  .ml-card .thumb{width:100%;aspect-ratio:1/1;object-fit:cover;background:#1a1a1a;display:block}
  .ml-card .meta{padding:7px 8px 8px;display:flex;flex-direction:column;gap:2px;min-height:0}
  .ml-card .mt{font-size:11px;color:var(--ink);line-height:1.3;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .ml-card .mv{font-size:9px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.4px}
  .ml-card .cw-chip{font-size:9px;color:#c2a064;background:var(--gold-faint);border:1px solid var(--gold-dim);
    border-radius:4px;padding:1px 5px;margin-top:2px;align-self:flex-start;cursor:help;letter-spacing:.3px}
  .ml-card .src{position:absolute;top:5px;left:5px;font-size:8px;background:rgba(0,0,0,.7);
    color:#e9d9a8;padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.4px}
  .ml-card .ml-acts{display:flex;border-top:1px solid var(--line)}
  .ml-card .ml-acts button{
    flex:1;border:none;background:var(--gold-faint);
    color:var(--gold);font:inherit;font-size:11px;padding:6px 4px;cursor:pointer;letter-spacing:.2px;
  }
  .ml-card .ml-acts button+button{border-left:1px solid var(--line)}
  .ml-card .ml-acts button:hover{background:var(--gold-faint2);color:var(--ink)}
  .ml-card .att.added{background:#1d3a1d;color:#9fe09f}
  .ml-card .rep{color:var(--ink-2)!important}
  .ml-card .rep:hover{color:var(--ink)!important}
  .ml-more{display:flex;align-items:center;gap:14px;padding:0 22px 30px}
  .ml-empty{grid-column:1/-1;text-align:center;padding:48px 20px;color:var(--ink-2);line-height:1.7;font-size:14px}

  /* ─── Counters ───────────────────────────────────────────────── */
  .counter{font-size:11px;color:var(--muted);text-align:right;margin-top:3px}
  .counter.over{color:var(--danger);font-weight:600}

  /* ─── Preview panel ──────────────────────────────────────────── */
  /* Outer wrapper — device chrome feel */
  .preview-device{
    background:var(--panel2);
    border:1px solid var(--line);
    border-radius:var(--radius-lg);
    padding:14px;
    margin-top:0;
  }
  .pv{
    background:#fff;
    color:#111;
    border-radius:10px;
    overflow:hidden;
    box-shadow:0 1px 4px rgba(0,0,0,.18);
  }
  .pv .pvhead{display:flex;align-items:center;gap:9px;padding:10px 13px}
  .pv .pvav{width:34px;height:34px;border-radius:50%;flex-shrink:0}
  .pv .pvname{font-weight:700;font-size:13px;color:#111;line-height:1.2}
  .pv .pvsub{font-size:11px;color:#888;margin-top:1px}
  .pv .pvbody{
    padding:2px 13px 11px;
    font-size:13px;
    line-height:1.5;
    white-space:pre-wrap;
    color:#1a1a1a;
  }
  .pv .pvimg{
    width:100%;
    background:#edeae4;
    display:flex;
    align-items:center;
    justify-content:center;
    color:#bbb;
    font-size:11px;
    letter-spacing:.5px;
    text-transform:uppercase;
  }
  .pv.ig .pvimg{aspect-ratio:1/1}
  .pv.pinterest .pvimg{aspect-ratio:2/3}
  .pv.youtube .pvimg{aspect-ratio:16/9}
  .pv.facebook .pvimg{aspect-ratio:1.91/1}
  .pv.linkedin .pvimg{aspect-ratio:1.91/1}
  .pv .pvactions{
    display:flex;
    gap:18px;
    padding:9px 13px;
    color:#777;
    font-size:12px;
    border-top:1px solid #f0ede8;
  }
  .pv .pvtitle{font-weight:700;font-size:14px;padding:9px 13px 3px;color:#111}
  .pv .thread .tw{border-bottom:1px solid #f0ede8;padding:8px 0}

  /* ─── Badges ─────────────────────────────────────────────────── */
  .badge{
    display:inline-block;
    font-size:9.5px;
    padding:2px 8px;
    border-radius:999px;
    letter-spacing:.6px;
    text-transform:uppercase;
    font-weight:500;
  }
  .b-draft{background:#252830;color:#7a8090;border:1px solid #2e3240}
  .b-pending{background:var(--warn-faint);color:var(--warn);border:1px solid rgba(184,146,58,.25)}
  .b-approved{background:var(--ok-faint);color:#5ab880;border:1px solid rgba(58,138,96,.25)}
  .b-published{background:var(--info-faint);color:#6aacda;border:1px solid rgba(58,106,160,.25)}

  /* ─── Gate bar ───────────────────────────────────────────────── */
  .gatebar{
    background:var(--panel2);
    border:1px solid var(--line);
    border-radius:var(--radius);
    padding:12px 14px;
    margin-top:10px;
    font-size:12.5px;
  }
  .gatebar.warn{border-color:rgba(184,146,58,.4);background:var(--warn-faint)}
  .gatebar strong{font-size:12.5px;color:var(--ink-2)}
  .gatebar .muted{margin-top:4px}

  /* Approval row — stack buttons cleanly */
  .approval-actions{display:flex;flex-direction:column;gap:8px;margin-top:14px}
  .approval-actions .row{gap:8px}
  .approval-actions .row>*{flex:1}

  /* ─── Misc helpers ───────────────────────────────────────────── */
  .muted{color:var(--muted);font-size:12px}
  .hide{display:none!important}
  .divider{height:1px;background:var(--line-soft);margin:14px 0}

  /* ─── Queue slots ────────────────────────────────────────────── */
  .queueslots{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
  .queueslots button{
    font-size:11px;
    padding:4px 10px;
    border-radius:var(--radius-sm);
    background:var(--panel2);
    border:1px solid var(--line);
    color:var(--ink-2);
    cursor:pointer;
    font-family:inherit;
    transition:border-color .12s, color .12s;
  }
  .queueslots button:hover{border-color:var(--gold-dim);color:var(--ink)}

  /* ─── Field block (platform fields) ─────────────────────────── */
  .field-block{
    border:1px solid var(--line-soft);
    border-radius:var(--radius-sm);
    padding:12px;
    margin-top:10px;
    background:var(--panel2);
  }

  /* ─── Calendar ───────────────────────────────────────────────── */
  .cal{display:grid;grid-template-columns:repeat(7,1fr);gap:5px}
  .cal .dh{
    font-size:10.5px;
    color:var(--muted);
    text-align:center;
    padding:5px 2px;
    letter-spacing:.5px;
    text-transform:uppercase;
  }
  .cal .cell{
    min-height:76px;
    background:var(--panel2);
    border:1px solid var(--line);
    border-radius:var(--radius-sm);
    padding:6px;
    font-size:11px;
  }
  .cal .cell .dn{color:var(--muted);font-size:10px;margin-bottom:2px}
  .cal .ev{
    font-size:10px;
    margin-top:3px;
    padding:2px 5px;
    border-radius:4px;
    background:var(--panel3);
    border-left:2px solid var(--gold);
    overflow:hidden;
    white-space:nowrap;
    text-overflow:ellipsis;
    color:var(--ink-2);
  }
  #cal-label{
    text-align:center;
    font-family:var(--serif);
    font-size:17px;
    color:var(--ink);
    letter-spacing:.3px;
  }

  /* ─── Launch cards (Drafts view) ─────────────────────────────── */
  .launchcard{
    background:var(--panel2);
    border:1px solid var(--line);
    border-radius:var(--radius);
    padding:13px 15px;
    margin-bottom:8px;
    cursor:pointer;
    transition:border-color .15s, background .15s;
  }
  .launchcard:hover{border-color:var(--gold-dim);background:var(--panel3)}
  .launchcard .when{
    font-size:11px;
    color:var(--muted);
    margin-top:5px;
    display:flex;
    align-items:center;
    gap:4px;
  }

  /* ─── Toast ──────────────────────────────────────────────────── */
  .toast{
    position:fixed;
    bottom:20px;
    left:50%;
    transform:translateX(-50%);
    background:var(--panel2);
    border:1px solid var(--gold-dim);
    padding:11px 18px;
    border-radius:var(--radius);
    z-index:50;
    max-width:500px;
    font-size:13px;
    color:var(--ink-2);
    box-shadow:0 4px 24px rgba(0,0,0,.4);
    animation:toastin .18s ease;
  }
  .toast.err{border-color:var(--danger);color:var(--danger)}
  @keyframes toastin{from{opacity:0;transform:translateX(-50%) translateY(8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}

  /* ─── State line ─────────────────────────────────────────────── */
  #state-line{margin-bottom:6px;display:flex;align-items:center;gap:8px}

  /* ─── Preview sticky wrapper ─────────────────────────────────── */
  .preview-col{
    flex:1;
    min-width:0;
    position:sticky;
    top:72px;
    align-self:flex-start;
  }

  /* ─── modal-rig theming (CSS vars it reads) ──────────────────── */
  :root{ --card:var(--panel); --border:var(--line); }

  /* ─── View switcher (Compose | Streams | Calendar …) ─────────── */
  .topbar-right{display:flex;align-items:center;gap:18px}
  .viewswitch{display:flex;gap:2px}
  .viewswitch button{
    background:transparent;border:none;color:var(--muted);
    padding:8px 14px;border-radius:var(--radius-sm);cursor:pointer;
    font-size:12.5px;font-family:var(--font);letter-spacing:.3px;
    transition:color .15s, background .15s;
  }
  .viewswitch button:hover{color:var(--ink-2);background:var(--panel2)}
  .viewswitch button.active{color:var(--ink);background:var(--panel2);box-shadow:inset 0 -2px 0 var(--gold)}
  .viewswitch button .pill{
    display:inline-block;min-width:16px;text-align:center;margin-left:6px;
    font-size:9.5px;padding:1px 5px;border-radius:999px;
    background:var(--gold-faint2);color:var(--gold);
  }

  /* ─── Density slider bar (above grids/streams) ───────────────── */
  .density-bar{
    display:flex;align-items:center;gap:12px;
    max-width:1520px;margin:0 auto;padding:0 28px;
  }
  .density-bar .controls{
    display:flex;align-items:center;gap:10px;margin-left:auto;
    background:var(--panel2);border:1px solid var(--line);
    border-radius:999px;padding:6px 14px;
  }
  .density-bar label{margin:0;text-transform:uppercase;font-size:10px;letter-spacing:.6px;color:var(--muted)}
  .density-bar input[type=range]{
    width:150px;-webkit-appearance:none;appearance:none;height:3px;
    background:var(--line);border-radius:3px;cursor:pointer;padding:0;
  }
  .density-bar input[type=range]::-webkit-slider-thumb{
    -webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;
    background:var(--gold);cursor:pointer;border:2px solid var(--bg);
  }
  .density-bar .selwrap{display:flex;align-items:center;gap:8px}
  .density-bar select{width:auto;min-width:130px;padding:6px 26px 6px 10px;font-size:12px}

  /* ─── Dashboard ──────────────────────────────────────────────── */
  .dash-wrap{max-width:1520px;margin:0 auto;padding:20px 28px}
  .stat-grid{
    display:grid;
    grid-template-columns:repeat(auto-fill,minmax(var(--statmin,200px),1fr));
    gap:14px;margin-bottom:24px;
  }
  .stat{
    background:var(--panel);border:1px solid var(--line);
    border-radius:var(--radius-lg);padding:18px 20px;
  }
  .stat .v{font-family:var(--serif);font-size:34px;line-height:1;color:var(--ink)}
  .stat .v.gold{color:var(--gold)}
  .stat .v.warn{color:var(--warn)}
  .stat .v.ok{color:#5ab880}
  .stat .v.info{color:#6aacda}
  .stat .k{font-size:10.5px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-top:8px}
  .dash-section-title{
    font-family:var(--serif);font-size:15px;color:var(--ink);
    margin:4px 0 14px;letter-spacing:.3px;
  }
  .chan-grid{
    display:grid;
    grid-template-columns:repeat(auto-fill,minmax(var(--chanmin,220px),1fr));
    gap:12px;
  }
  .chan-tile{
    background:var(--panel);border:1px solid var(--line);
    border-radius:var(--radius);padding:14px 16px;
    cursor:pointer;transition:border-color .15s, background .15s;
  }
  .chan-tile:hover{border-color:var(--gold-dim);background:var(--panel2)}
  .chan-tile .ch-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}
  .chan-tile .av{width:9px;height:9px;border-radius:50%;flex-shrink:0}
  .chan-tile .ch-name{font-size:13px;color:var(--ink)}
  .chan-tile .ch-stats{display:flex;gap:14px;font-size:11px;color:var(--ink-2)}
  .chan-tile .ch-stats b{color:var(--ink);font-size:14px;font-weight:500;display:block}
  .chan-tile .ch-stats .lab{font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
  .chan-tile.off{opacity:.5}

  /* ─── Streams board (Hootsuite-style columns) ────────────────── */
  .streams-board{
    max-width:1520px;margin:0 auto;padding:16px 28px 40px;
    display:grid;
    grid-template-columns:repeat(var(--cols,4),minmax(0,1fr));
    gap:14px;
    align-items:start;
  }
  .stream-col{
    background:var(--panel);border:1px solid var(--line);
    border-radius:var(--radius-lg);overflow:hidden;min-width:0;
  }
  .stream-col .sc-head{
    display:flex;align-items:center;gap:8px;
    padding:12px 14px;border-bottom:1px solid var(--line-soft);
    background:var(--panel2);position:sticky;top:0;
  }
  .stream-col .sc-head .av{width:9px;height:9px;border-radius:50%}
  .stream-col .sc-head .nm{font-size:12.5px;color:var(--ink);font-family:var(--serif)}
  .stream-col .sc-head .ct{margin-left:auto;font-size:10px;color:var(--muted)}
  .stream-col .sc-head .off{font-size:9px;color:var(--warn);border:1px solid var(--warn-faint);background:var(--warn-faint);padding:1px 5px;border-radius:4px}
  .stream-col .sc-body{padding:10px;display:flex;flex-direction:column;gap:14px}
  .stream-seg .seg-label{
    font-size:9.5px;text-transform:uppercase;letter-spacing:1px;
    color:var(--gold-dim);margin-bottom:7px;display:flex;align-items:center;gap:6px;
  }
  .stream-seg .seg-label .n{color:var(--muted)}
  .stream-seg .seg-empty{font-size:11px;color:var(--muted);font-style:italic;padding:2px 0 6px}
  .stream-card{
    background:var(--panel2);border:1px solid var(--line);
    border-left:3px solid var(--gold);border-radius:var(--radius-sm);
    padding:9px 11px;margin-bottom:7px;cursor:pointer;
    transition:border-color .15s, background .15s;
  }
  .stream-card:hover{border-color:var(--gold-dim);background:var(--panel3)}
  .stream-card .st-title{font-size:12px;color:var(--ink);font-weight:500;line-height:1.3}
  .stream-card .st-body{font-size:11px;color:var(--ink-2);margin-top:4px;line-height:1.4;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .stream-card .st-meta{display:flex;align-items:center;gap:6px;margin-top:7px;flex-wrap:wrap}
  .stream-card .st-when{font-size:10px;color:var(--muted)}

  /* ─── Approvals view ─────────────────────────────────────────── */
  .approvals-wrap{max-width:1100px;margin:0 auto;padding:20px 28px}
  .appr-card{
    background:var(--panel);border:1px solid var(--line);
    border-radius:var(--radius);padding:15px 17px;margin-bottom:10px;
    display:flex;align-items:flex-start;gap:14px;
  }
  .appr-card.pending{border-color:rgba(184,146,58,.4)}
  .appr-card .ac-main{flex:1;min-width:0}
  .appr-card .ac-title{font-size:13.5px;color:var(--ink);font-weight:500}
  .appr-card .ac-body{font-size:12px;color:var(--ink-2);margin-top:5px;line-height:1.45;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .appr-card .ac-meta{font-size:11px;color:var(--muted);margin-top:7px;display:flex;gap:10px;flex-wrap:wrap;align-items:center}
  .appr-card .ac-actions{display:flex;flex-direction:column;gap:7px;flex:0 0 auto;width:170px}
  .appr-card .ac-actions button{width:100%}

  /* ─── Hootsuite-style horizontal streams board ───────────────── */
  .hoot-toolbar{
    display:flex;align-items:center;gap:12px;
    max-width:100%;margin:0;padding:10px 28px;border-bottom:1px solid var(--line-soft);
    position:sticky;top:56px;background:var(--bg);z-index:10;
  }
  .hoot-toolbar .ht-title{font-size:12px;color:var(--muted)}
  .hoot-toolbar .ht-spacer{margin-left:auto}
  .hoot-toolbar button.ht-add{
    background:var(--gold-faint2);border:1px solid var(--gold-dim);color:var(--gold);
    padding:7px 14px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:12px;
  }
  .hoot-toolbar button.ht-add:hover{background:rgba(194,160,100,.22)}
  .hoot-toolbar .ht-ctrl{display:flex;align-items:center;gap:8px;background:var(--panel2);
    border:1px solid var(--line);border-radius:999px;padding:5px 12px}
  .hoot-toolbar .ht-ctrl label{margin:0;text-transform:uppercase;font-size:9.5px;letter-spacing:.6px;color:var(--muted)}
  .hoot-toolbar input[type=range]{width:120px;-webkit-appearance:none;appearance:none;height:3px;
    background:var(--line);border-radius:3px;cursor:pointer;padding:0}
  .hoot-toolbar input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:13px;height:13px;border-radius:50%;background:var(--gold);cursor:pointer;border:2px solid var(--bg)}

  .hoot-board{
    display:flex;gap:14px;align-items:flex-start;
    padding:16px 28px 40px;overflow-x:auto;min-height:60vh;
  }
  .hoot-col{
    flex:0 0 var(--colw,320px);width:var(--colw,320px);
    background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);
    display:flex;flex-direction:column;max-height:calc(100vh - 170px);overflow:hidden;
    transition:border-color .15s, opacity .15s;
  }
  .hoot-col.dragging{opacity:.4;border-color:var(--gold)}
  .hoot-col.dragover{border-color:var(--gold)}
  .hoot-col .hc-head{
    display:flex;align-items:center;gap:8px;padding:11px 12px;
    border-bottom:1px solid var(--line-soft);background:var(--panel2);cursor:grab;
  }
  .hoot-col .hc-head:active{cursor:grabbing}
  .hoot-col .hc-head .av{width:9px;height:9px;border-radius:50%;flex-shrink:0}
  .hoot-col .hc-head .hc-tt{min-width:0}
  .hoot-col .hc-head .nm{font-size:12.5px;color:var(--ink);font-family:var(--serif);line-height:1.1;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .hoot-col .hc-head .ty{font-size:9px;text-transform:uppercase;letter-spacing:.7px;color:var(--gold-dim);margin-top:2px}
  .hoot-col .hc-head .ct{margin-left:auto;font-size:10px;color:var(--muted);background:var(--panel3);
    border-radius:999px;padding:1px 7px}
  .hoot-col .hc-head .rm{cursor:pointer;color:var(--muted);font-size:15px;line-height:1;padding:0 2px}
  .hoot-col .hc-head .rm:hover{color:var(--danger)}
  .hoot-col .hc-body{padding:9px;overflow-y:auto;display:flex;flex-direction:column;gap:7px}
  .hoot-col .hc-empty{font-size:11px;color:var(--muted);font-style:italic;padding:14px 6px;text-align:center}

  /* inbound feed card */
  .feed-card{
    background:var(--panel2);border:1px solid var(--line);border-radius:var(--radius-sm);
    padding:10px 11px;transition:border-color .15s;
  }
  .feed-card:hover{border-color:var(--gold-dim)}
  .feed-card.unread{border-left:3px solid var(--gold)}
  .feed-card .fc-top{display:flex;align-items:center;gap:8px}
  .feed-card .fc-av{width:26px;height:26px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;
    justify-content:center;font-size:11px;color:#0d0b06;font-weight:700}
  .feed-card .fc-name{font-size:12px;color:var(--ink);font-weight:600;line-height:1.1}
  .feed-card .fc-handle{font-size:10.5px;color:var(--muted)}
  .feed-card .fc-when{margin-left:auto;font-size:10px;color:var(--muted);white-space:nowrap}
  .feed-card .fc-text{font-size:12px;color:var(--ink-2);margin-top:7px;line-height:1.45;white-space:pre-wrap}
  .feed-card .fc-actions{display:flex;gap:14px;margin-top:9px;color:var(--muted);font-size:11px}
  .feed-card .fc-actions span{cursor:pointer;transition:color .12s}
  .feed-card .fc-actions span:hover{color:var(--gold)}
  .feed-card .fc-sim{font-size:8.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);
    border:1px solid var(--line);border-radius:3px;padding:0 4px;margin-left:auto}

  /* add-stream picker */
  .picker label{margin-top:14px}
  .picker .typegrid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:6px}
  .picker .typeopt{
    border:1px solid var(--line);border-radius:var(--radius-sm);padding:9px 11px;cursor:pointer;
    background:var(--panel2);transition:border-color .12s, background .12s;
  }
  .picker .typeopt:hover{border-color:var(--gold-dim);background:var(--panel3)}
  .picker .typeopt.sel{border-color:var(--gold);background:var(--gold-faint2)}
  .picker .typeopt .tn{font-size:12.5px;color:var(--ink)}
  .picker .typeopt .tk{font-size:9px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold-dim);margin-top:3px}

  /* ─── Studio tools row (Compose) ─────────────────────────────── */
  .studio-tools{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 2px}
  .studio-tools button{
    font-size:11.5px;padding:6px 12px;border-radius:999px;cursor:pointer;font-family:inherit;
    background:var(--panel2);border:1px solid var(--line);color:var(--ink-2);
    transition:border-color .12s, color .12s, background .12s;
  }
  .studio-tools button:hover{border-color:var(--gold-dim);color:var(--ink);background:var(--panel3)}

  /* ─── Analytics view ─────────────────────────────────────────── */
  .ana-wrap{max-width:1520px;margin:0 auto;padding:20px 28px}
  .ana-note{font-size:11px;color:var(--warn);border:1px solid var(--warn-faint);background:var(--warn-faint);
    border-radius:var(--radius-sm);padding:7px 11px;margin-bottom:18px;display:inline-block}
  .ana-bars{display:flex;align-items:flex-end;gap:10px;height:150px;padding:10px 0;
    border-bottom:1px solid var(--line-soft);margin-bottom:8px}
  .ana-bars .bar{flex:1;background:linear-gradient(var(--gold),var(--gold-dim));border-radius:4px 4px 0 0;
    min-height:4px;position:relative}
  .ana-bars .bar .bl{position:absolute;bottom:-18px;left:0;right:0;text-align:center;font-size:10px;color:var(--muted)}
  .ana-table{width:100%;border-collapse:collapse;margin-top:10px}
  .ana-table th{text-align:left;font-size:9.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);
    padding:8px 10px;border-bottom:1px solid var(--line)}
  .ana-table td{padding:10px;border-bottom:1px solid var(--line-soft);font-size:12.5px;color:var(--ink-2)}
  .ana-table td .av{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:7px}
  .ana-table td b{color:var(--ink);font-weight:500}

  /* ─── Bulk composer view ─────────────────────────────────────── */
  .bulk-wrap{max-width:1100px;margin:0 auto;padding:20px 28px}
  .bulk-wrap textarea{min-height:150px;font-family:ui-monospace,Menlo,monospace;font-size:12px}
  .bulk-preview{margin-top:14px}
  .bulk-row{display:flex;gap:10px;align-items:center;padding:9px 12px;border:1px solid var(--line-soft);
    border-radius:var(--radius-sm);margin-bottom:6px;background:var(--panel2);font-size:12px}
  .bulk-row.bad{border-color:var(--danger);background:var(--danger-faint)}
  .bulk-row .br-ch{display:flex;gap:4px;flex-wrap:wrap}
  .bulk-row .dot{width:8px;height:8px;border-radius:50%;display:inline-block}
  .bulk-row .br-body{flex:1;min-width:0;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .bulk-row .br-when{color:var(--muted);font-size:11px;white-space:nowrap}
</style>
</head>
<body>
<header class="topbar">
  <div class="brand">
    <div class="brand-eyebrow">Launches · Social Composer</div>
    <div class="brand-name">Designer Wallcoverings<span class="dot">.</span></div>
  </div>
  <div class="topbar-right">
    <nav class="viewswitch tabs" id="viewswitch">
      <button id="nav-dashboard" onclick="showView('dashboard')">Dashboard</button>
      <button id="nav-compose" class="active" onclick="showView('compose')">Compose</button>
      <button id="nav-media" onclick="showView('media')">Media</button>
      <button id="nav-streams" onclick="showView('streams')">Streams</button>
      <button id="nav-inbox" onclick="showView('inbox')">Inbox<span class="pill hide" id="inbox-pill">0</span></button>
      <button id="nav-bulk" onclick="showView('bulk')">Bulk</button>
      <button id="nav-calendar" onclick="showView('calendar')">Calendar</button>
      <button id="nav-analytics" onclick="showView('analytics')">Analytics</button>
      <button id="nav-drafts" onclick="showView('drafts')">Drafts &amp; Queue</button>
      <button id="nav-approvals" onclick="showView('approvals')">Approvals<span class="pill hide" id="appr-pill">0</span></button>
    </nav>
  </div>
</header>

<!-- ============ DASHBOARD VIEW ============ -->
<div id="view-dashboard" class="hide">
  <div class="dash-wrap">
    <div class="stat-grid" id="dash-stats"></div>
    <div class="dash-section-title">Per-channel</div>
    <div class="chan-grid" id="dash-channels"></div>
  </div>
</div>

<!-- ============ COMPOSE VIEW ============ -->
<div id="view-compose">
  <div class="wrap">
    <!-- LEFT: editor -->
    <div class="col" style="flex:1.25">

      <!-- 1. Channel selector rail -->
      <div class="card">
        <div class="section-label">
          <span class="sl-step">01</span>
          <span class="sl-title">Channels</span>
        </div>
        <div id="chanrail" class="chanrail"></div>
      </div>

      <!-- 2. Global editor -->
      <div class="card">
        <div class="section-label">
          <span class="sl-step">02</span>
          <span class="sl-title">Global Post</span>
        </div>
        <label>Launch title <span class="muted" style="display:inline;text-transform:none;letter-spacing:0">(admin only)</span></label>
        <input type="text" id="launch-title" placeholder="e.g. Trending Wallcoverings Collection 2026 — Weekly Arrivals">
        <label>Master body <span class="muted" style="display:inline;text-transform:none;letter-spacing:0">— selected channels inherit this by default</span></label>
        <textarea id="global-body" placeholder='Write the master post… (the word "Wallpaper" is banned — use "Wallcoverings")'></textarea>
        <div class="counter" id="global-counter"></div>
        <div class="studio-tools">
          <button type="button" onclick="aiCaption()" title="Generate an on-brand caption (local, $0)">✨ AI caption</button>
          <button type="button" onclick="aiHashtags()" title="Append a DW hashtag set"># Hashtags</button>
          <button type="button" onclick="bestTime()" title="Recommended posting slots">🕐 Best time</button>
          <button type="button" onclick="shortenLinks()" title="Shorten URLs in the post to dw.ly links">🔗 Shorten links</button>
        </div>

        <!-- 4. Media attach -->
        <label>Media</label>
        <div class="media-drop" id="global-drop">Drag &amp; drop, or click to add image / carousel / video URL or file ref</div>
        <input type="file" id="global-file" class="hide" multiple accept="image/*,video/*">
        <div class="media-list" id="global-media"></div>
      </div>

      <!-- 3. Per-channel tabs + 6. platform fields -->
      <div class="card" id="perchan-card">
        <div class="section-label">
          <span class="sl-step">03</span>
          <span class="sl-title">Per-channel Overrides</span>
        </div>
        <div class="ctabs" id="ctabs"></div>
        <div id="perchan-editor" class="muted">Select 2+ channels to unlock per-channel overrides.</div>
      </div>

      <!-- 7. Scheduling -->
      <div class="card">
        <div class="section-label">
          <span class="sl-step">07</span>
          <span class="sl-title">Scheduling</span>
        </div>
        <div class="row">
          <div>
            <label>Mode</label>
            <select id="sched-mode" onchange="renderSchedule()">
              <option value="now">Post now</option>
              <option value="schedule" selected>Schedule</option>
              <option value="queue">Add to queue</option>
              <option value="draft">Keep as draft</option>
            </select>
          </div>
          <div id="sched-dt-wrap">
            <label>Date &amp; time</label>
            <input type="datetime-local" id="sched-dt">
          </div>
        </div>
        <div id="queue-wrap" class="hide">
          <label>Preset queue slots (PT)</label>
          <div class="queueslots" id="queue-slots"></div>
        </div>
        <p class="muted" style="margin-top:10px">Per-channel cadence overrides global schedule — set inside each channel tab.</p>
      </div>

      <!-- 9. State & approval -->
      <div class="card">
        <div class="section-label">
          <span class="sl-step">09</span>
          <span class="sl-title">State &amp; Approval</span>
        </div>
        <div id="state-line"><span class="badge b-draft">draft</span></div>
        <div class="gatebar" id="gatebar">
          <strong>Ready to publish.</strong>
          <div class="muted">No sign-off required — press Publish Now to go live.</div>
        </div>
        <div class="approval-actions">
          <div class="row">
            <button class="btn ghost" onclick="saveLaunch('draft')">Save draft</button>
            <button class="btn btn-publish" id="btn-publish" onclick="publish()">⚡ Publish Now</button>
          </div>
        </div>
      </div>
    </div>

    <!-- RIGHT: preview (sticky) -->
    <div class="col preview-col">
      <div class="card">
        <div class="section-label">
          <span class="sl-step">05</span>
          <span class="sl-title">Live Preview</span>
        </div>
        <div class="ctabs" id="pv-tabs"></div>
        <div class="preview-device">
          <div id="preview"></div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- ============ MEDIA LIBRARY VIEW ============ -->
<div id="view-media" class="hide">
  <div class="ml-toolbar">
    <nav class="ml-sources tabs" id="ml-sources"></nav>
    <div class="ht-spacer"></div>
    <input type="text" id="ml-search" class="ml-search" placeholder="Search title / vendor / caption…">
    <div class="ht-ctrl">
      <label for="ml-dedup">Show</label>
      <select id="ml-dedup" class="ml-search" style="min-width:0;padding:6px 8px">
        <option value="image">Unique photos</option>
        <option value="off">All colorways</option>
        <option value="pattern">One per pattern</option>
      </select>
    </div>
    <div class="ht-ctrl">
      <label for="ml-density">Density</label>
      <input type="range" id="ml-density" min="90" max="260" step="10" value="150">
    </div>
  </div>
  <div class="ml-note muted" id="ml-note">Real assets from our dw_unified account — Shopify product images, our Instagram posts, and vendor / IG imagery. Click <b>+ Attach</b> to add to the current compose.</div>
  <div class="ml-grid" id="ml-grid"></div>
  <div class="ml-more"><button id="ml-more-btn" class="ht-add" onclick="mediaLoadMore()">Load more</button><span class="muted" id="ml-status"></span></div>
</div>

<!-- ============ CALENDAR VIEW ============ -->
<div id="view-calendar" class="hide">
  <div class="wrap"><div class="col">
    <div class="card">
      <h3>Calendar</h3>
      <div class="row" style="align-items:center;margin-bottom:16px">
        <button class="btn ghost" onclick="calShift(-1)" style="flex:0 0 auto;padding:7px 14px">‹ Prev</button>
        <div id="cal-label"></div>
        <button class="btn ghost" onclick="calShift(1)" style="flex:0 0 auto;padding:7px 14px">Next ›</button>
      </div>
      <div class="cal" id="cal-grid"></div>
    </div>
  </div></div>
</div>

<!-- ============ DRAFTS VIEW ============ -->
<div id="view-drafts" class="hide">
  <div class="density-bar" style="padding-top:6px;padding-bottom:12px">
    <span class="muted" style="font-size:12px">Every launch — click a card to open as a movable modal, or load into Compose</span>
    <div class="controls">
      <label for="drafts-density">Density</label>
      <input type="range" id="drafts-density" min="200" max="460" step="20" value="300">
    </div>
  </div>
  <div class="dash-wrap" style="padding-top:0">
    <div id="drafts-list" class="chan-grid" style="--chanmin:var(--draftmin,300px)"></div>
  </div>
</div>

<!-- ============ STREAMS VIEW (Hootsuite-style columns) ============ -->
<div id="view-streams" class="hide">
  <div class="hoot-toolbar">
    <span class="ht-title">Streams — add a column per account &amp; stream type. Drag headers to reorder. Inbound feeds are simulated (local tool).</span>
    <div class="ht-spacer"></div>
    <div class="ht-ctrl">
      <label for="hoot-width">Width</label>
      <input type="range" id="hoot-width" min="260" max="440" step="20" value="320">
    </div>
    <button class="ht-add" onclick="openStreamPicker()">+ Add stream</button>
  </div>
  <div class="hoot-board" id="hoot-board"></div>
</div>

<!-- ============ INBOX VIEW (unified social inbox) ============ -->
<div id="view-inbox" class="hide">
  <div class="inbox-wrap">
    <div class="inbox-bar">
      <div class="inbox-filters" id="inbox-filters"></div>
      <div class="inbox-spacer"></div>
      <select id="inbox-channel" onchange="renderInbox()" style="width:auto;min-width:150px"></select>
    </div>
    <div class="ana-note" style="margin:14px 0 4px">⚠ Simulated inbox — conversations are sample data. Replies are drafted locally; nothing is sent.</div>
    <div id="inbox-list"></div>
  </div>
</div>

<!-- ============ BULK COMPOSER VIEW ============ -->
<div id="view-bulk" class="hide">
  <div class="bulk-wrap">
    <h3 style="font-family:var(--serif);font-size:15px;color:var(--ink);margin-bottom:6px;font-weight:400">Bulk composer</h3>
    <p class="muted" style="margin-bottom:14px">Paste one post per line — <code>YYYY-MM-DD HH:MM | channels | body</code>. Channels comma-separated (ig, fb, li, pin, tt, x, yt) or <code>all</code>. Each becomes a scheduled draft. The word "Wallpaper" is banned (server-rejected).</p>
    <textarea id="bulk-input" placeholder="2026-07-01 09:00 | ig,pin | New grasscloth arrivals in Oatmeal & Greige — to the trade now.
2026-07-02 12:30 | all | Heritage damask book just landed. Memo samples shipping this week.
2026-07-03 18:00 | li | Why warm minimalism is dominating 2026 contract specs."></textarea>
    <div class="studio-tools" style="margin-top:10px">
      <button onclick="bulkParse()">Parse &amp; preview</button>
      <button onclick="document.getElementById('bulk-input').value='';document.getElementById('bulk-preview').innerHTML=''">Clear</button>
      <button class="" id="bulk-create" onclick="bulkCreate()" style="display:none">Create drafts ↓</button>
    </div>
    <div class="bulk-preview" id="bulk-preview"></div>
  </div>
</div>

<!-- ============ ANALYTICS VIEW ============ -->
<div id="view-analytics" class="hide">
  <div class="ana-wrap">
    <div class="ana-note">⚠ Simulated metrics — this is a local build tool. No live social analytics API is called.</div>
    <div class="stat-grid" id="ana-stats"></div>
    <div class="dash-section-title">Reach — last 7 days</div>
    <div class="ana-bars" id="ana-bars"></div>
    <div class="dash-section-title" style="margin-top:30px">Per-channel performance</div>
    <table class="ana-table" id="ana-table"></table>
  </div>
</div>

<!-- ============ APPROVALS VIEW ============ -->
<div id="view-approvals" class="hide">
  <div class="approvals-wrap">
    <h3 style="font-family:var(--serif);font-size:15px;color:var(--ink);margin-bottom:6px;font-weight:400">Approvals queue</h3>
    <p class="muted" style="margin-bottom:16px">Publish Now posts live to connected channels (Instagram via Norma → Meta Graph). A channel with no valid token records the post locally and reports <em>live:false</em> with the reason — connect/refresh that channel's token to post for real.</p>
    <div id="approvals-list"></div>
  </div>
</div>

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

<script src="/modal-rig.js"></script>
<script src="/hoot-extras.js"></script>
<script>
const CHAR_LIMITS = { instagram:2200, facebook:63206, linkedin:3000, pinterest:500, tiktok:2200, twitter:280, youtube:5000 };
const COLORS = { instagram:'#E1306C', facebook:'#1877F2', linkedin:'#0A66C2', pinterest:'#E60023', tiktok:'#25F4EE', twitter:'#1DA1F2', youtube:'#FF0000' };
const QUEUE_SLOTS = ['09:00','10:30','12:00','13:30','16:00','18:00','19:30'];

let CHANNELS = [];
let state = null;        // the launch being composed
let activeChanTab = null;
let activePvTab = null;

function blankLaunch(){
  return { id:null, title:'', channelsSelected:[], global:{body:'',media:[]}, channels:{},
           schedule:{mode:'schedule',datetime:''}, status:'draft',
           approval:{approved:false,by:null,at:null}, published:false };
}

async function api(path, opts){
  const r = await fetch(path, Object.assign({headers:{'Content-Type':'application/json'}}, opts||{}));
  const j = await r.json().catch(()=>({}));
  if(!r.ok) throw Object.assign(new Error(j.message||j.error||'error'), {body:j});
  return j;
}
function toast(msg, isErr){
  const t=document.getElementById('toast'); t.textContent=msg; t.className='toast'+(isErr?' err':'');
  setTimeout(()=>t.classList.add('hide'), 4200);
}

// ---------- init ----------
async function init(){
  CHANNELS = (await api('/api/channels')).channels;
  // Deep-link: a "Build marketing" click from the CNCP new-SKU calendar lands here
  // with ?compose=1&title=&img=&link=&sku=&vendor= → start a launch for that product.
  if(maybeComposeFromURL()) return;
  // load seed/first launch if exists
  const ls = (await api('/api/launches')).launches;
  state = ls.find(l=>l.id==='seed-trending-2026-w25') || ls[0] || blankLaunch();
  hydrate();
}
// Build a fresh launch from product query-params (CNCP SKU → DW marketing on-ramp).
function maybeComposeFromURL(){
  const q = new URLSearchParams(location.search);
  if(!q.get('compose') && !q.get('title') && !q.get('img')) return false;
  window.__dlCompose = true; // deep-link wins over the saved-view restore
  const title = (q.get('title')||'').trim();
  const img = (q.get('img')||'').trim();
  const link = (q.get('link')||'').trim();
  const vendor = (q.get('vendor')||'').trim();
  state = blankLaunch();
  state.title = title || 'New product launch';
  // Starter caption — "Wallcoverings" never "Wallpaper" (server gate also enforces).
  let body = title ? `Just in: ${title}` : 'New arrival just landed.';
  if(vendor) body += ` by ${vendor}`;
  body += '. New to the trade — designers, DM for pricing.';
  if(link) body += `\n\nShop it: ${link}`;
  state.global.body = body;
  if(img) state.global.media = [{ type:'image', ref:img, alt:title }];
  // default to the visual channels
  ['instagram','pinterest','facebook'].forEach(ch=>{
    if(CHANNELS.some(c=>c.id===ch && c.connected)){
      state.channelsSelected.push(ch);
      state.channels[ch] = { body:'', media:[], fields:{}, schedule:{mode:state.schedule.mode,datetime:state.schedule.datetime} };
    }
  });
  activeChanTab = state.channelsSelected[0] || null;
  activePvTab = state.channelsSelected[0] || null;
  hydrate();
  showView('compose');
  toast('Started a launch from the new SKU — edit & save as a draft.');
  // clear params so a refresh doesn't re-trigger
  try{ history.replaceState({}, '', location.pathname); }catch(e){}
  return true;
}
function hydrate(){
  document.getElementById('launch-title').value = state.title||'';
  document.getElementById('global-body').value = state.global.body||'';
  document.getElementById('sched-mode').value = state.schedule.mode||'schedule';
  if(state.schedule.datetime) document.getElementById('sched-dt').value = toLocalInput(state.schedule.datetime);
  renderChanRail(); renderGlobalMedia(); renderCounter('global');
  renderCTabs(); renderSchedule(); renderState(); renderPreview();
}
function toLocalInput(iso){ try{const d=new Date(iso);const p=n=>String(n).padStart(2,'0');
  return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;}catch{return '';} }

// ---------- 1. channel rail ----------
function renderChanRail(){
  const el=document.getElementById('chanrail'); el.innerHTML='';
  CHANNELS.forEach(c=>{
    const sel = state.channelsSelected.includes(c.id);
    const d=document.createElement('div');
    d.className='chip'+(sel?' sel':'')+(c.connected?'':' disabled');
    // refined: colored dot, not full candy avatar
    d.innerHTML=`<span class="av" style="background:${COLORS[c.id]}"></span>${c.name}`
      + (c.connected?'':' <span class="connect">connect</span>');
    if(c.connected) d.onclick=()=>toggleChan(c.id);
    el.appendChild(d);
  });
}
function toggleChan(id){
  const i=state.channelsSelected.indexOf(id);
  if(i>=0){ state.channelsSelected.splice(i,1); delete state.channels[id]; }
  else {
    state.channelsSelected.push(id);
    state.channels[id] = state.channels[id] || { body:'', media:[], fields:{}, schedule:{mode:state.schedule.mode,datetime:state.schedule.datetime} };
  }
  if(!state.channelsSelected.includes(activeChanTab)) activeChanTab=state.channelsSelected[0]||null;
  if(!state.channelsSelected.includes(activePvTab)) activePvTab=state.channelsSelected[0]||null;
  renderChanRail(); renderCTabs(); renderPreview();
}

// ---------- 2. global editor ----------
document.getElementById('launch-title').addEventListener('input',e=>{state.title=e.target.value;});
document.getElementById('global-body').addEventListener('input',e=>{state.global.body=e.target.value;renderCounter('global');renderPreview();});

// ---------- 4. media ----------
function wireDrop(dropId, fileId, getArr){
  const drop=document.getElementById(dropId), file=document.getElementById(fileId);
  drop.onclick=()=>{
    const url=prompt('Add media — paste an image/video URL or local file ref (or Cancel to pick a file):');
    if(url){ getArr().push({type:guessType(url),ref:url,alt:''}); refreshMedia(); }
    else file.click();
  };
  ['dragover','dragenter'].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.add('over');}));
  ['dragleave','drop'].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.remove('over');}));
  drop.addEventListener('drop',e=>{ [...e.dataTransfer.files].forEach(f=>addFile(f,getArr())); });
  file.addEventListener('change',e=>{ [...e.target.files].forEach(f=>addFile(f,getArr())); e.target.value=''; });
}
function guessType(ref){ return /\.(mp4|mov|webm)$/i.test(ref)?'video':'image'; }
function addFile(f,arr){
  const url=URL.createObjectURL(f);
  arr.push({type:f.type.startsWith('video')?'video':'image', ref:url, name:f.name, alt:''});
  refreshMedia();
}
function refreshMedia(){ renderGlobalMedia(); renderPerChanMedia(); renderPreview(); }
function renderMediaInto(elId, arr){
  const el=document.getElementById(elId); if(!el) return; el.innerHTML='';
  arr.forEach((m,i)=>{
    const d=document.createElement('div'); d.className='media-item';
    const label=m.name||m.ref.split('/').pop()||m.type;
    d.innerHTML = m.type==='image' && /^blob:|^https?:|^\/api\//.test(m.ref)
      ? `<img src="${m.ref}" alt="">` : `<span>${m.type}<br>${label.slice(0,18)}</span>`;
    d.innerHTML += `<span class="x" title="remove">×</span><span class="tag">${m.type}</span>`;
    d.querySelector('.x').onclick=(e)=>{e.stopPropagation();arr.splice(i,1);refreshMedia();};
    el.appendChild(d);
  });
}
function renderGlobalMedia(){ renderMediaInto('global-media', state.global.media); }

// ---------- counters ----------
function renderCounter(scope){
  if(scope==='global'){
    const el=document.getElementById('global-counter');
    el.textContent = `${(state.global.body||'').length} chars (master)`;
  }
}

// ---------- 3 + 6. per-channel tabs ----------
function renderCTabs(){
  const el=document.getElementById('ctabs'); el.innerHTML='';
  const sel=state.channelsSelected;
  if(sel.length<2){
    document.getElementById('perchan-editor').innerHTML='<span class="muted">Select 2+ channels to unlock per-channel overrides.</span>';
    el.innerHTML=''; renderPvTabs(); return;
  }
  if(!sel.includes(activeChanTab)) activeChanTab=sel[0];
  sel.forEach(id=>{
    const b=document.createElement('button'); b.textContent=chanName(id);
    if(id===activeChanTab) b.className='active';
    b.onclick=()=>{activeChanTab=id;renderCTabs();};
    el.appendChild(b);
  });
  renderPerChanEditor(); renderPvTabs();
}
function chanName(id){ return (CHANNELS.find(c=>c.id===id)||{}).name||id; }
function ensureChan(id){ return state.channels[id] = state.channels[id] || {body:'',media:[],fields:{},schedule:{mode:state.schedule.mode,datetime:state.schedule.datetime}}; }

function renderPerChanEditor(){
  const id=activeChanTab; const c=ensureChan(id);
  const limit=CHAR_LIMITS[id];
  const effBody = c.body || state.global.body || '';
  let html = `
    <label>${chanName(id)} copy <span class="muted" style="display:inline;text-transform:none;letter-spacing:0">(overrides master; blank = inherit master)</span></label>
    <textarea id="pc-body" placeholder="Inherit master, or override here…">${esc(c.body||'')}</textarea>
    <div class="counter" id="pc-counter"></div>
    <label>${chanName(id)} media <span class="muted" style="display:inline;text-transform:none;letter-spacing:0">(blank = inherit master media)</span></label>
    <div class="media-drop" id="pc-drop">Add ${chanName(id)} media</div>
    <input type="file" id="pc-file" class="hide" multiple accept="image/*,video/*">
    <div class="media-list" id="pc-media"></div>`;
  html += platformFields(id, c);
  // per-channel schedule
  html += `
    <div class="field-block">
      <label>${chanName(id)} schedule (PT cadence)</label>
      <div class="row">
        <select id="pc-mode">
          ${['now','schedule','queue','draft'].map(m=>`<option value="${m}" ${ (c.schedule&&c.schedule.mode)===m?'selected':''}>${m}</option>`).join('')}
        </select>
        <input type="datetime-local" id="pc-dt" value="${c.schedule&&c.schedule.datetime?toLocalInput(c.schedule.datetime):''}">
      </div>
    </div>`;
  document.getElementById('perchan-editor').innerHTML=html;

  const bodyEl=document.getElementById('pc-body');
  bodyEl.addEventListener('input',()=>{ c.body=bodyEl.value; renderPcCounter(id); renderPreview(); });
  renderPcCounter(id);
  wireDrop('pc-drop','pc-file',()=>ensureChan(activeChanTab).media);
  renderPerChanMedia();
  wirePlatformFields(id,c);
  document.getElementById('pc-mode').onchange=e=>{ c.schedule=c.schedule||{}; c.schedule.mode=e.target.value; };
  document.getElementById('pc-dt').onchange=e=>{ c.schedule=c.schedule||{}; c.schedule.datetime=e.target.value?new Date(e.target.value).toISOString():''; };
}
function renderPcCounter(id){
  const el=document.getElementById('pc-counter'); if(!el) return;
  const c=ensureChan(id); const len=(c.body||state.global.body||'').length; const lim=CHAR_LIMITS[id];
  el.textContent=`${len} / ${lim} chars`;
  el.className='counter'+(len>lim?' over':'');
}
function renderPerChanMedia(){ if(activeChanTab && state.channelsSelected.length>=2) renderMediaInto('pc-media', ensureChan(activeChanTab).media); }

// 6. platform-specific fields
function platformFields(id,c){
  const f=c.fields||{}; const fb=v=>esc(v||'');
  if(id==='instagram') return `<div class="field-block"><label>IG first comment</label><input type="text" id="f-firstComment" value="${fb(f.firstComment)}" placeholder="First comment (hashtags / link)"></div>`;
  if(id==='youtube') return `<div class="field-block"><label>YouTube title</label><input type="text" id="f-ytTitle" value="${fb(f.ytTitle)}"><label>Thumbnail ref</label><input type="text" id="f-ytThumbnail" value="${fb(f.ytThumbnail)}"></div>`;
  if(id==='pinterest') return `<div class="field-block"><label>Pin title</label><input type="text" id="f-pinTitle" value="${fb(f.pinTitle)}"><label>Board</label><input type="text" id="f-board" value="${fb(f.board)}"><label>Destination link</label><input type="text" id="f-destinationLink" value="${fb(f.destinationLink)}"></div>`;
  if(id==='twitter') return `<div class="field-block"><label>Thread split <span class="muted" style="display:inline;text-transform:none;letter-spacing:0">(one tweet per line)</span></label><textarea id="f-thread" style="min-height:90px">${fb((f.thread||[]).join('\n'))}</textarea></div>`;
  return '';
}
function wirePlatformFields(id,c){
  c.fields=c.fields||{};
  const bind=(elId,key)=>{const e=document.getElementById(elId); if(e) e.addEventListener('input',()=>{c.fields[key]=e.value;renderPreview();});};
  bind('f-firstComment','firstComment'); bind('f-ytTitle','ytTitle'); bind('f-ytThumbnail','ytThumbnail');
  bind('f-pinTitle','pinTitle'); bind('f-board','board'); bind('f-destinationLink','destinationLink');
  const th=document.getElementById('f-thread'); if(th) th.addEventListener('input',()=>{c.fields.thread=th.value.split('\n').filter(Boolean);renderPreview();});
}

// ---------- 7. scheduling ----------
function renderSchedule(){
  const mode=document.getElementById('sched-mode').value;
  state.schedule.mode=mode;
  document.getElementById('sched-dt-wrap').style.display = (mode==='schedule')?'block':'none';
  document.getElementById('queue-wrap').classList.toggle('hide', mode!=='queue');
  if(mode==='queue'){
    const q=document.getElementById('queue-slots'); q.innerHTML='';
    QUEUE_SLOTS.forEach(t=>{const b=document.createElement('button');b.textContent=t+' PT';
      b.onclick=()=>{ applyQueueSlot(t); }; q.appendChild(b);});
  }
}
document.getElementById('sched-dt').addEventListener('change',e=>{ state.schedule.datetime = e.target.value?new Date(e.target.value).toISOString():''; });
function applyQueueSlot(hhmm){
  // assign next-day slot at chosen time PT to currently selected channels lacking a time
  const [h,m]=hhmm.split(':').map(Number);
  const d=new Date(); d.setDate(d.getDate()+1); d.setHours(h,m,0,0);
  state.channelsSelected.forEach(id=>{ const c=ensureChan(id); c.schedule=c.schedule||{}; c.schedule.mode='schedule'; c.schedule.datetime=d.toISOString(); });
  toast(`Queued selected channels at ${hhmm} PT (${d.toLocaleDateString()})`);
}

// ---------- 5. preview ----------
function renderPvTabs(){
  const el=document.getElementById('pv-tabs'); el.innerHTML='';
  const sel=state.channelsSelected.length?state.channelsSelected:CHANNELS.map(c=>c.id);
  if(!sel.includes(activePvTab)) activePvTab=sel[0];
  sel.forEach(id=>{const b=document.createElement('button');b.textContent=chanName(id);
    if(id===activePvTab)b.className='active'; b.onclick=()=>{activePvTab=id;renderPvTabs();renderPreview();}; el.appendChild(b);});
}
function effective(id){
  const c=state.channels[id]||{};
  return { body:(c.body||state.global.body||''), media:((c.media&&c.media.length)?c.media:state.global.media)||[], fields:c.fields||{} };
}
function renderPreview(){
  renderPvTabs();
  const id=activePvTab; if(!id){document.getElementById('preview').innerHTML='<span class="muted">Select a channel.</span>';return;}
  const e=effective(id); const lim=CHAR_LIMITS[id];
  const img = e.media[0];
  const imgBox = img ? (img.type==='image'&&/^blob:|^https?:/.test(img.ref)
        ? `<div class="pvimg"><img src="${img.ref}" style="width:100%;height:100%;object-fit:cover"></div>`
        : `<div class="pvimg">${img.type.toUpperCase()} · ${esc((img.name||img.ref).split('/').pop())}</div>`)
      : `<div class="pvimg">no media</div>`;
  let body=e.body, extra='';
  let cls='pv '+id;
  if(id==='twitter'){
    const thread=(e.fields.thread&&e.fields.thread.length)?e.fields.thread:[body];
    body = thread.map(t=>{
      const over=t.length>280;
      return `<div class="tw">${esc(t.slice(0,280))}${over?'…':''}<div class="counter ${over?'over':''}">${t.length}/280</div></div>`;
    }).join('');
    extra = `<div class="pvbody thread">${body}</div>`;
    document.getElementById('preview').innerHTML = pvShell(id,'@designerwall', imgBox, extra, 'X / Twitter');
    return;
  }
  const over = e.body.length>lim;
  const counter = `<div class="pvbody"><div class="counter ${over?'over':''}">${e.body.length}/${lim}</div></div>`;
  let head='', titleLine='';
  if(id==='youtube'){ titleLine=`<div class="pvtitle">${esc(e.fields.ytTitle||state.title||'Untitled')}</div>`; }
  if(id==='pinterest' && e.fields.pinTitle){ titleLine=`<div class="pvtitle">${esc(e.fields.pinTitle)}</div>`; }
  let firstC = (id==='instagram'&&e.fields.firstComment)?`<div class="pvbody muted">↳ first comment: ${esc(e.fields.firstComment)}</div>`:'';
  let pinMeta = (id==='pinterest')?`<div class="pvbody muted">Board: ${esc(e.fields.board||'—')} · → ${esc(e.fields.destinationLink||'—')}</div>`:'';
  const inner = `${(id==='youtube')?imgBox+titleLine:''}${(id!=='youtube')?titleLine:''}${(id!=='youtube')?imgBox:''}<div class="pvbody">${esc(e.body)}</div>${counter}${firstC}${pinMeta}`;
  document.getElementById('preview').innerHTML = pvShell(id,'Designer Wallcoverings', inner, '', chanName(id));
}
function pvShell(id, name, inner, extra, label){
  return `<div class="pv ${id}">
    <div class="pvhead">
      <div class="pvav" style="background:${COLORS[id]}"></div>
      <div><div class="pvname">${name}</div><div class="pvsub">${label} preview</div></div>
    </div>
    ${inner}${extra}
    <div class="pvactions"><span>♥ Like</span><span>💬 Comment</span><span>↗ Share</span></div>
  </div>`;
}

// ---------- 9. state / approval ----------
function renderState(){
  const m={draft:'b-draft',pending_approval:'b-pending',approved:'b-approved',published:'b-published'};
  document.getElementById('state-line').innerHTML=`<span class="badge ${m[state.status]||'b-draft'}">${(state.status||'draft').replace('_',' ')}</span>`
    + (state.approval&&state.approval.approved?` <span class="muted">· signed off by ${state.approval.by} ${new Date(state.approval.at).toLocaleString()}</span>`:'');
  const gate=document.getElementById('gatebar');
  const pub=state.published||state.status==='published';
  const btn=document.getElementById('btn-publish');
  if(btn){ btn.disabled=pub; btn.textContent=pub?'✓ Published':'⚡ Publish Now'; }
  gate.className='gatebar'+(pub?'':' ');
  gate.querySelector('strong').textContent = pub?'Published — live.':'Ready to publish.';
}

// ---------- actions ----------
async function saveLaunch(forceStatus){
  if(forceStatus) state.status=forceStatus;
  // pull current title/body
  state.title=document.getElementById('launch-title').value;
  state.global.body=document.getElementById('global-body').value;
  try{
    const j=await api('/api/launches',{method:'POST',body:JSON.stringify(state)});
    state=j.launch; toast('Saved draft ✓'); renderState();
  }catch(e){
    if(e.body&&e.body.error==='BANNED_WORD'){ toast('Blocked: "Wallpaper" is banned — use "Wallcoverings". '+(e.body.hits||[]).join('; '),true); }
    else toast('Save failed: '+e.message,true);
  }
}
async function submitApproval(){
  await saveLaunch('draft');
  if(!state.id) return;
  try{ const j=await api(`/api/launches/${state.id}/submit-approval`,{method:'POST'}); state=j.launch; renderState(); toast('Submitted for approval — awaiting Steve sign-off.'); }
  catch(e){ toast('Failed: '+e.message,true); }
}
async function approve(){
  if(!state.id){ toast('Save the launch first.',true); return; }
  // local Steve sign-off token gate
  const token=prompt('Steve sign-off required to unlock publish. Enter sign-off token (STEVE-GO):');
  if(token===null) return;
  try{ const j=await api(`/api/launches/${state.id}/approve`,{method:'POST',body:JSON.stringify({token,by:'Steve'})}); state=j.launch; renderState(); toast('Signed off ✓ — Publish Now posts live to connected channels.'); }
  catch(e){ toast('Sign-off rejected: '+e.message,true); }
}
async function publish(){
  if(!state.id){ await saveLaunch('draft'); if(!state.id) return; }
  try{ const j=await api(`/api/launches/${state.id}/publish`,{method:'POST'}); state=j.launch; renderState();
    toast(j.note||'Published.', j.live===false); }
  catch(e){ toast('Publish failed: '+e.message,true); }
}

// ---------- views ----------
const VIEWS=['dashboard','compose','media','streams','inbox','bulk','calendar','analytics','drafts','approvals'];
function showView(v){
  if(!VIEWS.includes(v)) v='compose';
  VIEWS.forEach(x=>{
    document.getElementById('view-'+x).classList.toggle('hide',x!==v);
    document.getElementById('nav-'+x).classList.toggle('active',x===v);
  });
  try{ localStorage.setItem('dwlaunch:view', v); }catch(e){}
  if(v==='dashboard') renderDashboard();
  if(v==='media') renderMedia();
  if(v==='streams') renderStreams();
  if(v==='inbox') renderInbox();
  if(v==='bulk') {/* static */}
  if(v==='calendar') renderCalendar();
  if(v==='analytics') renderAnalytics();
  if(v==='drafts') renderDrafts();
  if(v==='approvals') renderApprovals();
}

// 8. calendar
let calRef=new Date();
function calShift(n){ calRef.setMonth(calRef.getMonth()+n); renderCalendar(); }
async function renderCalendar(){
  const {events}=await api('/api/calendar');
  const y=calRef.getFullYear(), mo=calRef.getMonth();
  document.getElementById('cal-label').textContent=calRef.toLocaleString(undefined,{month:'long',year:'numeric'});
  const first=new Date(y,mo,1), startDow=first.getDay(), days=new Date(y,mo+1,0).getDate();
  const grid=document.getElementById('cal-grid'); grid.innerHTML='';
  ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dh';h.textContent=d;grid.appendChild(h);});
  for(let i=0;i<startDow;i++){const c=document.createElement('div');c.className='cell';grid.appendChild(c);}
  for(let d=1;d<=days;d++){
    const cell=document.createElement('div'); cell.className='cell';
    cell.innerHTML=`<div class="dn">${d}</div>`;
    events.filter(e=>{const dt=new Date(e.datetime);return dt.getFullYear()===y&&dt.getMonth()===mo&&dt.getDate()===d;})
      .forEach(e=>{const ev=document.createElement('div');ev.className='ev';
        ev.style.borderLeftColor=COLORS[e.channel]||'#c2a064';
        ev.title=`${e.title}\n${e.channel} · ${new Date(e.datetime).toISOString()}\nstatus: ${e.status}`;
        ev.textContent=`${new Date(e.datetime).toLocaleTimeString([], {hour:'numeric',minute:'2-digit'})} ${chanName(e.channel)}`;
        cell.appendChild(ev);});
    grid.appendChild(cell);
  }
}

const BADGE_MAP={draft:'b-draft',pending_approval:'b-pending',approved:'b-approved',published:'b-published'};
function fmtWhen(iso){ if(!iso) return '—'; try{ return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }catch(e){ return '—'; } }

// drafts list — admin cards w/ created date+time, open as movable modal
async function renderDrafts(){
  const {launches}=await api('/api/launches');
  const el=document.getElementById('drafts-list'); el.innerHTML = launches.length?'':'<span class="muted">No launches yet.</span>';
  launches.forEach(l=>{
    const d=document.createElement('div'); d.className='launchcard';
    const whenStr=fmtWhen(l.created_at);
    d.innerHTML=`<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
        <strong>${esc(l.title)}</strong>
        <span class="badge ${BADGE_MAP[l.status]||'b-draft'}">${(l.status||'draft').replace('_',' ')}</span></div>
      <div class="muted" style="margin-top:4px">${(l.channelsSelected||[]).map(chanName).join(' · ')||'no channels'}</div>
      <div class="when" title="${esc(l.created_at||'')}">🕓 ${whenStr}</div>`;
    d.onclick=()=>openLaunchModal(l.id);
    el.appendChild(d);
  });
}

// ---------- dashboard ----------
// ---------- Media Library ----------
const mlState={ source:'all', q:'', offset:0, limit:60, loading:false, sourcesLoaded:false };
let mlSearchTimer=null;

async function renderMedia(){
  // density slider -> --mlmin (persisted)
  const dens=document.getElementById('ml-density');
  const savedDens=localStorage.getItem('dwlaunch:mlmin');
  if(savedDens){ dens.value=savedDens; }
  document.getElementById('ml-grid').style.setProperty('--mlmin', dens.value+'px');
  dens.oninput=()=>{ document.getElementById('ml-grid').style.setProperty('--mlmin',dens.value+'px');
    try{localStorage.setItem('dwlaunch:mlmin',dens.value);}catch(e){} };
  // search (debounced)
  const search=document.getElementById('ml-search');
  search.oninput=()=>{ clearTimeout(mlSearchTimer); mlSearchTimer=setTimeout(()=>{
    mlState.q=search.value.trim(); mlReset(); }, 300); };
  // dedup mode toggle (default = unique photos: hide byte-identical reuploads,
  // but keep the colorway names aggregated onto the surviving tile), persisted
  const dd=document.getElementById('ml-dedup');
  mlState.dedup = localStorage.getItem('dwlaunch:dedup') || 'image';
  dd.value=mlState.dedup;
  dd.onchange=()=>{ mlState.dedup=dd.value; try{localStorage.setItem('dwlaunch:dedup',dd.value);}catch(e){} mlReset(); };

  if(!mlState.sourcesLoaded) await mlLoadSources();
  mlReset();
}

async function mlLoadSources(){
  const tabs=document.getElementById('ml-sources');
  try{
    const d=await api('/api/media/sources');
    mlState.sourcesLoaded=true;
    tabs.innerHTML='';
    (d.sources||[]).forEach(s=>{
      const b=document.createElement('button');
      b.dataset.source=s.id;
      b.innerHTML=`${s.name}`+(s.count!=null?`<span class="ct">${s.count.toLocaleString()}</span>`:'');
      b.classList.toggle('active', s.id===mlState.source);
      b.onclick=()=>{ mlState.source=s.id; [...tabs.children].forEach(x=>x.classList.toggle('active',x===b)); mlReset(); };
      tabs.appendChild(b);
    });
    document.getElementById('ml-note').innerHTML =
      `Real assets from our <b>dw_unified</b> account — `+
      `${(d.counts.shopify||0).toLocaleString()} Shopify product images · `+
      `${(d.counts.instagram||0).toLocaleString()} of our Instagram posts · `+
      `${(d.counts.vendor||0).toLocaleString()} vendor / IG. Click <b>+ Attach</b> to add to the current compose.`;
  }catch(e){
    tabs.innerHTML='';
    document.getElementById('ml-note').innerHTML=
      `<span style="color:#e0a0a0">Media DB unreachable (${e.message}). The composer still works with file/URL attach.</span>`;
  }
}

// Normalised image identity for dedup. Collapses the same photo re-uploaded
// once per colorway/variant — Shopify suffixes a 4-hex hash to the filename
// (Y_Shagreen_WALLPAPER_e3e1.jpg, _f1c1.jpg, _3347.jpg…), so strip that +
// any ?v= cache-buster and key on the base filename.
function mlImgKey(u){
  if(!u) return u;
  try{ u=decodeURIComponent(u.replace(/^\/api\/media\/img\?u=/,'')); }catch(e){}
  u=u.split('?')[0];
  const base=(u.split('/').pop()||u).toLowerCase();
  return base.replace(/_[0-9a-f]{4}\.(jpg|jpeg|png|webp|gif)$/,'') || base;
}
// Dedup key per media item. For Shopify products, collapse a whole PATTERN to
// one tile (colorways/shots like "Darcy - Smoked Pearl", "Darcy - Vine Black",
// the _01/_02 reshoots all reuse the same room photo) by keying on
// vendor + pattern-name (title before the first " - " / " | "). Other sources
// (IG posts) have no such convention → fall back to image identity.
function mlItemKey(m){
  if(m.source==='shopify' && m.title){
    const pattern=m.title.split(/\s[-|]\s/)[0].trim().toLowerCase().replace(/\s+/g,' ');
    if(pattern) return 'p:'+(m.vendor||'').toLowerCase()+'|'+pattern;
  }
  return 'i:'+mlImgKey(m.url);
}

// colorway name from a product title: "Shagreen - Como Blue wallcovering | Zoffany" -> "Como Blue"
function mlColorway(m){
  if(!m || !m.title) return '';
  const parts=m.title.split(/\s-\s/);
  if(parts.length<2) return '';
  return parts.slice(1).join(' - ').replace(/\b(wall ?covering|wall ?paper|fabric)\b.*$/i,'').replace(/\|.*$/,'').trim();
}
// fold a duplicate item onto the already-shown tile: track its colorway and
// show a "· N colorways" chip so no colorway info is lost when photos collapse.
function mlFoldColorway(entry, m){
  const cw=mlColorway(m);
  if(cw && !entry.colorways.includes(cw)) entry.colorways.push(cw);
  const chip=entry.el.querySelector('.cw-chip');
  if(chip && entry.colorways.length>1){
    chip.style.display='';
    chip.textContent='+'+(entry.colorways.length-1)+' more colorway'+(entry.colorways.length-1===1?'':'s');
    chip.title=entry.colorways.join(' · ');
  }
}

function mlReset(){
  mlState.offset=0;
  mlState.seen=new Map();
  mlState.dups=0;
  document.getElementById('ml-grid').innerHTML='';
  document.getElementById('ml-status').textContent='';
  mediaLoadMore();
}

async function mediaLoadMore(){
  if(mlState.loading) return;
  mlState.loading=true;
  const btn=document.getElementById('ml-more-btn'), status=document.getElementById('ml-status');
  btn.disabled=true; status.textContent='Loading…';
  try{
    const qs=new URLSearchParams({source:mlState.source,q:mlState.q,limit:mlState.limit,offset:mlState.offset});
    const d=await api('/api/media?'+qs.toString());
    const grid=document.getElementById('ml-grid');
    if(!mlState.seen) mlState.seen=new Set();
    let added=0;
    const dedupOn = mlState.dedup && mlState.dedup!=='off';
    if(!(mlState.seen instanceof Map)) mlState.seen=new Map();
    (d.items||[]).forEach(m=>{
      if(dedupOn){
        // 'image' mode: dedup on the actual image content hash (byte-identical
        // colorway reuploads collapse) — fall back to filename if not hashed.
        const k = mlState.dedup==='pattern' ? mlItemKey(m)
                : (m.imgHash ? 'h:'+m.imgHash : 'i:'+mlImgKey(m.url));
        const ex=mlState.seen.get(k);
        if(ex){ mlState.dups=(mlState.dups||0)+1; mlFoldColorway(ex,m); return; }
        const card=mlCard(m);
        const entry={el:card, colorways:[mlColorway(m)].filter(Boolean)};
        mlState.seen.set(k, entry);
        grid.appendChild(card); added++;
        return;
      }
      grid.appendChild(mlCard(m)); added++;
    });
    mlState.offset += (d.items||[]).length;
    btn.style.display = d.hasMore ? '' : 'none';
    // if a whole page collapsed to duplicates, keep pulling so the grid grows
    if(added===0 && d.hasMore && mlState.offset < 600){ mlState.loading=false; return mediaLoadMore(); }
    if(grid.children.length===0){
      if(mlState.source==='vendor' && !mlState.q){
        grid.innerHTML=`<div class="ml-empty">No vendor Instagram posts ingested yet.<br>`+
          `<span class="muted">Handles for 20 major houses are seeded (Schumacher, Thibaut, de Gournay, Cole &amp; Son…). `+
          `Populate via Meta Graph (token) or by pasting post URLs — then real, cached images appear here.</span></div>`;
      } else {
        grid.innerHTML=`<div class="ml-empty muted">No matches.</div>`;
      }
    }
    const shown=grid.querySelectorAll('.ml-card').length;
    status.textContent = shown.toLocaleString()+' shown'+(mlState.dups?` · ${mlState.dups.toLocaleString()} duplicate${mlState.dups===1?'':'s'} hidden`:'');
  }catch(e){ status.textContent='Error: '+e.message; }
  finally{ mlState.loading=false; btn.disabled=false; }
}

function mlCard(m){
  const c=document.createElement('div'); c.className='ml-card';
  const srcLabel={shopify:'Shopify',instagram:'Instagram',vendor:'Vendor IG'}[m.source]||m.source;
  const safe=s=>(s||'').replace(/"/g,'&quot;').replace(/</g,'&lt;');
  // vendor IG urls are ALREADY a local cached path (/api/media/img?u=…) — use
  // as-is. http(s) urls: shopify CDN loads direct (fast); others via the proxy.
  const isLocal = /^\//.test(m.url);
  const proxied = isLocal ? m.url : '/api/media/img?u='+encodeURIComponent(m.url);
  const initialSrc = (m.source==='shopify') ? safe(m.url) : proxied;
  c.innerHTML=
    `<span class="src">${srcLabel}</span>`+
    `<img class="thumb" loading="lazy" src="${initialSrc}" alt="${safe(m.title)}" `+
      `data-raw="${safe(m.url)}" data-proxied="0">`+
    `<div class="meta"><div class="mt" title="${safe(m.title)}">${safe(m.title)}</div>`+
      (m.vendor?`<div class="mv">${safe(m.vendor)}</div>`:'')+
      `<div class="cw-chip" style="display:none"></div>`+
      (m.link?`<a class="mv" style="color:var(--gold-dim)" href="${safe(m.link)}" target="_blank" rel="noopener noreferrer">view post ↗</a>`:'')+`</div>`+
    `<div class="ml-acts"><button class="att">+ Attach</button><button class="rep">↻ Repost / Use</button></div>`;
  // proxy fallback on load error (one retry), then dim.
  const img=c.querySelector('img.thumb');
  img.onerror=function(){
    const raw=this.dataset.raw;
    if(this.dataset.proxied==='0' && /^https?:/.test(raw)){ this.dataset.proxied='1'; this.src='/api/media/img?u='+encodeURIComponent(raw); }
    else { this.style.opacity=.2; this.alt='(image unavailable)'; this.onerror=null; }
  };
  c.querySelector('.att').onclick=(e)=>{
    const b=e.currentTarget;
    state.global.media.push({type:guessType(m.url),ref:m.url,alt:m.title||'',name:(m.title||'').slice(0,40)});
    refreshMedia();
    b.textContent='✓ Attached'; b.classList.add('added');
    setTimeout(()=>{ b.textContent='+ Attach'; b.classList.remove('added'); },1600);
    toast('Added to compose — '+(state.global.media.length)+' media item'+(state.global.media.length>1?'s':''));
  };
  c.querySelector('.rep').onclick=()=>mlRepost(m);
  return c;
}

// Repost / Use — start a NEW draft launch pre-filled with this asset + an
// attribution line, then jump to Compose. Nothing posts live: it flows through
// the normal Save → Submit → Steve sign-off → (simulated) publish gate.
function mlRepost(m){
  const credit = m.source==='vendor'
    ? `\n\n📷 via ${m.vendor||'vendor'}${m.link?` — original: ${m.link}`:''}`
    : '';
  const body = (m.caption || m.title || '').slice(0,400) + credit;
  state = {
    id:null,
    title: (m.source==='vendor'?'Repost — ':'Use — ')+(m.title||'asset').slice(0,50),
    channelsSelected:['instagram'],
    global:{ body, media:[{type:guessType(m.url),ref:m.url,alt:m.title||'',name:(m.title||'').slice(0,40)}] },
    channels:{}, schedule:{mode:'draft',datetime:''}, status:'draft',
    approval:{approved:false,by:null,at:null}, published:false,
    repostOf: m.link||m.url, repostSource:m.source
  };
  activeChanTab=null; activePvTab=null;
  showView('compose'); hydrate();
  toast(m.source==='vendor'
    ? 'Repost draft started — review credit/caption, then Save → Submit for sign-off.'
    : 'Draft started from asset — edit, then Save.');
}

async function renderDashboard(){
  const dash=await api('/api/dashboard');
  const c=dash.counts;
  const cards=[
    {v:c.scheduledToday,k:'Scheduled today',cls:'gold'},
    {v:c.pendingApproval,k:'Pending approval',cls:'warn'},
    {v:c.publishedThisWeek,k:'Published this week',cls:'info'},
    {v:c.approved,k:'Approved (awaiting publish)',cls:'ok'},
    {v:c.drafts,k:'Drafts',cls:''},
    {v:c.totalLaunches,k:'Total launches',cls:''}
  ];
  document.getElementById('dash-stats').innerHTML = cards.map(x=>
    `<div class="stat"><div class="v ${x.cls}">${x.v}</div><div class="k">${x.k}</div></div>`).join('');
  updateApprovalsPill(c.pendingApproval+c.approved);
  const cg=document.getElementById('dash-channels'); cg.innerHTML='';
  dash.perChannel.forEach(p=>{
    const t=document.createElement('div'); t.className='chan-tile'+(p.connected?'':' off');
    t.innerHTML=`<div class="ch-head"><span class="av" style="background:${COLORS[p.id]}"></span>
        <span class="ch-name">${esc(p.name)}</span>
        ${p.connected?'':'<span class="connect" style="margin-left:auto;font-size:9px;color:var(--warn)">not connected</span>'}</div>
      <div class="ch-stats">
        <div><b>${p.scheduled}</b><span class="lab">sched</span></div>
        <div><b>${p.pending}</b><span class="lab">pending</span></div>
        <div><b>${p.published}</b><span class="lab">live</span></div>
        <div><b>${p.drafts}</b><span class="lab">draft</span></div>
      </div>`;
    t.onclick=()=>{ showView('streams'); };
    cg.appendChild(t);
  });
}
function updateApprovalsPill(n){
  const p=document.getElementById('appr-pill');
  if(n>0){ p.textContent=n; p.classList.remove('hide'); } else p.classList.add('hide');
}

// ---------- streams (TRUE Hootsuite model: user-addable columns) ----------
// Each column = { id, channel:'all'|chanId, type:streamType, q:'' }
let STREAM_TYPES=[]; // loaded from server
let hootCols=[];     // active columns (persisted)

const TYPE_LABEL={scheduled:'Scheduled',pending:'Pending approval',published:'Published / Sent',
  drafts:'Drafts',mentions:'Mentions',comments:'Comments',dms:'Direct messages',home:'Home feed',search:'Keyword / # search'};

function loadHootCols(){
  try{ const s=JSON.parse(localStorage.getItem('dwlaunch:hootCols')); if(Array.isArray(s)&&s.length) return s; }catch(e){}
  // sensible Hootsuite-style default board
  return [
    {id:cid(),channel:'all',type:'pending',q:''},
    {id:cid(),channel:'instagram',type:'mentions',q:''},
    {id:cid(),channel:'instagram',type:'comments',q:''},
    {id:cid(),channel:'all',type:'scheduled',q:''},
    {id:cid(),channel:'linkedin',type:'home',q:''},
    {id:cid(),channel:'all',type:'search',q:'grasscloth'}
  ];
}
function cid(){ return 'c'+Math.random().toString(36).slice(2,8); }
function saveHootCols(){ try{ localStorage.setItem('dwlaunch:hootCols',JSON.stringify(hootCols)); }catch(e){} }

async function renderStreams(){
  if(!STREAM_TYPES.length){ try{ STREAM_TYPES=(await api('/api/stream-types')).types; }catch(e){} }
  if(!hootCols.length) hootCols=loadHootCols();
  const board=document.getElementById('hoot-board'); board.innerHTML='';
  hootCols.forEach(col=>{
    const el=document.createElement('div'); el.className='hoot-col'; el.dataset.id=col.id; el.draggable=false;
    const chName=col.channel==='all'?'All accounts':chanName(col.channel);
    const dot=col.channel==='all'?'var(--gold)':(COLORS[col.channel]||'var(--gold)');
    const tLabel=TYPE_LABEL[col.type]||col.type;
    const sub=col.type==='search'&&col.q?`“${esc(col.q)}”`:tLabel;
    el.innerHTML=`
      <div class="hc-head" draggable="true">
        <span class="av" style="background:${dot}"></span>
        <span class="hc-tt"><span class="nm">${esc(chName)}</span><span class="ty">${esc(sub)}</span></span>
        <span class="ct">…</span>
        <span class="rm" title="Remove column">×</span>
      </div>
      <div class="hc-body"><div class="hc-empty">Loading…</div></div>`;
    board.appendChild(el);
    el.querySelector('.rm').onclick=()=>{ hootCols=hootCols.filter(c=>c.id!==col.id); saveHootCols(); renderStreams(); };
    wireColDrag(el);
    loadColumn(col, el);
  });
  // trailing add-column tile
  const add=document.createElement('div'); add.className='hoot-col';
  add.style.cssText='flex:0 0 200px;width:200px;align-items:center;justify-content:center;min-height:160px;cursor:pointer;border-style:dashed;background:transparent';
  add.innerHTML='<div style="text-align:center;color:var(--muted);padding:30px 10px"><div style="font-size:26px;color:var(--gold-dim)">+</div><div style="font-size:12px;margin-top:6px">Add stream</div></div>';
  add.onclick=openStreamPicker; board.appendChild(add);
}

async function loadColumn(col, el){
  const body=el.querySelector('.hc-body'); const ct=el.querySelector('.ct');
  let items=[];
  try{
    const j=await api(`/api/stream?channel=${encodeURIComponent(col.channel)}&type=${encodeURIComponent(col.type)}&q=${encodeURIComponent(col.q||'')}`);
    items=j.items||[];
  }catch(e){ body.innerHTML='<div class="hc-empty">Could not load.</div>'; ct.textContent='!'; return; }
  ct.textContent=items.length;
  if(!items.length){ body.innerHTML=`<div class="hc-empty">Nothing here yet${col.type==='search'&&col.q?` for “${esc(col.q)}”`:''}.</div>`; return; }
  body.innerHTML=items.map(it=>it.outbound?outboundCard(it):feedCard(it)).join('');
  body.querySelectorAll('[data-open]').forEach(x=>x.onclick=()=>openLaunchModal(x.getAttribute('data-open')));
  body.querySelectorAll('[data-reply]').forEach(x=>x.onclick=()=>replyToFeed(x.getAttribute('data-reply'), x.getAttribute('data-ch'), x.getAttribute('data-name')));
  body.querySelectorAll('[data-like]').forEach(x=>x.onclick=()=>{ x.textContent='♥ Liked'; x.style.color='var(--gold)'; toast('Simulated like — no live API called.'); });
}

function fmtShort(iso){ try{ return new Date(iso).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }catch(e){ return ''; } }

function feedCard(it){
  const initials=(it.author.name||'?').split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase();
  const canReply=it.kind==='mentions'||it.kind==='comments'||it.kind==='dms';
  return `<div class="feed-card${it.unread?' unread':''}">
    <div class="fc-top">
      <span class="fc-av" style="background:${it.author.color}">${initials}</span>
      <span><span class="fc-name">${esc(it.author.name)}</span><br><span class="fc-handle">@${esc(it.author.handle)} · ${chanName(it.channel)}</span></span>
      <span class="fc-when">${fmtShort(it.datetime)}</span>
    </div>
    <div class="fc-text">${esc(it.text)}</div>
    <div class="fc-actions">
      <span data-like>♥ ${it.likes||0}</span>
      ${canReply?`<span data-reply="${esc(it.id)}" data-ch="${esc(it.channel)}" data-name="${esc(it.author.handle)}">↩ Reply</span>`:`<span data-reply="${esc(it.id)}" data-ch="${esc(it.channel)}" data-name="">↗ Share to compose</span>`}
      <span class="fc-sim">sim</span>
    </div>
  </div>`;
}
function outboundCard(it){
  const when=it.datetime?fmtShort(it.datetime):(it.mode||'no schedule');
  return `<div class="stream-card" data-open="${esc(it.launchId)}" style="border-left-color:${COLORS[it.channel]||'var(--gold)'}" title="created ${esc(it.created_at||'')}">
    <div class="st-title">${esc(it.title)}</div>
    ${it.body?`<div class="st-body">${esc(it.body)}</div>`:''}
    <div class="st-meta">
      <span class="badge ${BADGE_MAP[it.status]||'b-draft'}">${(it.status||'draft').replace('_',' ')}</span>
      <span class="st-when">🕓 ${when} · ${chanName(it.channel)}</span>
    </div>
  </div>`;
}

// Reply / share an inbound item → opens Compose prefilled (stays a draft, simulated)
function replyToFeed(feedId, ch, handle){
  state=blankLaunch();
  if(ch&&ch!=='all'){ state.channelsSelected=[ch]; state.channels[ch]={body:'',media:[],fields:{},schedule:{mode:'schedule',datetime:''}}; }
  state.title = handle ? `Reply to @${handle}` : 'Shared from streams';
  state.global.body = handle ? `@${handle} ` : '';
  activeChanTab=ch&&ch!=='all'?ch:null; activePvTab=ch&&ch!=='all'?ch:null;
  showView('compose'); hydrate();
  toast(handle?`Replying to @${handle} — drafted (simulated, nothing sent).`:'Shared into Compose as a draft.');
  setTimeout(()=>{ const b=document.getElementById('global-body'); if(b){b.focus();b.setSelectionRange(b.value.length,b.value.length);} },60);
}

// ---- drag-to-reorder columns ----
let dragColId=null;
function wireColDrag(el){
  const head=el.querySelector('.hc-head');
  head.addEventListener('dragstart',e=>{ dragColId=el.dataset.id; el.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; });
  head.addEventListener('dragend',()=>{ el.classList.remove('dragging'); document.querySelectorAll('.hoot-col').forEach(c=>c.classList.remove('dragover')); });
  el.addEventListener('dragover',e=>{ if(!dragColId||dragColId===el.dataset.id) return; e.preventDefault(); el.classList.add('dragover'); });
  el.addEventListener('dragleave',()=>el.classList.remove('dragover'));
  el.addEventListener('drop',e=>{
    e.preventDefault(); el.classList.remove('dragover');
    if(!dragColId||dragColId===el.dataset.id) return;
    const from=hootCols.findIndex(c=>c.id===dragColId), to=hootCols.findIndex(c=>c.id===el.dataset.id);
    if(from<0||to<0) return;
    const [m]=hootCols.splice(from,1); hootCols.splice(to,0,m);
    dragColId=null; saveHootCols(); renderStreams();
  });
}

// ---- add-stream picker (modal-rig) ----
function openStreamPicker(){
  let pickCh='all', pickType='mentions';
  const chOpts=`<option value="all">All accounts</option>`+CHANNELS.map(c=>`<option value="${c.id}">${esc(c.name)}${c.connected?'':' (not connected)'}</option>`).join('');
  const typeCard=t=>`<div class="typeopt${t.id===pickType?' sel':''}" data-t="${t.id}"><div class="tn">${esc(TYPE_LABEL[t.id]||t.id)}</div><div class="tk">${t.kind}</div></div>`;
  const bodyHtml=`<div class="picker">
    <label>Account</label>
    <select id="pick-ch">${chOpts}</select>
    <label>Stream type</label>
    <div class="typegrid" id="pick-types">${(STREAM_TYPES.length?STREAM_TYPES:Object.keys(TYPE_LABEL).map(id=>({id,kind:''}))).map(typeCard).join('')}</div>
    <div id="pick-q-wrap" class="hide"><label>Keyword or #hashtag</label><input type="text" id="pick-q" placeholder="e.g. grasscloth, #DesignerWallcoverings"></div>
    <div style="display:flex;gap:8px;margin-top:18px">
      <button class="btn" id="pick-add" style="flex:1">Add column</button>
      <button class="btn ghost" id="pick-cancel">Cancel</button>
    </div>
  </div>`;
  const m=ModalRig.open({key:'stream-picker',title:'Add a stream',bodyHtml,width:440,height:520,bg:'var(--panel)'});
  const qWrap=m.body.querySelector('#pick-q-wrap');
  const syncQ=()=>qWrap.classList.toggle('hide',pickType!=='search');
  m.body.querySelector('#pick-ch').onchange=e=>pickCh=e.target.value;
  m.body.querySelectorAll('.typeopt').forEach(o=>o.onclick=()=>{
    pickType=o.getAttribute('data-t');
    m.body.querySelectorAll('.typeopt').forEach(x=>x.classList.toggle('sel',x===o));
    syncQ();
  });
  syncQ();
  m.body.querySelector('#pick-cancel').onclick=()=>m.close();
  m.body.querySelector('#pick-add').onclick=()=>{
    const q=(m.body.querySelector('#pick-q')||{}).value||'';
    hootCols.push({id:cid(),channel:pickCh,type:pickType,q:pickType==='search'?q.trim():''});
    saveHootCols(); m.close(); renderStreams();
    toast(`Added ${TYPE_LABEL[pickType]} column.`);
  };
}

// ---------- approvals ----------
async function renderApprovals(){
  const {launches}=await api('/api/launches');
  const queue=launches.filter(l=>l.status==='pending_approval'||l.status==='approved');
  updateApprovalsPill(queue.length);
  const el=document.getElementById('approvals-list');
  el.innerHTML = queue.length?'':'<span class="muted">Nothing awaiting sign-off. Submit a launch from Compose to populate this queue.</span>';
  queue.forEach(l=>{
    const pending=l.status==='pending_approval';
    const body=(l.global&&l.global.body)||'';
    const d=document.createElement('div'); d.className='appr-card'+(pending?' pending':'');
    d.innerHTML=`<div class="ac-main">
        <div class="ac-title">${esc(l.title)}</div>
        ${body?`<div class="ac-body">${esc(body)}</div>`:''}
        <div class="ac-meta">
          <span class="badge ${BADGE_MAP[l.status]}">${l.status.replace('_',' ')}</span>
          <span>${(l.channelsSelected||[]).map(chanName).join(' · ')||'no channels'}</span>
          <span title="${esc(l.created_at||'')}">🕓 ${fmtWhen(l.created_at)}</span>
          ${l.approval&&l.approval.approved?`<span style="color:#5ab880">✓ ${esc(l.approval.by)} · ${fmtWhen(l.approval.at)}</span>`:''}
        </div>
      </div>
      <div class="ac-actions">
        <button class="btn ghost" data-act="open" data-id="${esc(l.id)}">Open</button>
        ${(l.published||l.status==='published')?`<span class="muted" style="color:#5ab880">✓ Published</span>`
          :`<button class="btn btn-publish" data-act="publish" data-id="${esc(l.id)}">⚡ Publish Now</button>`}
      </div>`;
    el.appendChild(d);
  });
  el.querySelectorAll('button[data-act]').forEach(b=>{
    const id=b.getAttribute('data-id'), act=b.getAttribute('data-act');
    b.onclick=async()=>{
      if(act==='open') return openLaunchModal(id);
      if(act==='approve'){ await approveById(id); renderApprovals(); }
      if(act==='publish'){ await publishById(id); renderApprovals(); }
    };
  });
}

// ---------- id-based approval/publish (used by Approvals + modal) ----------
async function approveById(id){
  const token=prompt('Steve sign-off required to unlock publish. Enter sign-off token (STEVE-GO):');
  if(token===null) return false;
  try{
    const j=await api(`/api/launches/${id}/approve`,{method:'POST',body:JSON.stringify({token,by:'Steve'})});
    if(state&&state.id===id) state=j.launch;
    toast('Signed off ✓ — Publish Now posts live to connected channels.'); return true;
  }catch(e){ toast('Sign-off rejected: '+e.message,true); return false; }
}
async function publishById(id){
  try{
    const j=await api(`/api/launches/${id}/publish`,{method:'POST'});
    if(state&&state.id===id) state=j.launch;
    toast(j.note||'Published.', j.live===false); return true;
  }catch(e){ toast('Publish failed: '+e.message,true); return false; }
}

// ---------- launch as movable modal (modal-rig) ----------
async function openLaunchModal(id){
  let l;
  try{ l=(await api('/api/launches/'+id)).launch; }
  catch(e){ toast('Could not load launch.',true); return; }
  const approved=l.approval&&l.approval.approved;
  const chans=(l.channelsSelected&&l.channelsSelected.length)?l.channelsSelected:Object.keys(l.channels||{});
  const chanRows=chans.map(ch=>{
    const c=(l.channels&&l.channels[ch])||{};
    const cs=c.schedule||l.schedule||{};
    const when=cs.datetime?new Date(cs.datetime).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):(cs.mode||'—');
    const body=c.body||l.global?.body||'';
    return `<div style="border:1px solid var(--line-soft);border-radius:6px;padding:10px 12px;margin-bottom:8px;background:var(--panel2)">
      <div style="display:flex;align-items:center;gap:8px;margin-bottom:5px">
        <span style="width:8px;height:8px;border-radius:50%;background:${COLORS[ch]}"></span>
        <strong style="font-size:12px">${esc(chanName(ch))}</strong>
        <span class="muted" style="margin-left:auto;font-size:10px">🕓 ${esc(when)}</span>
      </div>
      <div style="font-size:12px;color:var(--ink-2);white-space:pre-wrap;line-height:1.45">${esc(body.slice(0,400))}${body.length>400?'…':''}</div>
    </div>`;
  }).join('') || '<span class="muted">No channels selected.</span>';

  const bodyHtml=`
    <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
      <span class="badge ${BADGE_MAP[l.status]||'b-draft'}">${(l.status||'draft').replace('_',' ')}</span>
      <span class="muted" style="font-size:11px" title="${esc(l.created_at||'')}">created 🕓 ${fmtWhen(l.created_at)}</span>
      ${approved?`<span class="muted" style="font-size:11px;color:#5ab880">✓ signed off by ${esc(l.approval.by)}</span>`:''}
    </div>
    <div id="ml-sec-head" style="font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--gold-dim);margin-bottom:8px">Per-channel</div>
    <div id="ml-sec-body">${chanRows}</div>
    <div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap">
      <button class="btn ghost" id="ml-edit">Load into Compose</button>
      ${(l.published||l.status==='published')
        ?`<span class="muted" style="align-self:center;color:#5ab880">✓ Published</span>`
        :`<button class="btn btn-publish" id="ml-publish">⚡ Publish Now</button>`}
    </div>`;

  const m=ModalRig.open({ key:'launch-viewer', title:l.title||'Launch', bodyHtml, width:560, height:560, bg:'var(--panel)' });
  const sh=m.body.querySelector('#ml-sec-head'), sb=m.body.querySelector('#ml-sec-body');
  if(sh&&sb) ModalRig.section(sh, sb, 'launch-viewer:perchan');
  const edit=m.body.querySelector('#ml-edit');
  if(edit) edit.onclick=()=>{ state=l; activeChanTab=null; activePvTab=null; m.close(); showView('compose'); hydrate(); toast('Loaded: '+l.title); };
  const sub=m.body.querySelector('#ml-submit');
  if(sub) sub.onclick=async()=>{ try{ await api(`/api/launches/${id}/submit-approval`,{method:'POST'}); m.close(); toast('Submitted for approval.'); refreshActiveView(); }catch(e){ toast('Failed: '+e.message,true); } };
  const appr=m.body.querySelector('#ml-approve');
  if(appr) appr.onclick=async()=>{ if(await approveById(id)){ m.close(); refreshActiveView(); } };
  const pub=m.body.querySelector('#ml-publish');
  if(pub) pub.onclick=async()=>{ if(await publishById(id)){ m.close(); refreshActiveView(); } };
}
function refreshActiveView(){
  const v=localStorage.getItem('dwlaunch:view')||'compose';
  if(v==='dashboard')renderDashboard(); else if(v==='streams')renderStreams();
  else if(v==='drafts')renderDrafts(); else if(v==='approvals')renderApprovals();
  else if(v==='compose'){ renderState(); }
}

// ---------- density sliders (persisted) ----------
function wireDensity(){
  // Hootsuite board — column width
  const hw=document.getElementById('hoot-width');
  if(hw){
    const savedW=localStorage.getItem('dwlaunch:hootWidth');
    if(savedW) hw.value=savedW;
    const applyW=()=>document.documentElement.style.setProperty('--colw',hw.value+'px');
    applyW();
    hw.addEventListener('input',()=>{ applyW(); localStorage.setItem('dwlaunch:hootWidth',hw.value); });
  }

  const dd=document.getElementById('drafts-density');
  const savedDraft=localStorage.getItem('dwlaunch:draftMin');
  if(savedDraft){ dd.value=savedDraft; }
  const applyDraft=()=>{ document.documentElement.style.setProperty('--draftmin',dd.value+'px'); document.getElementById('drafts-list').style.setProperty('--chanmin',dd.value+'px'); };
  applyDraft();
  dd.addEventListener('input',()=>{ applyDraft(); localStorage.setItem('dwlaunch:draftMin',dd.value); });
}

function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }

// wire global media drop
wireDrop('global-drop','global-file',()=>state.global.media);
wireDensity();
init().then(()=>{
  // deep-link: ?media=<query> opens the Media → Vendor IG tab pre-filtered
  // (e.g. /?media=zoffany). ?source= overrides the source (default vendor).
  const params=new URLSearchParams(location.search);
  if(params.has('media')){
    const qv=params.get('media')||'';
    mlState.source=params.get('source')||'vendor';
    mlState.q=qv;
    showView('media');
    const box=document.getElementById('ml-search'); if(box) box.value=qv;
    return;
  }
  // restore last-used view (persisted), default Compose
  const saved=localStorage.getItem('dwlaunch:view');
  if(!window.__dlCompose && saved && VIEWS.includes(saved) && saved!=='compose') showView(saved);
  else { // refresh the approvals pill on load even when starting on Compose
    api('/api/dashboard').then(d=>updateApprovalsPill((d.counts.pendingApproval||0)+(d.counts.approved||0))).catch(()=>{});
  }
});
</script>
</body>
</html>