[object Object]

← back to Marketing Command Center

nav-agent: universal grid-controls drop-in on internal dashboard/command-center pages (STRONG-only)

7eaa41feb2f12ca7241096b0b3caf89aafbcaea5 · 2026-08-13 09:27:26 -0700 · Steve Abrams

Files touched

Diff

commit 7eaa41feb2f12ca7241096b0b3caf89aafbcaea5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 09:27:26 2026 -0700

    nav-agent: universal grid-controls drop-in on internal dashboard/command-center pages (STRONG-only)
---
 public/ig-activity.html                 |   2 +
 public/nav-agent/nav-agent.css          | 100 +++++++
 public/nav-agent/nav-agent.js           | 478 ++++++++++++++++++++++++++++++++
 public/panels/ab-tests.html             |   2 +
 public/panels/browse-abandon.html       |   2 +
 public/panels/channels.html             |   2 +
 public/panels/constant-contact.html     |   2 +
 public/panels/copy.html                 |   2 +
 public/panels/follow-counts.html        |   2 +
 public/panels/journeys.html             |   2 +
 public/panels/performance.html          |   2 +
 public/panels/playbook.html             |   2 +
 public/panels/profiles.html             |   2 +
 public/panels/segment-perf.html         |   2 +
 public/panels/segments.html             |   2 +
 public/panels/send-times.html           |   2 +
 public/panels/templates.html            |   2 +
 scripts/li-cma-watch.py                 | 138 +++++++++
 videos/mcc-streamline-promo/square.html | 195 -------------
 19 files changed, 746 insertions(+), 195 deletions(-)

diff --git a/public/ig-activity.html b/public/ig-activity.html
index 50d7292..5557b04 100644
--- a/public/ig-activity.html
+++ b/public/ig-activity.html
@@ -48,6 +48,7 @@
   tr:hover td{background:#161410}
   .empty{color:var(--mut);text-align:center;padding:80px 20px}
 </style>
+  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
 </head>
 <body>
 <header>
@@ -150,5 +151,6 @@ async function load(){
 }
 load(); setInterval(load,60000);
 </script>
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
 </body>
 </html>
diff --git a/public/nav-agent/nav-agent.css b/public/nav-agent/nav-agent.css
new file mode 100644
index 0000000..844d787
--- /dev/null
+++ b/public/nav-agent/nav-agent.css
@@ -0,0 +1,100 @@
+/* ============================================================================
+ * Nav Agent — universal grid-controls drop-in  (v1.0)
+ * Left field-toggle panel + sort-every-field + grouping + movable/removable/
+ * expandable data panels.  Zero dependencies. Additive & defensive.
+ * Honors the house internal-line-viewer spec (left panel, density type-scale,
+ * tight cards, localStorage-persisted).
+ * ==========================================================================*/
+
+:root{
+  --na-acc:var(--acc,#1f9e8f);
+  --na-line:var(--line,#2a2f3a);
+  --na-bg:var(--panel,#12151c);
+  --na-bg2:var(--bg,#0c0e13);
+  --na-fg:var(--fg,#e7e9ee);
+  --na-mut:var(--mut,#8b93a3);
+}
+
+/* ---- launcher (bottom-left cog) --------------------------------------- */
+.na-launch{position:fixed;left:14px;bottom:14px;z-index:2147483000;
+  width:42px;height:42px;border-radius:50%;border:1px solid var(--na-line);
+  background:var(--na-bg);color:var(--na-fg);font-size:19px;cursor:pointer;
+  display:flex;align-items:center;justify-content:center;
+  box-shadow:0 4px 18px rgba(0,0,0,.45);transition:transform .15s}
+.na-launch:hover{transform:scale(1.08);border-color:var(--na-acc)}
+
+/* ---- left field/sort/group panel -------------------------------------- */
+.na-panel{position:fixed;left:0;top:0;bottom:0;width:290px;z-index:2147483001;
+  background:var(--na-bg);border-right:1px solid var(--na-line);color:var(--na-fg);
+  display:flex;flex-direction:column;font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
+  transform:translateX(-100%);transition:transform .2s ease;box-shadow:2px 0 24px rgba(0,0,0,.4)}
+.na-panel.open{transform:translateX(0)}
+.na-panel header{display:flex;align-items:center;gap:8px;padding:12px 14px;
+  border-bottom:1px solid var(--na-line);font-weight:600;letter-spacing:.3px}
+.na-panel header .na-title{flex:1}
+.na-panel header button{background:none;border:0;color:var(--na-mut);cursor:pointer;font-size:16px;padding:2px 6px;border-radius:4px}
+.na-panel header button:hover{color:var(--na-fg);background:rgba(255,255,255,.06)}
+.na-body{flex:1;overflow:auto;padding:8px 12px 40px}
+.na-sec{border-top:1px solid var(--na-line);padding:10px 0}
+.na-sec:first-child{border-top:0}
+.na-sec>.na-h{display:flex;align-items:center;gap:6px;cursor:pointer;
+  font-weight:600;color:var(--na-mut);text-transform:uppercase;font-size:11px;letter-spacing:.6px;user-select:none}
+.na-sec>.na-h::before{content:'▸';font-size:9px;transition:transform .15s}
+.na-sec.open>.na-h::before{transform:rotate(90deg)}
+.na-sec .na-c{display:none;padding:8px 2px 2px}
+.na-sec.open .na-c{display:block}
+.na-search{width:100%;box-sizing:border-box;background:var(--na-bg2);border:1px solid var(--na-line);
+  color:var(--na-fg);border-radius:6px;padding:8px 10px;margin:4px 0 10px;font-size:13px}
+.na-fieldrow{display:flex;align-items:center;gap:8px;padding:4px 2px;cursor:pointer;border-radius:4px}
+.na-fieldrow:hover{background:rgba(255,255,255,.04)}
+.na-fieldrow input{accent-color:var(--na-acc);cursor:pointer}
+.na-fieldrow .na-cnt{margin-left:auto;color:var(--na-mut);font-size:11px}
+.na-row{display:flex;gap:6px;align-items:center;margin:6px 0}
+.na-row label{color:var(--na-mut);min-width:52px}
+.na-row select,.na-row input[type=range]{flex:1;background:var(--na-bg2);border:1px solid var(--na-line);
+  color:var(--na-fg);border-radius:6px;padding:6px 8px;font-size:13px}
+.na-mini{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
+.na-mini button{background:var(--na-bg2);border:1px solid var(--na-line);color:var(--na-fg);
+  border-radius:6px;padding:5px 9px;font-size:12px;cursor:pointer}
+.na-mini button:hover,.na-mini button.on{border-color:var(--na-acc);color:var(--na-acc)}
+.na-reset{width:100%;margin-top:14px;background:var(--na-bg2);border:1px solid var(--na-line);
+  color:var(--na-fg);border-radius:6px;padding:9px;cursor:pointer;font-size:13px}
+.na-reset:hover{border-color:#c0563f;color:#e08a76}
+.na-note{color:var(--na-mut);font-size:11px;padding:2px;line-height:1.5}
+
+/* ---- grouping headers injected into the grid -------------------------- */
+.na-group-h{grid-column:1/-1;display:flex;align-items:center;gap:8px;
+  padding:8px 4px;margin:6px 0 2px;border-bottom:1px solid var(--na-line);
+  color:var(--na-acc);font-weight:600;font-size:13px;cursor:pointer;user-select:none}
+.na-group-h .na-gcount{color:var(--na-mut);font-weight:400}
+.na-group-h.collapsed + .na-group-body{display:none}
+table .na-group-h{display:table-row}
+table .na-group-h td{padding:8px 6px;color:var(--na-acc);font-weight:600;border-bottom:1px solid var(--na-line)}
+
+/* ---- movable / resizable / removable data-panel chrome ---------------- */
+.na-wrap{position:relative}
+.na-bar{display:flex;align-items:center;gap:6px;padding:4px 8px;cursor:move;
+  background:var(--na-bg);border:1px solid var(--na-line);border-bottom:0;
+  border-radius:6px 6px 0 0;color:var(--na-mut);font-size:12px;user-select:none}
+.na-bar .na-bt{flex:1;font-weight:600;color:var(--na-fg)}
+.na-bar button{background:none;border:0;color:var(--na-mut);cursor:pointer;font-size:14px;padding:1px 5px;border-radius:4px}
+.na-bar button:hover{color:var(--na-fg);background:rgba(255,255,255,.08)}
+.na-wrap.na-float{position:fixed;z-index:2000;box-shadow:0 10px 40px rgba(0,0,0,.55);
+  background:var(--na-bg2);border-radius:6px;resize:both;overflow:auto;min-width:280px;min-height:160px}
+.na-wrap.na-collapsed .na-inner{display:none}
+.na-wrap.na-removed{display:none}
+.na-wrap.na-max{position:fixed;inset:12px;z-index:2001;background:var(--na-bg2);
+  box-shadow:0 10px 60px rgba(0,0,0,.6);overflow:auto;resize:none}
+.na-restorebar{position:fixed;left:14px;bottom:66px;z-index:2147483000;display:flex;flex-direction:column;gap:6px}
+.na-restorebar button{background:var(--na-bg);border:1px solid var(--na-line);color:var(--na-fg);
+  border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer}
+.na-restorebar button:hover{border-color:var(--na-acc)}
+
+/* ---- merged mode: folded INTO an existing house panel ----------------- */
+.na-merge{border-top:1px dashed var(--na-line);margin-top:12px;padding-top:10px}
+.na-merge .na-mh{display:flex;align-items:center;gap:6px;font-weight:600;color:var(--na-acc);
+  text-transform:uppercase;font-size:11px;letter-spacing:.6px;margin-bottom:8px}
+.na-merge .na-row label{color:var(--na-mut);min-width:48px}
+.na-merge select{background:var(--na-bg2);border:1px solid var(--na-line);color:var(--na-fg);border-radius:6px;padding:6px 8px;font-size:13px;flex:1}
+
+@media (max-width:640px){.na-panel{width:86vw}}
diff --git a/public/nav-agent/nav-agent.js b/public/nav-agent/nav-agent.js
new file mode 100644
index 0000000..991a82f
--- /dev/null
+++ b/public/nav-agent/nav-agent.js
@@ -0,0 +1,478 @@
+/* ============================================================================
+ * Nav Agent — universal grid-controls drop-in  (v1.0)
+ *
+ * Drop one <script src=".../nav-agent.js" defer></script> (+ the CSS) onto any
+ * page. It auto-detects the primary data grid (a <table> or a .grid/.card list),
+ * then injects:
+ *   • a LEFT collapsible panel: text search, per-FIELD show/hide toggles,
+ *     sort-by-any-field (asc/desc), group-by-any-field, density slider
+ *   • movable / resizable / collapsible / removable / maximizable panel chrome
+ *     on the grid itself
+ * All state persists to localStorage, namespaced per page path, so every
+ * surface remembers its own layout. Strictly additive & wrapped in try/catch —
+ * if it can't understand a page it degrades (panel chrome + search + density)
+ * and never throws into the host page.
+ *
+ * Manual control:  window.NavAgent.mount({ target:'#grid', title:'Deals' })
+ * Opt-out a page:  <body data-nav-agent="off">
+ * Force a target:  <div id="grid" data-nav-grid> ... </div>
+ * ==========================================================================*/
+(function () {
+  'use strict';
+  if (window.NavAgent) return;
+
+  var _uid = 0;
+  var navKey = function (target) {
+    if (!target.getAttribute('data-na-uid')) target.setAttribute('data-na-uid', target.id || ('g' + (++_uid)));
+    return 'navagent:' + location.pathname + ':' + target.getAttribute('data-na-uid');
+  };
+  var load = function (key) { try { return JSON.parse(localStorage.getItem(key)) || {}; } catch (e) { return {}; } };
+  var save = function (key, s) { try { localStorage.setItem(key, JSON.stringify(s)); } catch (e) {} };
+  var slug = function (t) { return String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'f'; };
+  var txt = function (el) { return (el && el.textContent || '').trim(); };
+  // magnitude-aware numeric parse — $2.1B > $780M > 42.  Handles K/M/B/T + commas.
+  var num = function (v) {
+    var s = String(v).replace(/,/g, '').trim();
+    var m = s.match(/-?\d*\.?\d+/); if (!m) return null;
+    var n = parseFloat(m[0]); if (isNaN(n)) return null;
+    var suf = (s.slice(m.index + m[0].length).match(/[kmbt]/i) || [''])[0].toLowerCase();
+    var mult = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }[suf] || 1;
+    return n * mult;
+  };
+  var el = function (tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; };
+
+  /* -- target detection -------------------------------------------------- */
+  function findTarget(opt) {
+    if (opt && opt.target) return document.querySelector(opt.target);
+    var q = document.querySelector('[data-nav-grid]');
+    if (q) return q;
+    // biggest visible table wins, else #grid, else first .grid, else .cards
+    var tables = [].slice.call(document.querySelectorAll('table'))
+      .filter(function (t) { return t.querySelector('tbody tr') && t.offsetParent !== null; })
+      .sort(function (a, b) { return b.querySelectorAll('tr').length - a.querySelectorAll('tr').length; });
+    if (tables[0] && tables[0].querySelectorAll('tr').length > 2) return tables[0];
+    return document.querySelector('#grid') || document.querySelector('.grid') ||
+           document.querySelector('.cards') || tables[0] || null;
+  }
+
+  /* -- field model ------------------------------------------------------- */
+  // Returns { kind:'table'|'cards', fields:[{key,label,idx?,cls?}], items() }
+  function model(target) {
+    if (target.tagName === 'TABLE') {
+      var ths = [].slice.call(target.querySelectorAll('thead th, thead td'));
+      if (!ths.length) { // header-less table: synthesize from first row
+        var r0 = target.querySelector('tbody tr, tr');
+        ths = r0 ? [].slice.call(r0.children).map(function (_, i) { var s = el('th'); s.textContent = 'Col ' + (i + 1); return s; }) : [];
+      }
+      var fields = ths.map(function (th, i) { return { key: slug(txt(th)) + '-' + i, label: txt(th) || ('Col ' + (i + 1)), idx: i }; });
+      return {
+        kind: 'table', fields: fields,
+        rows: function () { return [].slice.call(target.querySelectorAll('tbody tr')).filter(function (r) { return !r.classList.contains('na-group-h'); }); },
+        cellText: function (row, f) { var c = row.children[f.idx]; return txt(c); }
+      };
+    }
+    // card grid — infer fields from repeated child element classes / [data-field]
+    var cards = [].slice.call(target.children).filter(function (c) { return c.nodeType === 1 && !c.classList.contains('na-group-h') && !c.classList.contains('na-group-body'); });
+    var sample = cards.slice(0, 24), seen = {}, order = [];
+    sample.forEach(function (card) {
+      [].slice.call(card.querySelectorAll('[data-field]')).forEach(function (n) {
+        var k = n.getAttribute('data-field'); if (!seen[k]) { seen[k] = { key: slug(k), label: k, cls: '[data-field="' + k + '"]', df: k, hits: 0 }; order.push(seen[k]); } seen[k].hits++;
+      });
+      // also collect direct-child element classes as coarse fields when no data-field present
+      [].slice.call(card.children).forEach(function (n) {
+        var cls = (n.className && typeof n.className === 'string') ? n.className.split(/\s+/)[0] : '';
+        if (!cls || cls.indexOf('na-') === 0) return;
+        var k = 'c-' + cls;
+        if (!seen[k]) { seen[k] = { key: slug(cls), label: cls.replace(/[-_]/g, ' '), cls: '.' + cls, hits: 0 }; order.push(seen[k]); } seen[k].hits++;
+      });
+    });
+    // keep fields present on a majority of sampled cards (stable structure)
+    var min = Math.max(1, Math.floor(sample.length * 0.4));
+    var fields = order.filter(function (f) { return f.hits >= min; });
+    return {
+      kind: 'cards', cards: true, fields: fields,
+      rows: function () { return [].slice.call(target.children).filter(function (c) { return c.nodeType === 1 && !c.classList.contains('na-group-h') && !c.classList.contains('na-group-body') && c.offsetParent !== null || (c.nodeType === 1 && !c.classList.contains('na-group-h')); }); },
+      cellText: function (card, f) { var n = f.df ? card.querySelector('[data-field="' + f.df + '"]') : card.querySelector(f.cls); return txt(n); }
+    };
+  }
+
+  /* -- one instance ------------------------------------------------------ */
+  function Agent(target, opt) {
+    var key = navKey(target);
+    var st = load(key);
+    var M = model(target);
+    var self = { target: target, model: M, state: st, key: key };
+    var applyingSort = false;
+
+    /* field show/hide via CONTAINER classes (survives host re-renders) */
+    function fieldStyleTag() {
+      var id = 'na-style-' + key.replace(/[^a-z0-9]/gi, '');
+      var tag = document.getElementById(id) || (function () { var t = el('style'); t.id = id; document.head.appendChild(t); return t; })();
+      var css = '';
+      (st.hidden || []).forEach(function (key) {
+        var f = M.fields.filter(function (x) { return x.key === key; })[0]; if (!f) return;
+        if (M.kind === 'table') {
+          css += 'table[data-na] thead th:nth-child(' + (f.idx + 1) + '),table[data-na] tbody td:nth-child(' + (f.idx + 1) + '){display:none}';
+        } else {
+          css += target.getAttribute('data-na-sel') + ' ' + f.cls + '{display:none}';
+        }
+      });
+      tag.textContent = css;
+    }
+
+    function applySort() {
+      if (!st.sort) return;
+      var f = M.fields.filter(function (x) { return x.key === st.sort; })[0]; if (!f) return;
+      var dir = st.dir === 'desc' ? -1 : 1;
+      var rows = M.rows();
+      if (!rows.length) return;
+      applyingSort = true;
+      // detect numeric
+      var numeric = rows.slice(0, 12).every(function (r) { var v = M.cellText(r, f); return v === '' || num(v) !== null; });
+      rows.sort(function (a, b) {
+        var va = M.cellText(a, f), vb = M.cellText(b, f);
+        if (numeric) { var na = num(va), nb = num(vb); na = na == null ? -Infinity : na; nb = nb == null ? -Infinity : nb; return (na - nb) * dir; }
+        return va.localeCompare(vb, undefined, { numeric: true }) * dir;
+      });
+      var parent = M.kind === 'table' ? target.querySelector('tbody') : target;
+      rows.forEach(function (r) { parent.appendChild(r); });
+      applyGroup(); // grouping depends on sorted order
+      applyingSort = false;
+    }
+
+    function clearGroups() {
+      [].slice.call(target.querySelectorAll('.na-group-h')).forEach(function (n) { n.remove(); });
+    }
+
+    function applyGroup() {
+      clearGroups();
+      if (!st.group) return;
+      var f = M.fields.filter(function (x) { return x.key === st.group; })[0]; if (!f) return;
+      var rows = M.rows(), last = null, parent = M.kind === 'table' ? target.querySelector('tbody') : target;
+      var counts = {};
+      rows.forEach(function (r) { var v = M.cellText(r, f) || '—'; counts[v] = (counts[v] || 0) + 1; });
+      rows.forEach(function (r) {
+        var v = M.cellText(r, f) || '—';
+        if (v !== last) {
+          var h;
+          if (M.kind === 'table') { h = el('tr', 'na-group-h'); var td = el('td'); td.colSpan = M.fields.length; td.innerHTML = '▸ ' + esc(v) + ' <span class="na-gcount">(' + counts[v] + ')</span>'; h.appendChild(td); }
+          else { h = el('div', 'na-group-h', '▸ ' + esc(v) + ' <span class="na-gcount">(' + counts[v] + ')</span>'); }
+          parent.insertBefore(h, r); last = v;
+        }
+      });
+    }
+
+    function applySearch() {
+      var qraw = (st.q || '').toLowerCase();
+      M.rows().forEach(function (r) {
+        var show = !qraw || r.textContent.toLowerCase().indexOf(qraw) >= 0;
+        r.style.display = show ? '' : 'none';
+      });
+    }
+
+    function applyAll() { fieldStyleTag(); applySort(); applySearch(); }
+
+    /* re-apply after host re-renders (debounced, ignores our own mutations).
+       If the FIELD SET changed (async data arrived, filter repopulated), tell
+       mount() to rebuild the panel UI. */
+    function fieldSig(m) { return m.kind + ':' + m.fields.map(function (f) { return f.key; }).join(','); }
+    var sig = fieldSig(M);
+    var t;
+    var mo = new MutationObserver(function () {
+      if (applyingSort) return;
+      clearTimeout(t); t = setTimeout(function () {
+        M = model(target); self.model = M;
+        var ns = fieldSig(M);
+        if (ns !== sig) { sig = ns; if (self.onFields) try { self.onFields(); } catch (e) {} }
+        applyAll();
+      }, 140);
+    });
+    try { mo.observe(M.kind === 'table' ? (target.querySelector('tbody') || target) : target, { childList: true }); } catch (e) {}
+
+    self.applyAll = applyAll;
+    self.applySort = applySort;
+    self.applyGroup = applyGroup;
+    self.applySearch = applySearch;
+    self.save = function () { save(key, st); };
+    return self;
+  }
+
+  function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); }
+
+  /* -- panel UI ---------------------------------------------------------- */
+  function buildPanel(agent, opt) {
+    var M = agent.model, st = agent.state;
+    var panel = el('aside', 'na-panel');
+    panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'View controls');
+    var title = (opt && opt.title) || document.title || 'View';
+    panel.appendChild(el('header', null,
+      '<span class="na-title">⚙ View · ' + esc(title) + '</span>' +
+      '<button class="na-x" title="Close">✕</button>'));
+    var body = el('div', 'na-body'); panel.appendChild(body);
+
+    // search
+    var search = el('input', 'na-search'); search.type = 'search'; search.placeholder = 'Search all fields…';
+    search.value = st.q || '';
+    search.addEventListener('input', function () { st.q = search.value; agent.save(); agent.applySearch(); });
+    body.appendChild(search);
+
+    function section(label, openDefault) {
+      var s = el('div', 'na-sec' + (openDefault ? ' open' : ''));
+      s.appendChild(el('div', 'na-h', label));
+      var c = el('div', 'na-c'); s.appendChild(c);
+      s.querySelector('.na-h').addEventListener('click', function () { s.classList.toggle('open'); });
+      body.appendChild(s); return c;
+    }
+
+    var haveFields = M.fields && M.fields.length;
+
+    // FIELDS toggle
+    if (haveFields) {
+      var fc = section('Fields (' + M.fields.length + ')', true);
+      st.hidden = st.hidden || [];
+      M.fields.forEach(function (f) {
+        var row = el('label', 'na-fieldrow');
+        var cb = el('input'); cb.type = 'checkbox'; cb.checked = st.hidden.indexOf(f.key) < 0;
+        cb.addEventListener('change', function () {
+          var i = st.hidden.indexOf(f.key);
+          if (cb.checked && i >= 0) st.hidden.splice(i, 1);
+          else if (!cb.checked && i < 0) st.hidden.push(f.key);
+          agent.save(); agent.applyAll();
+        });
+        row.appendChild(cb);
+        row.appendChild(el('span', null, esc(f.label)));
+        fc.appendChild(row);
+      });
+      var mini = el('div', 'na-mini',
+        '<button data-all>All</button><button data-none>None</button>');
+      mini.querySelector('[data-all]').onclick = function () { st.hidden = []; agent.save(); agent.applyAll(); refreshChecks(); };
+      mini.querySelector('[data-none]').onclick = function () { st.hidden = M.fields.map(function (f) { return f.key; }); agent.save(); agent.applyAll(); refreshChecks(); };
+      fc.appendChild(mini);
+      function refreshChecks() { [].slice.call(fc.querySelectorAll('.na-fieldrow input')).forEach(function (cb, i) { cb.checked = st.hidden.indexOf(M.fields[i].key) < 0; }); }
+    }
+
+    // SORT
+    if (haveFields) {
+      var sc = section('Sort', true);
+      var srow = el('div', 'na-row', '<label>Field</label>');
+      var sel = el('select'); sel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { sel.appendChild(new Option(f.label, f.key)); });
+      sel.value = st.sort || ''; srow.appendChild(sel); sc.appendChild(srow);
+      var dir = el('div', 'na-mini', '<button data-asc>▲ Asc</button><button data-desc>▼ Desc</button>');
+      function markDir() { dir.querySelector('[data-asc]').classList.toggle('on', st.dir !== 'desc'); dir.querySelector('[data-desc]').classList.toggle('on', st.dir === 'desc'); }
+      sel.addEventListener('change', function () { st.sort = sel.value; agent.save(); agent.applySort(); });
+      dir.querySelector('[data-asc]').onclick = function () { st.dir = 'asc'; markDir(); agent.save(); agent.applySort(); };
+      dir.querySelector('[data-desc]').onclick = function () { st.dir = 'desc'; markDir(); agent.save(); agent.applySort(); };
+      markDir(); sc.appendChild(dir);
+    }
+
+    // GROUP
+    if (haveFields) {
+      var gc = section('Group by', false);
+      var grow = el('div', 'na-row', '<label>Field</label>');
+      var gsel = el('select'); gsel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { gsel.appendChild(new Option(f.label, f.key)); });
+      gsel.value = st.group || ''; grow.appendChild(gsel); gc.appendChild(grow);
+      gsel.addEventListener('change', function () { st.group = gsel.value; agent.save(); agent.applyGroup(); });
+    }
+
+    // DENSITY (only if the target reacts to --cols or has a page density var)
+    var dc = section('Density', false);
+    var drow = el('div', 'na-row', '<label>Cols</label>');
+    var rng = el('input'); rng.type = 'range'; rng.min = 2; rng.max = 12; rng.value = st.cols || getComputedCols(agent.target) || 5;
+    rng.addEventListener('input', function () { st.cols = +rng.value; agent.save(); setCols(agent.target, +rng.value); });
+    drow.appendChild(rng); dc.appendChild(drow);
+    dc.appendChild(el('div', 'na-note', 'Adjusts grid columns where the layout supports it.'));
+    if (st.cols) setCols(agent.target, st.cols);
+
+    if (!haveFields) body.appendChild(el('div', 'na-note',
+      'No structured fields detected on this grid — field toggles, sort & group need <code>&lt;table&gt;</code> headers or <code>[data-field]</code>/classed card elements. Search, density and the movable panel are active.'));
+
+    var reset = el('button', 'na-reset', '↺ Reset this page’s view');
+    reset.onclick = function () { try { localStorage.removeItem(agent.key); } catch (e) {} location.reload(); };
+    body.appendChild(reset);
+
+    panel.querySelector('.na-x').onclick = function () { panel.classList.remove('open'); };
+    document.body.appendChild(panel);
+    return panel;
+  }
+
+  function getComputedCols(t) { try { var v = getComputedStyle(t).getPropertyValue('--cols'); return v ? parseInt(v) : null; } catch (e) { return null; } }
+  function setCols(t, n) {
+    try {
+      t.style.setProperty('--cols', n);
+      // fallback for grids without a --cols hook
+      if (!/grid-template-columns/.test(t.style.cssText) && getComputedStyle(t).display === 'grid')
+        t.style.gridTemplateColumns = 'repeat(' + n + ',minmax(0,1fr))';
+    } catch (e) {}
+  }
+
+  /* -- movable / resizable / removable panel chrome ---------------------- */
+  function chrome(agent, opt) {
+    var t = agent.target;
+    if (t.closest('.na-wrap')) return;
+    var wrap = el('div', 'na-wrap');
+    var title = (opt && opt.title) || document.title || 'Data';
+    var bar = el('div', 'na-bar',
+      '<span class="na-bt">▤ ' + esc(title) + '</span>' +
+      '<button data-collapse title="Collapse">▁</button>' +
+      '<button data-max title="Maximize / float">⤢</button>' +
+      '<button data-remove title="Remove panel">✕</button>');
+    var inner = el('div', 'na-inner');
+    t.parentNode.insertBefore(wrap, t);
+    wrap.appendChild(bar); wrap.appendChild(inner); inner.appendChild(t);
+
+    var g = agent.state.geo || {};
+    bar.querySelector('[data-collapse]').onclick = function () { wrap.classList.toggle('na-collapsed'); persist(); };
+    bar.querySelector('[data-max]').onclick = function () { wrap.classList.toggle('na-max'); wrap.classList.remove('na-float'); persist(); };
+    bar.querySelector('[data-remove]').onclick = function () { wrap.classList.add('na-removed'); addRestore(); persist(); };
+
+    // drag to float/move
+    var down = null;
+    bar.addEventListener('mousedown', function (e) {
+      if (e.target.tagName === 'BUTTON') return;
+      var r = wrap.getBoundingClientRect();
+      if (!wrap.classList.contains('na-float')) { wrap.classList.add('na-float'); wrap.style.left = r.left + 'px'; wrap.style.top = r.top + 'px'; wrap.style.width = r.width + 'px'; }
+      down = { x: e.clientX, y: e.clientY, l: parseFloat(wrap.style.left) || r.left, t: parseFloat(wrap.style.top) || r.top };
+      e.preventDefault();
+    });
+    window.addEventListener('mousemove', function (e) {
+      if (!down) return;
+      wrap.style.left = (down.l + e.clientX - down.x) + 'px';
+      wrap.style.top = Math.max(0, down.t + e.clientY - down.y) + 'px';
+    });
+    window.addEventListener('mouseup', function () { if (down) { down = null; persist(); } });
+
+    function persist() {
+      agent.state.geo = {
+        float: wrap.classList.contains('na-float'), max: wrap.classList.contains('na-max'),
+        collapsed: wrap.classList.contains('na-collapsed'), removed: wrap.classList.contains('na-removed'),
+        left: wrap.style.left, top: wrap.style.top, width: wrap.style.width, height: wrap.style.height
+      };
+      agent.save();
+    }
+    function addRestore() {
+      var rb = document.querySelector('.na-restorebar') || (function () { var b = el('div', 'na-restorebar'); document.body.appendChild(b); return b; })();
+      var btn = el('button', null, '⟲ ' + esc(title));
+      btn.onclick = function () { wrap.classList.remove('na-removed'); btn.remove(); persist(); };
+      rb.appendChild(btn);
+    }
+    // restore saved geometry
+    if (g.float) { wrap.classList.add('na-float'); ['left', 'top', 'width', 'height'].forEach(function (k) { if (g[k]) wrap.style[k] = g[k]; }); }
+    if (g.max) wrap.classList.add('na-max');
+    if (g.collapsed) wrap.classList.add('na-collapsed');
+    if (g.removed) { wrap.classList.add('na-removed'); addRestore(); }
+  }
+
+  /* -- merged mode: fold into an existing house panel ("2 into 1") ------- */
+  function findHousePanel(target) {
+    var asides = [].slice.call(document.querySelectorAll('aside, .sidebar, .panel-left, [data-house-panel]'));
+    for (var i = 0; i < asides.length; i++) {
+      var a = asides[i];
+      if (a.classList.contains('na-panel')) continue;
+      if (a.contains(target)) continue;            // a real control sidebar won't contain the grid
+      if (a.querySelector('input, select, details, .toggle, .chip')) return a; // has controls
+    }
+    return null;
+  }
+
+  function buildMergeSection(agent, house, opt) {
+    var M = agent.model, st = agent.state;
+    var old = house.querySelector('.na-merge'); if (old) old.remove();
+    var box = el('div', 'na-merge');
+    box.appendChild(el('div', 'na-mh', '⚙ Nav Agent'));
+    if (M.fields && M.fields.length) {
+      // sort ANY field (superset of the house's fixed sort options)
+      var srow = el('div', 'na-row', '<label>Sort</label>');
+      var ssel = el('select'); ssel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { ssel.appendChild(new Option(f.label, f.key)); });
+      ssel.value = st.sort || ''; srow.appendChild(ssel); box.appendChild(srow);
+      var dir = el('div', 'na-mini', '<button data-asc>▲ Asc</button><button data-desc>▼ Desc</button>');
+      function markDir() { dir.querySelector('[data-asc]').classList.toggle('on', st.dir !== 'desc'); dir.querySelector('[data-desc]').classList.toggle('on', st.dir === 'desc'); }
+      ssel.onchange = function () { st.sort = ssel.value; agent.save(); agent.applySort(); };
+      dir.querySelector('[data-asc]').onclick = function () { st.dir = 'asc'; agent.save(); agent.applySort(); markDir(); };
+      dir.querySelector('[data-desc]').onclick = function () { st.dir = 'desc'; agent.save(); agent.applySort(); markDir(); };
+      markDir(); box.appendChild(dir);
+      // group ANY field
+      var grow = el('div', 'na-row', '<label>Group</label>');
+      var gsel = el('select'); gsel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { gsel.appendChild(new Option(f.label, f.key)); });
+      gsel.value = st.group || ''; gsel.onchange = function () { st.group = gsel.value; agent.save(); agent.applyGroup(); };
+      grow.appendChild(gsel); box.appendChild(grow);
+    }
+    box.appendChild(el('div', 'na-note', 'Drag the grid’s ▤ bar to float · ▁ collapse · ⤢ maximize · ✕ remove.'));
+    house.appendChild(box);
+    return box;
+  }
+
+  /* -- public mount ------------------------------------------------------ */
+  var API = {
+    instances: [],
+    mount: function (opt) {
+      opt = opt || {};
+      try {
+        var target = findTarget(opt);
+        if (!target) return null;
+        if (target.__naMounted) return target.__naAgent;
+        target.__naMounted = true;
+        if (target.tagName === 'TABLE') target.setAttribute('data-na', '1');
+        else target.setAttribute('data-na-sel', target.id ? '#' + target.id : '.' + (target.className.split(/\s+/)[0] || 'grid'));
+        var agent = Agent(target, opt);
+        target.__naAgent = agent;
+        // "2 into 1": if the page already has a house control panel, fold into it
+        var house = opt.merge === false ? null : findHousePanel(target);
+        // (re)build the controls — called again by the observer when fields change
+        function makePanel() {
+          if (house) { agent._merge = buildMergeSection(agent, house, opt); return; }
+          var wasOpen = agent._panel && agent._panel.classList.contains('open');
+          if (agent._panel) agent._panel.remove();
+          agent._panel = buildPanel(agent, opt);
+          if (wasOpen) agent._panel.classList.add('open');
+        }
+        agent.onFields = makePanel;
+        agent.merged = !!house;
+        makePanel();
+        chrome(agent, opt);
+        agent.applyAll();
+
+        // standalone launcher only when NOT merged into a house panel
+        if (!house && !document.querySelector('.na-launch')) {
+          var b = el('button', 'na-launch', '⚙'); b.title = 'View controls';
+          b.onclick = function () {
+            var anyOpen = document.querySelector('.na-panel.open');
+            [].slice.call(document.querySelectorAll('.na-panel')).forEach(function (p) { p.classList.toggle('open', !anyOpen); });
+          };
+          document.body.appendChild(b);
+        }
+        API.instances.push(agent);
+        return agent;
+      } catch (e) { if (window.console) console.warn('[nav-agent] mount skipped:', e); return null; }
+    }
+  };
+  window.NavAgent = API;
+
+  function boot() {
+    try {
+      var body = document.body;
+      if (!body || body.getAttribute('data-nav-agent') === 'off') return;
+      // wait a tick for API-driven grids to render their first batch
+      var tries = 0;
+      (function attempt() {
+        var t = findTarget();
+        var populated = false;
+        if (t) {
+          if (t.tagName === 'TABLE') populated = !!t.querySelector('tbody tr');
+          else {
+            var kids = [].slice.call(t.children).filter(function (c) { return c.nodeType === 1; });
+            // ignore an empty-state placeholder (a lone div, often "No … match")
+            populated = !!t.querySelector('.card') || kids.length >= 2;
+          }
+        }
+        if (populated || tries++ > 32) { API.mount(); return; } // mount anyway after ~8s; observer fills fields when data arrives
+        setTimeout(attempt, 250);
+      })();
+    } catch (e) {}
+  }
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
+  else boot();
+})();
diff --git a/public/panels/ab-tests.html b/public/panels/ab-tests.html
index bbbb1f4..499bb23 100644
--- a/public/panels/ab-tests.html
+++ b/public/panels/ab-tests.html
@@ -93,3 +93,5 @@
   </div>
 
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/browse-abandon.html b/public/panels/browse-abandon.html
index 93c5164..bf6bb5f 100644
--- a/public/panels/browse-abandon.html
+++ b/public/panels/browse-abandon.html
@@ -89,3 +89,5 @@
   </div>
   <div id="ba-preview" style="margin-top:10px"></div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/channels.html b/public/panels/channels.html
index 9dce96a..a413866 100644
--- a/public/panels/channels.html
+++ b/public/panels/channels.html
@@ -67,3 +67,5 @@
   .setup-card .saved-pill{background:#e3efe0;color:#3a6b3a;font-size:9.5px;padding:1px 7px}
   .ch-sublabel{font-size:11.5px;font-weight:700;color:var(--mut);margin:11px 0 3px;text-transform:uppercase;letter-spacing:.4px}
 </style>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/constant-contact.html b/public/panels/constant-contact.html
index a9e3278..a3d3bc5 100644
--- a/public/panels/constant-contact.html
+++ b/public/panels/constant-contact.html
@@ -89,3 +89,5 @@
     <span class="muted" id="cc-draft-msg" style="font-size:12.5px"></span>
   </div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/copy.html b/public/panels/copy.html
index d41783a..25ab8d1 100644
--- a/public/panels/copy.html
+++ b/public/panels/copy.html
@@ -71,3 +71,5 @@
 
   <div id="score-out" style="margin-top:18px"></div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/follow-counts.html b/public/panels/follow-counts.html
index 21645a0..ac46e05 100644
--- a/public/panels/follow-counts.html
+++ b/public/panels/follow-counts.html
@@ -36,3 +36,5 @@
   <div id="fc-chart" style="margin-top:14px"></div>
   <div class="muted" id="fc-chart-foot" style="font-size:11px;margin-top:8px"></div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/journeys.html b/public/panels/journeys.html
index 74846aa..7f410e4 100644
--- a/public/panels/journeys.html
+++ b/public/panels/journeys.html
@@ -51,3 +51,5 @@
   </div>
 
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/performance.html b/public/panels/performance.html
index dcb7621..ecea7f8 100644
--- a/public/panels/performance.html
+++ b/public/panels/performance.html
@@ -109,3 +109,5 @@
 .perf-table tbody tr:hover td{background:var(--cream)}
 .perf-empty{color:var(--mut);text-align:center;padding:18px 0}
 </style>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/playbook.html b/public/panels/playbook.html
index f82b77c..5e5e393 100644
--- a/public/panels/playbook.html
+++ b/public/panels/playbook.html
@@ -75,3 +75,5 @@
   </div>
   <div id="pb-edit" style="margin-top:12px"></div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/profiles.html b/public/panels/profiles.html
index 46bccb4..dc7cc1d 100644
--- a/public/panels/profiles.html
+++ b/public/panels/profiles.html
@@ -25,3 +25,5 @@
   </div>
 
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/segment-perf.html b/public/panels/segment-perf.html
index 49b40de..6bb4fd7 100644
--- a/public/panels/segment-perf.html
+++ b/public/panels/segment-perf.html
@@ -28,3 +28,5 @@
 <div id="sp-cards" class="grid" style="grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:18px">
   <div class="loading" style="grid-column:1 / -1">Loading segments…</div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/segments.html b/public/panels/segments.html
index 286cfc4..7cc61e2 100644
--- a/public/panels/segments.html
+++ b/public/panels/segments.html
@@ -66,3 +66,5 @@
   </div>
 
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/send-times.html b/public/panels/send-times.html
index ec756f8..5950790 100644
--- a/public/panels/send-times.html
+++ b/public/panels/send-times.html
@@ -62,3 +62,5 @@
   </div>
   <div id="st-contacts" class="loading" style="margin-top:14px">Loading contacts…</div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/public/panels/templates.html b/public/panels/templates.html
index 9865cbe..0106a1e 100644
--- a/public/panels/templates.html
+++ b/public/panels/templates.html
@@ -24,3 +24,5 @@
     <div id="templates-modal-body"></div>
   </div>
 </div>
+
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
diff --git a/scripts/li-cma-watch.py b/scripts/li-cma-watch.py
new file mode 100644
index 0000000..a920501
--- /dev/null
+++ b/scripts/li-cma-watch.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""
+li-cma-watch.py — unattended watcher for LinkedIn's Community Management API
+(CMA) access-request DECISION on the DW app 259400006.
+
+The CMA request for Designer Wallcoverings sits in LinkedIn's review queue
+(stage 1 of 2, ~10-14 business days). LinkedIn notifies by email to the app's
+verified business email = info@designerwallcoverings.com. This poller checks
+that inbox (read-only, via George's local HTTP API — no token cost, no send
+gate touched) and, when a *decision* email arrives, loudly surfaces it to Steve
+with the exact next step.
+
+Runs from a launchd LaunchAgent 3x/day. Alerts only on NEW, decision-looking
+mail — routine "Verify your business email" / device notices are filtered out,
+and each message id is remembered so it never double-alerts.
+
+Next step on approval (told to Steve in the alert):
+  python3 ~/.claude/skills/linkedin-api/scripts/connect.py --scope org
+  (secrets + redirect already staged 2026-08-12) -> route token+URN into MCC ->
+  ssh root@45.61.58.125 'pm2 reload marketing-command-center --update-env'
+"""
+import json, os, re, subprocess, sys, time, urllib.parse, urllib.request
+
+HERE      = os.path.dirname(os.path.abspath(__file__))
+SEEN_PATH = os.path.join(HERE, ".li-cma-watch-seen.json")
+LOG_PATH  = os.path.join(HERE, "li-cma-watch.log")
+DESKTOP_MARKER = os.path.expanduser("~/Desktop/LINKEDIN-CMA-RESPONDED.txt")
+GEORGE    = "http://127.0.0.1:9850"
+ACCOUNT   = "info"          # info@designerwallcoverings.com — the app's business email
+SEARCH_Q  = "from:linkedin newer_than:25d"
+
+# A message is a DECISION worth alerting on if it matches a signal AND is NOT
+# one of the known routine notices.
+SIGNAL = re.compile(r"(access request|community management|marketing developer|"
+                    r"approved|granted|declined|rejected|denied|unable to|"
+                    r"additional (information|documentation)|your request|"
+                    r"api (access|product))", re.I)
+NOISE  = re.compile(r"(verify your business email|verify your new device|"
+                    r"new device|sign-?in|password|security code|weekly|"
+                    r"who'?s viewed|notification digest|invitation)", re.I)
+
+
+def log(msg):
+    line = time.strftime("%Y-%m-%d %H:%M:%S") + "  " + msg
+    try:
+        with open(LOG_PATH, "a") as f:
+            f.write(line + "\n")
+    except Exception:
+        pass
+    print(line, flush=True)
+
+
+def george_auth():
+    p = os.path.expanduser("~/.claude.json")
+    d = json.load(open(p))
+    return d.get("mcpServers", {}).get("george", {}).get("env", {}).get("GEORGE_BASIC_AUTH", "")
+
+
+def search():
+    auth = george_auth()
+    if not auth:
+        log("ERR no GEORGE_BASIC_AUTH in ~/.claude.json"); return []
+    url = (GEORGE + "/api/search?account=" + ACCOUNT + "&maxResults=15&q="
+           + urllib.parse.quote(SEARCH_Q))
+    req = urllib.request.Request(url, headers={"Authorization": "Basic " + auth})
+    try:
+        raw = urllib.request.urlopen(req, timeout=12).read().decode()
+    except Exception as e:
+        log("ERR George search failed: %s" % e); return []
+    d = json.loads(raw)
+    return d if isinstance(d, list) else d.get("messages", d.get("results", []))
+
+
+def load_seen():
+    try:    return set(json.load(open(SEEN_PATH)))
+    except Exception: return set()
+
+
+def save_seen(s):
+    try:    json.dump(sorted(s), open(SEEN_PATH, "w"))
+    except Exception as e: log("ERR save seen: %s" % e)
+
+
+def alert(hits):
+    subj = hits[0].get("subject", "(no subject)")
+    body = (
+        "LinkedIn responded about the DW Community Management API (CMA) access "
+        "request.\n\n"
+        + "\n".join("- %s | %s" % (h.get("date", "?")[:16], h.get("subject", "?"))
+                    for h in hits)
+        + "\n\nIf APPROVED, the go-live is one command (secrets + redirect already "
+          "staged):\n"
+          "  python3 ~/.claude/skills/linkedin-api/scripts/connect.py --scope org\n"
+          "  -> route token+URN into MCC -> pm2 reload marketing-command-center\n\n"
+          "If they ask for DOCS, the request is still open — reply from info@.\n"
+          "Check info@ for the full message.\n"
+    )
+    # 1) Desktop marker (impossible to miss, durable)
+    try:
+        with open(DESKTOP_MARKER, "w") as f:
+            f.write(time.strftime("%Y-%m-%d %H:%M:%S") + "\n\n" + body)
+    except Exception as e:
+        log("ERR desktop marker: %s" % e)
+    # 2) macOS notification with sound
+    try:
+        subprocess.run(["osascript", "-e",
+            'display notification "%s" with title "LinkedIn CMA — LinkedIn responded" '
+            'subtitle "Check info@ / Desktop marker" sound name "Glass"'
+            % subj.replace('"', "'")[:200]], timeout=8)
+    except Exception as e:
+        log("ERR osascript: %s" % e)
+    # 3) log
+    log("ALERT — LinkedIn CMA decision mail detected: " + subj)
+
+
+def main():
+    seen = load_seen()
+    msgs = search()
+    hits = []
+    for m in msgs:
+        mid  = m.get("id") or m.get("messageId") or ""
+        text = (m.get("subject", "") + " " + m.get("snippet", ""))
+        if not mid:
+            continue
+        if SIGNAL.search(text) and not NOISE.search(text):
+            if mid not in seen:
+                hits.append(m)
+        seen.add(mid)          # remember every LinkedIn msg id so noise never re-alerts
+    if hits:
+        alert(hits)
+    else:
+        log("heartbeat — %d LinkedIn msgs scanned, no new CMA decision" % len(msgs))
+    save_seen(seen)
+    return 0 if not hits else 10
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/videos/mcc-streamline-promo/square.html b/videos/mcc-streamline-promo/square.html
deleted file mode 100644
index 36a3afd..0000000
--- a/videos/mcc-streamline-promo/square.html
+++ /dev/null
@@ -1,195 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=1080, height=1080" />
-    <link rel="preconnect" href="https://fonts.googleapis.com" />
-    <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;0,700;1,500;1,600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
-    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
-    <style>
-      * { margin: 0; padding: 0; box-sizing: border-box; }
-      html, body { width: 1080px; height: 1080px; overflow: hidden; }
-      :root {
-        --ink:#14110f; --cream:#f4efe7; --paper:#fbf9f5; --gold:#b8925a;
-        --gold-soft:#d8c19a; --accent:#8a5a44; --line:#e3ddd0; --mut:#8a8275;
-      }
-      #root {
-        position: relative; width: 1080px; height: 1080px; overflow: hidden;
-        font-family: "Inter", sans-serif; color: var(--ink);
-        background: radial-gradient(1200px 900px at 50% 12%, #fffdf9 0%, var(--paper) 45%, var(--cream) 100%);
-      }
-      .bgtex { position:absolute; inset:0;
-        background-image:
-          linear-gradient(0deg, rgba(184,146,90,.05) 1px, transparent 1px),
-          linear-gradient(90deg, rgba(184,146,90,.05) 1px, transparent 1px);
-        background-size: 60px 60px; opacity:.5; }
-      .wordmark { position:absolute; top:30px; left:0; right:0; text-align:center;
-        font-family:"Cormorant Garamond",serif; font-weight:600; letter-spacing:.22em;
-        font-size:28px; color:var(--ink); text-transform:uppercase; }
-      .wordmark .dot { color:#9c7328; }
-      .footer { position:absolute; bottom:30px; left:0; right:0; text-align:center;
-        font-size:22px; letter-spacing:.32em; text-transform:uppercase; color:#544d44; }
-
-      .stage { position:absolute; left:0; right:0; top:0; height:1080px;
-        display:flex; flex-direction:column; align-items:center; justify-content:center; }
-
-      .card { width:680px; border-radius:26px; overflow:hidden; background:#fff;
-        border:1px solid var(--line); box-shadow:0 40px 90px -30px rgba(20,17,15,.45);
-        will-change:transform,opacity; }
-      .card img { width:100%; display:block; }
-
-      .kicker { font-size:26px; letter-spacing:.34em; text-transform:uppercase; color:#6f4030; font-weight:600; }
-      .display { font-family:"Cormorant Garamond",serif; font-weight:700; color:var(--ink); line-height:.98; }
-      .huge { font-family:"Cormorant Garamond",serif; font-weight:700; color:var(--ink); line-height:1; }
-      .huge .g { color:#9c7328; }
-      .sub { font-family:"Cormorant Garamond",serif; font-style:italic; font-size:44px; color:#5f4232; }
-
-      .groups { display:flex; flex-direction:column; gap:10px; align-items:center; margin-top:8px; }
-      .chip { font-size:34px; font-weight:600; color:var(--ink); background:#fff; border:1px solid var(--line);
-        border-radius:999px; padding:14px 34px; box-shadow:0 10px 24px -14px rgba(20,17,15,.3); }
-
-      .beatlbl { text-align:center; margin-bottom:16px; }
-      .beatlbl .k { font-size:24px; letter-spacing:.3em; text-transform:uppercase; color:#7a5416; font-weight:600; }
-      .beatlbl .t { font-family:"Cormorant Garamond",serif; font-weight:700; font-size:52px; color:var(--ink); line-height:1.02; margin-top:6px; }
-      .beatlbl .t i { font-style:italic; color:var(--accent); }
-
-      .caption { position:absolute; left:60px; right:60px; bottom:40px; text-align:center;
-        font-size:33px; line-height:1.4; font-weight:600; color:#fff; }
-      .caption span.pill { background:rgba(20,17,15,.82); border-radius:14px; padding:8px 16px;
-        box-decoration-break:clone; -webkit-box-decoration-break:clone; }
-      .caption .g { color:var(--gold-soft); }
-    </style>
-  </head>
-  <body>
-    <div id="root" data-composition-id="main" data-start="0" data-duration="23.6" data-width="1080" data-height="1080">
-
-      <div class="bgtex"></div>
-      <div class="wordmark">Designer Wallcoverings<span class="dot">.</span></div>
-      <div class="footer">Marketing Command Center</div>
-
-      <!-- BEAT 1 — title -->
-      <div id="b1" class="clip stage" data-start="0" data-duration="3.7" data-track-index="1">
-        <div class="kicker" id="b1k">The Command Center</div>
-        <div class="display" style="font-size:104px;margin-top:10px" id="b1t">Streamlined</div>
-        <div class="sub" id="b1s" style="margin-top:18px">a cleaner way to run marketing</div>
-      </div>
-
-      <!-- BEAT 2 — 28 tabs, gone -->
-      <div id="b2" class="clip stage" data-start="3.5" data-duration="2.9" data-track-index="3">
-        <div class="kicker" id="b2k">Before</div>
-        <div class="huge" id="b2n" style="font-size:300px">28</div>
-        <div class="sub" id="b2s" style="font-size:52px;color:var(--accent)">tabs — gone</div>
-      </div>
-
-      <!-- BEAT 3 — 6 groups + chips -->
-      <div id="b3" class="clip stage" data-start="6.2" data-duration="3.1" data-track-index="1">
-        <div class="beatlbl">
-          <div class="k">Now</div>
-          <div class="t"><span class="huge" style="font-size:92px"><span class="g">6</span></span> <i>calendar-led groups</i></div>
-        </div>
-        <div class="groups" id="b3g">
-          <div class="chip">Calendar</div>
-          <div class="chip">Plan</div>
-          <div class="chip">Compose</div>
-          <div class="chip">Library</div>
-          <div class="chip">Accounts &amp; Email</div>
-          <div class="chip">Insights</div>
-        </div>
-      </div>
-
-      <!-- BEAT 4 — Compose -->
-      <div id="b4" class="clip stage" data-start="9.1" data-duration="4.0" data-track-index="3">
-        <div class="beatlbl"><div class="k">Smart hub · 1</div><div class="t"><i>Compose</i></div></div>
-        <div class="card" id="b4c1" style="width:680px"><img src="assets/img/compose-m.png" alt=""></div>
-      </div>
-
-      <!-- BEAT 5 — Calendar -->
-      <div id="b5" class="clip stage" data-start="13.0" data-duration="3.1" data-track-index="1">
-        <div class="beatlbl"><div class="k">Smart hub · 2</div><div class="t"><i>Calendar</i></div></div>
-        <div class="card" id="b5c"><img src="assets/img/calendar.png" alt=""></div>
-      </div>
-
-      <!-- BEAT 6 — Insights -->
-      <div id="b6" class="clip stage" data-start="16.0" data-duration="4.1" data-track-index="3">
-        <div class="beatlbl"><div class="k">Smart hub · 3</div><div class="t">8 tools, <i>one Insights tab</i></div></div>
-        <div class="card" id="b6c"><img src="assets/img/insights.png" alt=""></div>
-      </div>
-
-      <!-- BEAT 7 — outro -->
-      <div id="b7" class="clip stage" data-start="20.0" data-duration="3.6" data-track-index="1">
-        <div class="display" id="b7t" style="font-size:92px;text-align:center">Everything you need</div>
-        <div class="sub" id="b7s" style="font-size:40px;margin-top:14px">a click or two away</div>
-        <div id="b7w" style="margin-top:60px;font-family:'Cormorant Garamond',serif;font-weight:600;letter-spacing:.22em;font-size:40px;text-transform:uppercase">Designer Wallcoverings<span style="color:#9c7328">.</span></div>
-      </div>
-
-      <!-- CAPTIONS (muted-safe) -->
-      <div class="caption clip" id="c1" data-start="0.2" data-duration="3.3" data-track-index="2"><span class="pill">The command center just got a <span class="g">serious cleanup</span></span></div>
-      <div class="caption clip" id="c2" data-start="3.6" data-duration="2.7" data-track-index="2"><span class="pill"><span class="g">28 tabs</span> — gone</span></div>
-      <div class="caption clip" id="c3" data-start="6.3" data-duration="2.8" data-track-index="2"><span class="pill">Now <span class="g">6</span> calendar-led groups</span></div>
-      <div class="caption clip" id="c4" data-start="9.2" data-duration="3.8" data-track-index="2"><span class="pill"><span class="g">Compose</span> merges Suggested + Manual</span></div>
-      <div class="caption clip" id="c5" data-start="13.1" data-duration="2.8" data-track-index="2"><span class="pill">One <span class="g">Calendar</span> for your whole timeline</span></div>
-      <div class="caption clip" id="c6" data-start="16.1" data-duration="3.8" data-track-index="2"><span class="pill">8 preview tools → one <span class="g">Insights</span> tab</span></div>
-      <div class="caption clip" id="c7" data-start="20.1" data-duration="3.3" data-track-index="2"><span class="pill">A click or two <span class="g">away</span></span></div>
-
-      <!-- continuous narration -->
-      <audio id="narration" src=".media/audio/narration.wav" data-start="0" data-track-index="10"></audio>
-    </div>
-
-    <script>
-      window.__timelines = window.__timelines || {};
-      const tl = gsap.timeline({ paused: true });
-      const ease = "power3.out";
-
-      tl.from("#b1k", { opacity:0, y:20, duration:.6, ease }, 0.1)
-        .from("#b1t", { opacity:0, y:40, duration:.8, ease }, 0.25)
-        .from("#b1s", { opacity:0, y:20, duration:.7, ease }, 0.6)
-        .to("#b1", { opacity:0, duration:.4 }, 3.3)
-        .set("#b1", { opacity:0 }, 3.7);
-
-      tl.from("#b2k", { opacity:0, duration:.4 }, 3.6)
-        .from("#b2n", { opacity:0, scale:1.6, duration:.6, ease:"back.out(1.6)" }, 3.6)
-        .from("#b2s", { opacity:0, y:20, duration:.5, ease }, 4.1)
-        .to("#b2n", { opacity:.15, scale:.9, duration:.5, ease }, 5.4)
-        .to("#b2", { opacity:0, duration:.4 }, 6.0)
-        .set("#b2", { opacity:0 }, 6.4);
-
-      tl.from("#b3 .beatlbl", { opacity:0, y:30, duration:.6, ease }, 6.35)
-        .from("#b3 .chip", { opacity:0, y:26, duration:.5, stagger:.08, ease }, 6.6)
-        .to("#b3", { opacity:0, duration:.4 }, 8.9)
-        .set("#b3", { opacity:0 }, 9.3);
-
-      // card intro (opacity+y) then a slow parallax drift that STARTS after the intro (no property overlap)
-      tl.from("#b4 .beatlbl", { opacity:0, y:24, duration:.5, ease }, 9.25)
-        .from("#b4c1", { opacity:0, y:60, duration:.7, ease }, 9.4)
-        .to("#b4c1", { y:-46, duration:2.6, ease:"none" }, 10.2)
-        .to("#b4", { opacity:0, duration:.4 }, 12.7)
-        .set("#b4", { opacity:0 }, 13.1);
-
-      tl.from("#b5 .beatlbl", { opacity:0, y:24, duration:.5, ease }, 13.15)
-        .from("#b5c", { opacity:0, y:50, duration:.8, ease }, 13.3)
-        .to("#b5c", { y:-28, duration:1.9, ease:"none" }, 14.2)
-        .to("#b5", { opacity:0, duration:.4 }, 15.7)
-        .set("#b5", { opacity:0 }, 16.1);
-
-      tl.from("#b6 .beatlbl", { opacity:0, y:24, duration:.5, ease }, 16.15)
-        .from("#b6c", { opacity:0, y:60, duration:.8, ease }, 16.3)
-        .to("#b6c", { y:-34, duration:2.6, ease:"none" }, 17.1)
-        .to("#b6", { opacity:0, duration:.4 }, 19.7)
-        .set("#b6", { opacity:0 }, 20.1);
-
-      tl.from("#b7t", { opacity:0, y:40, duration:.8, ease }, 20.15)
-        .from("#b7s", { opacity:0, y:24, duration:.7, ease }, 20.6)
-        .from("#b7w", { opacity:0, duration:.8, ease }, 21.2);
-
-      ["c1","c2","c3","c4","c5","c6","c7"].forEach(id => {
-        const el = document.getElementById(id);
-        const s = parseFloat(el.dataset.start), d = parseFloat(el.dataset.duration);
-        tl.from("#"+id, { opacity:0, y:16, duration:.35, ease }, s)
-          .to("#"+id, { opacity:0, duration:.3 }, s + d - 0.3)
-          .set("#"+id, { opacity:0 }, s + d);
-      });
-
-      window.__timelines["main"] = tl;
-    </script>
-  </body>
-</html>

← 9c6fd71 auto-data-snapshot: 2026-08-13T08:21:55 (1 data files) — pub  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-13T17:06:27 (1 data files) — pub 8d790fe →