[object Object]

← back to Marketing Command Center

Nav: ⌘K command palette + regroup + relabels + collapsible groups

3a6c04f6260ae36e8873e5ac4ff5d95e5e293fe9 · 2026-08-25 11:26:07 -0700 · Steve

Make the Command Center easy to navigate (Steve: 'is the UI easy to navigate?').
Design merged from two parallel review lenses (ui-ux-designer + general), then
contrarian-hardened.

- ⌘K / Ctrl-K command palette (public/cmd-palette.js) — fuzzy jump to any of 33
  panels, ↑↓ wrap + Enter, matches title AND group, includes hidden/merged panels
  with a 'via Host' breadcrumb; a 'Search panels…' trigger in the sidebar
- Regroup (app.js GROUPS): Calendar · Compose · Plan · Library · Insights ·
  Accounts&Email — un-strands 'IG Posts' from the dead 'More' bucket into Insights
- TITLE_OVERRIDES (rail label only, never touches the module a peer session owns):
  vendors→'IG Account Health' (it now holds the Owned·DW Fleet view), board→'Streams
  Pipeline', layouts→'Layouts'
- Collapsible nav groups, expanded by default, persisted (localStorage mcc_nav_collapsed)

Contrarian fixes (Cody, FIX FIRST):
- palette z-index 2001→10001 so it wins over the 9999 clients drawer / amplify
  popover / quickpost toast
- route() highlights a hidden panel's HOST nav item (was leaving the sidebar blank)
- Tab now dismisses+restores focus (ARIA combobox) instead of swallowing the key
- '/' shortcut only fires from a non-interactive context

Verified: palette opens/filters/navigates, Ctrl+K + Tab + arrows, no 'More' orphan,
relabels render, collapse persists, hidden-panel nav highlights host, z-index 10001,
no page errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 3a6c04f6260ae36e8873e5ac4ff5d95e5e293fe9
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 25 11:26:07 2026 -0700

    Nav: ⌘K command palette + regroup + relabels + collapsible groups
    
    Make the Command Center easy to navigate (Steve: 'is the UI easy to navigate?').
    Design merged from two parallel review lenses (ui-ux-designer + general), then
    contrarian-hardened.
    
    - ⌘K / Ctrl-K command palette (public/cmd-palette.js) — fuzzy jump to any of 33
      panels, ↑↓ wrap + Enter, matches title AND group, includes hidden/merged panels
      with a 'via Host' breadcrumb; a 'Search panels…' trigger in the sidebar
    - Regroup (app.js GROUPS): Calendar · Compose · Plan · Library · Insights ·
      Accounts&Email — un-strands 'IG Posts' from the dead 'More' bucket into Insights
    - TITLE_OVERRIDES (rail label only, never touches the module a peer session owns):
      vendors→'IG Account Health' (it now holds the Owned·DW Fleet view), board→'Streams
      Pipeline', layouts→'Layouts'
    - Collapsible nav groups, expanded by default, persisted (localStorage mcc_nav_collapsed)
    
    Contrarian fixes (Cody, FIX FIRST):
    - palette z-index 2001→10001 so it wins over the 9999 clients drawer / amplify
      popover / quickpost toast
    - route() highlights a hidden panel's HOST nav item (was leaving the sidebar blank)
    - Tab now dismisses+restores focus (ARIA combobox) instead of swallowing the key
    - '/' shortcut only fires from a non-interactive context
    
    Verified: palette opens/filters/navigates, Ctrl+K + Tab + arrows, no 'More' orphan,
    relabels render, collapse persists, hidden-panel nav highlights host, z-index 10001,
    no page errors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/app.js         |  62 +++++++++++++---
 public/cmd-palette.js | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html     |   1 +
 public/style.css      |   6 ++
 4 files changed, 254 insertions(+), 12 deletions(-)

diff --git a/public/app.js b/public/app.js
index dcb2127..c26d334 100644
--- a/public/app.js
+++ b/public/app.js
@@ -13,13 +13,32 @@ let PANELS = [];
 // (Suggested+Manual). Their underlying panels are hidden via HIDDEN below.
 const GROUPS = [
   { name: 'Calendar', ids: ['calendarhub'] },
-  { name: 'Plan', ids: ['playbook', 'vendors'] },
-  { name: 'Compose', ids: ['compose', 'quickpost', 'linkedin', 'board', 'social'] },
-  { name: 'Library', ids: ['assets', 'sounds', 'reels', 'copy', 'layouts', 'templates'] },
+  { name: 'Compose', ids: ['compose', 'quickpost', 'linkedin', 'social'] },
+  { name: 'Plan', ids: ['playbook', 'board'] },
+  { name: 'Library', ids: ['assets', 'sounds', 'reels', 'layouts', 'copy', 'templates'] },
+  { name: 'Insights', ids: ['insights', 'ig-activity', 'vendors'] },   // ig-activity un-stranded from "More"
   { name: 'Accounts & Email', ids: ['accounts', 'channels', 'clients', 'constant-contact', 'browse-abandon'] },
-  { name: 'Insights · preview', ids: ['insights'] },
 ];
 
+// Relabel a panel in the SIDEBAR only, WITHOUT editing its module (a module may be
+// owned by another session — e.g. modules/vendors/index.js). Keyed by panel id.
+const TITLE_OVERRIDES = {
+  vendors: 'IG Account Health',   // now also holds the Owned·DW Fleet health view, not just vendors
+  board: 'Streams Pipeline',      // "Board" was vague — it's the cross-channel queue
+  layouts: 'Layouts',             // trim "On-Demand Layouts" for the rail
+};
+// Hidden/merged panels → their host, so the command palette can show a "· via Host"
+// breadcrumb and still let power users jump straight to them.
+const HIDDEN_HOST = {
+  calendars: 'calendarhub', calendar: 'calendarhub', engine: 'compose', composer: 'compose',
+  performance: 'insights', 'segment-perf': 'insights', 'send-times': 'insights', segments: 'insights',
+  journeys: 'insights', profiles: 'insights', 'follow-counts': 'insights', 'ab-tests': 'insights',
+};
+// Static maps the command palette reads (panel id → group label, and the nav order).
+window.MCC_GROUP_MAP = Object.fromEntries(GROUPS.flatMap(g => g.ids.map(id => [id, g.name])));
+window.MCC_HIDDEN_HOST = HIDDEN_HOST;
+window.MCC_NAV_ORDER = GROUPS.flatMap(g => g.ids);
+
 // Panels whose ROUTERS stay mounted (their APIs are still called) but whose NAV
 // entries are hidden — because they were merged into a host panel above. Removing
 // them from registry.js would unmount their APIs and break the host, so we hide
@@ -42,32 +61,47 @@ async function jget(url) {
 }
 
 function linkHTML(p) {
+  const label = TITLE_OVERRIDES[p.id] || p.title;      // rail label only; the panel header keeps its module title
   return `<a data-id="${p.id}" class="${p.pending ? 'pending' : ''}">` +
-    `<span class="i">${p.icon || '▪'}</span><span class="t">${p.title}</span></a>`;
+    `<span class="i">${p.icon || '▪'}</span><span class="t">${label}</span></a>`;
 }
 
 function renderNav() {
   const visible = PANELS.filter(p => !HIDDEN.has(p.id));   // hide merged-away panels
   const byId = Object.fromEntries(visible.map(p => [p.id, p]));
+  const collapsed = new Set(JSON.parse(localStorage.getItem('mcc_nav_collapsed') || '[]'));
+  const group = (name, items, canCollapse = true) => {
+    const isC = canCollapse && collapsed.has(name);
+    return `<div class="navgroup${isC ? ' collapsed' : ''}">` +
+      `<div class="navhead" data-group="${name}"><span class="caret">${isC ? '▸' : '▾'}</span>${name}</div>` +
+      `<div class="navitems">${items.map(linkHTML).join('')}</div></div>`;
+  };
   const used = new Set();
   let html = '';
   for (const g of GROUPS) {
     const items = g.ids.map(id => byId[id]).filter(Boolean);
     if (!items.length) continue;
     items.forEach(p => used.add(p.id));
-    html += `<div class="navgroup"><div class="navhead">${g.name}</div>` +
-      items.map(linkHTML).join('') + `</div>`;
+    html += group(g.name, items);
   }
   const leftover = visible.filter(p => !used.has(p.id));
-  if (leftover.length) {
-    html += `<div class="navgroup"><div class="navhead">More</div>` +
-      leftover.map(linkHTML).join('') + `</div>`;
-  }
+  if (leftover.length) html += group('More', leftover);   // should be empty now that ig-activity has a home
   $('#tabs').innerHTML = html;
   $('#tabs').querySelectorAll('a').forEach(a => a.onclick = () => {
     location.hash = a.dataset.id;
     if (window.matchMedia('(max-width:860px)').matches) document.body.classList.remove('nav-open');
   });
+  // Collapse/expand a group (persisted). renderNav re-runs — cheap, and it re-inserts
+  // the palette trigger below.
+  $('#tabs').querySelectorAll('.navhead[data-group]').forEach(h => h.onclick = () => {
+    const c = new Set(JSON.parse(localStorage.getItem('mcc_nav_collapsed') || '[]'));
+    c.has(h.dataset.group) ? c.delete(h.dataset.group) : c.add(h.dataset.group);
+    localStorage.setItem('mcc_nav_collapsed', JSON.stringify([...c]));
+    renderNav();
+  });
+  // setting #tabs.innerHTML wiped the palette's "Search panels…" trigger — re-mount it
+  // (setData re-inserts the trigger + refreshes the palette's item list).
+  if (window.cmdPalette && window.cmdPalette.setData) window.cmdPalette.setData(PANELS);
 }
 
 function renderBootError(e) {
@@ -114,7 +148,11 @@ async function route() {
   const id = location.hash.replace('#', '') || (PANELS[0] && PANELS[0].id);
   if (!id) return;
   const meta = PANELS.find(p => p.id === id) || { id, title: id };
-  $('#tabs').querySelectorAll('a').forEach(a => a.classList.toggle('active', a.dataset.id === id));
+  // A hidden/merged panel (e.g. composer, follow-counts — reachable via the palette)
+  // has no nav <a> of its own, so highlight its HOST group entry instead of leaving
+  // the whole sidebar deselected.
+  const activeId = (HIDDEN_HOST && HIDDEN_HOST[id]) || id;
+  $('#tabs').querySelectorAll('a').forEach(a => a.classList.toggle('active', a.dataset.id === activeId));
   $('#paneltitle').textContent = meta.title;
   $('#crumb').textContent = meta.pending ? '— not built yet' : '';
   const panel = $('#panel');
diff --git a/public/cmd-palette.js b/public/cmd-palette.js
new file mode 100644
index 0000000..604fe2e
--- /dev/null
+++ b/public/cmd-palette.js
@@ -0,0 +1,197 @@
+// Command palette — ⌘K / Ctrl-K (or "/" when not typing) to jump to any panel.
+// Self-contained IIFE, zero deps. app.js hands it the panel list + group map via
+// window.cmdPalette.setData(PANELS); reads window.MCC_GROUP_MAP / MCC_HIDDEN_HOST /
+// MCC_NAV_ORDER (all set by app.js boot) for group labels + default ordering.
+// The whole point is that it feels INSTANT: on open we render + focus BEFORE the
+// open animation paints, so the first keystroke always lands.
+(function () {
+  const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
+    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
+
+  let allItems = [];   // [{id,title,icon,pending,group,hostLabel}]
+  let filtered = [];
+  let activeIdx = -1;
+  let isOpen = false;
+  let prevFocus = null;
+
+  const byId = id => document.getElementById(id);
+
+  function injectCSS() {
+    if (byId('cmd-palette-css')) return;
+    const st = document.createElement('style'); st.id = 'cmd-palette-css';
+    st.textContent = `
+    /* z-index above every in-panel drawer/popover/toast (clients drawer + amplify popover + quickpost toast all sit at 9999) */
+    #cmd-backdrop{position:fixed;inset:0;background:rgba(20,15,10,.5);z-index:10000;animation:cmdfade .12s ease}
+    #cmd-modal{position:fixed;top:16vh;left:50%;transform:translateX(-50%);width:min(560px,calc(100vw - 28px));z-index:10001;animation:cmdslide .14s ease}
+    #cmd-shell{background:#fbf9f5;border:1px solid #e3ddd0;border-radius:14px;box-shadow:0 24px 80px rgba(20,15,8,.30),0 2px 8px rgba(20,15,8,.12);overflow:hidden}
+    #cmd-input-wrap{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid #e7e1d6}
+    #cmd-search-icon{font-size:17px;color:#8a8275;flex:none}
+    #cmd-input{flex:1;border:0;background:transparent;font:500 15px/1 Inter,-apple-system,sans-serif;color:#2a2620;outline:none;padding:0}
+    #cmd-input::placeholder{color:#a49a88}
+    #cmd-esc-hint{flex:none;font:600 10px/1 Inter,sans-serif;background:#f4efe7;border:1px solid #e7e1d6;border-radius:5px;padding:3px 7px;color:#8a8275;letter-spacing:.3px}
+    #cmd-results{list-style:none;margin:0;padding:6px;max-height:56vh;overflow-y:auto;scroll-padding:6px}
+    #cmd-results::-webkit-scrollbar{width:8px}#cmd-results::-webkit-scrollbar-thumb{background:#e0d8cc;border-radius:8px}
+    .cmd-item{display:flex;align-items:center;gap:12px;padding:9px 11px;border-radius:9px;cursor:pointer}
+    .cmd-item:hover{background:#f4efe7}
+    .cmd-item[aria-selected="true"]{background:linear-gradient(120deg,#f0e8d4,#ecdfc9);box-shadow:inset 0 0 0 2px #d8c19a}
+    .cmd-icon{width:24px;text-align:center;font-size:16px;flex:none}
+    .cmd-label{display:flex;flex-direction:column;gap:1px;min-width:0}
+    .cmd-title{font:500 13.5px/1.3 Inter,sans-serif;color:#2a2620;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+    .cmd-group{font:400 11px/1 Inter,sans-serif;color:#8a8275;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+    .cmd-pending .cmd-title{opacity:.5}
+    .cmd-empty{padding:16px 12px;color:#8a8275;font-size:12.5px}
+    @keyframes cmdfade{from{opacity:0}to{opacity:1}}
+    @keyframes cmdslide{from{opacity:0;transform:translateX(-50%) translateY(-6px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
+    /* Sidebar "Search panels…" trigger */
+    #cmd-trigger{display:flex;align-items:center;gap:8px;margin:2px 10px 8px;padding:8px 11px;border-radius:9px;
+      border:1px solid #2f2a24;background:transparent;color:#b7ad9c;font:400 12.5px/1 Inter,sans-serif;cursor:pointer;width:calc(100% - 20px)}
+    #cmd-trigger:hover{background:#241f1b;border-color:#d8c19a;color:#d8c19a}
+    #cmd-trigger .k{margin-left:auto;font-size:10px;opacity:.65;letter-spacing:.3px}
+    @media(max-width:860px){#cmd-modal{top:10vh}#cmd-trigger .k{display:none}}`;
+    document.head.appendChild(st);
+  }
+
+  function injectDOM() {
+    if (byId('cmd-modal')) return;
+    injectCSS();
+    const backdrop = document.createElement('div');
+    backdrop.id = 'cmd-backdrop'; backdrop.hidden = true; backdrop.setAttribute('aria-hidden', 'true');
+    const modal = document.createElement('div');
+    modal.id = 'cmd-modal'; modal.hidden = true;
+    modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-label', 'Jump to panel');
+    modal.innerHTML = `<div id="cmd-shell">
+      <div id="cmd-input-wrap">
+        <span id="cmd-search-icon" aria-hidden="true">⌕</span>
+        <input id="cmd-input" type="text" placeholder="Jump to panel…" autocomplete="off" spellcheck="false"
+               aria-autocomplete="list" aria-controls="cmd-results" aria-activedescendant="">
+        <kbd id="cmd-esc-hint">Esc</kbd>
+      </div>
+      <ul id="cmd-results" role="listbox" aria-label="Panels"></ul></div>`;
+    document.body.appendChild(backdrop);
+    document.body.appendChild(modal);
+    document.addEventListener('keydown', onDocKeydown);
+    modal.addEventListener('keydown', onModalKeydown);
+    backdrop.addEventListener('mousedown', close);
+    byId('cmd-input').addEventListener('input', () => filter(byId('cmd-input').value));
+  }
+
+  function injectTrigger() {
+    const tabs = byId('tabs'); if (!tabs || byId('cmd-trigger')) return;
+    const t = document.createElement('button');
+    t.id = 'cmd-trigger'; t.type = 'button'; t.setAttribute('aria-label', 'Search panels (Command K)');
+    const mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
+    t.innerHTML = `<span aria-hidden="true">⌕</span><span>Search panels…</span><span class="k">${mac ? '⌘K' : 'Ctrl K'}</span>`;
+    t.onclick = open;
+    tabs.insertBefore(t, tabs.firstChild);
+  }
+
+  function buildItems(panels) {
+    const gmap = window.MCC_GROUP_MAP || {};
+    const hostOf = window.MCC_HIDDEN_HOST || {};
+    return panels.map(p => {
+      const hostId = hostOf[p.id];
+      const group = gmap[p.id] || (hostId ? (gmap[hostId] || 'More') : 'More');
+      const hostLabel = hostId ? ` · via ${(panels.find(x => x.id === hostId) || {}).title || hostId}` : '';
+      return { id: p.id, title: p.title, icon: p.icon, pending: !!p.pending, group, hostLabel };
+    });
+  }
+
+  function setData(panels) {
+    allItems = buildItems(panels || []);
+    injectTrigger();
+    filter('');
+  }
+
+  function filter(q) {
+    const raw = String(q || '').trim().toLowerCase();
+    if (!raw) {
+      const order = window.MCC_NAV_ORDER || [];
+      filtered = allItems.slice().sort((a, b) => {
+        if (!!a.pending !== !!b.pending) return a.pending ? 1 : -1;
+        const ai = order.indexOf(a.id), bi = order.indexOf(b.id);
+        return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
+      });
+    } else {
+      const t1 = [], t2 = [], t3 = [];
+      for (const it of allItems) {
+        const t = it.title.toLowerCase(), g = (it.group + it.hostLabel).toLowerCase();
+        if (t.startsWith(raw)) t1.push(it);
+        else if (t.includes(raw)) t2.push(it);
+        else if (g.includes(raw)) t3.push(it);
+      }
+      filtered = t1.concat(t2, t3);
+    }
+    activeIdx = filtered.length ? 0 : -1;
+    render();
+  }
+
+  function render() {
+    const ul = byId('cmd-results');
+    if (!filtered.length) { ul.innerHTML = '<li class="cmd-empty">No panels match.</li>'; byId('cmd-input').setAttribute('aria-activedescendant', ''); return; }
+    ul.innerHTML = filtered.map((p, i) =>
+      `<li id="cmd-item-${esc(p.id)}" role="option" aria-selected="${i === activeIdx}" class="cmd-item${p.pending ? ' cmd-pending' : ''}" data-id="${esc(p.id)}">
+         <span class="cmd-icon" aria-hidden="true">${esc(p.icon || '▪')}</span>
+         <span class="cmd-label"><span class="cmd-title">${esc(p.title)}</span><span class="cmd-group">${esc(p.group)}${esc(p.hostLabel)}</span></span>
+       </li>`).join('');
+    ul.querySelectorAll('.cmd-item[data-id]').forEach(li =>
+      li.addEventListener('mousedown', e => { e.preventDefault(); navigate(li.dataset.id); }));
+    setActive(activeIdx);
+  }
+
+  function setActive(idx) {
+    activeIdx = idx;
+    const items = byId('cmd-results').querySelectorAll('.cmd-item[data-id]');
+    items.forEach((li, i) => li.setAttribute('aria-selected', String(i === idx)));
+    if (idx >= 0 && items[idx]) { items[idx].scrollIntoView({ block: 'nearest' }); byId('cmd-input').setAttribute('aria-activedescendant', items[idx].id); }
+    else byId('cmd-input').setAttribute('aria-activedescendant', '');
+  }
+
+  function open() {
+    if (isOpen) return;
+    isOpen = true; prevFocus = document.activeElement;
+    byId('cmd-backdrop').hidden = false; byId('cmd-backdrop').removeAttribute('aria-hidden');
+    byId('cmd-modal').hidden = false;
+    const input = byId('cmd-input'); input.value = '';
+    filter('');        // render BEFORE focus/paint so the first keystroke lands
+    input.focus();
+    document.body.style.overflow = 'hidden';
+  }
+
+  function close() {
+    if (!isOpen) return;
+    isOpen = false;
+    byId('cmd-backdrop').hidden = true; byId('cmd-backdrop').setAttribute('aria-hidden', 'true');
+    byId('cmd-modal').hidden = true;
+    document.body.style.overflow = '';
+    activeIdx = -1;
+    if (prevFocus && prevFocus.focus) prevFocus.focus();
+    prevFocus = null;
+  }
+
+  function navigate(id) {
+    close();
+    document.body.classList.remove('nav-open');   // close mobile drawer
+    location.hash = id;
+  }
+
+  function onDocKeydown(e) {
+    if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); isOpen ? close() : open(); return; }
+    if (e.key === '/' && !isOpen) {
+      // Only fire "/" from a non-interactive context (page body) — never while a
+      // form field, button, or nav link has focus, so "/" doesn't pop unexpectedly.
+      const ae = document.activeElement, tag = ae && ae.tagName;
+      if (!ae || ae === document.body || (!['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'].includes(tag) && !ae.isContentEditable && ae.tabIndex < 0)) { e.preventDefault(); open(); }
+    }
+  }
+  function onModalKeydown(e) {
+    if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
+    else if (e.key === 'ArrowDown') { e.preventDefault(); if (filtered.length) setActive((activeIdx + 1) % filtered.length); }
+    else if (e.key === 'ArrowUp') { e.preventDefault(); if (filtered.length) setActive((activeIdx - 1 + filtered.length) % filtered.length); }
+    else if (e.key === 'Enter') { e.preventDefault(); if (activeIdx >= 0 && filtered[activeIdx]) navigate(filtered[activeIdx].id); }
+    else if (e.key === 'Tab') { e.preventDefault(); close(); }   // ARIA combobox: Tab dismisses + restores focus (not a black hole)
+  }
+
+  window.cmdPalette = { setData, open, close };
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', injectDOM);
+  else injectDOM();
+})();
diff --git a/public/index.html b/public/index.html
index 720a483..8d51f51 100644
--- a/public/index.html
+++ b/public/index.html
@@ -48,5 +48,6 @@
 </main>
 <script src="/dw-accounts.js"></script>
 <script src="/quickpost.js"></script>
+<script src="/cmd-palette.js"></script>
 <script src="/app.js"></script>
 </body></html>
diff --git a/public/style.css b/public/style.css
index e1a9734..1fc2024 100644
--- a/public/style.css
+++ b/public/style.css
@@ -29,6 +29,12 @@ body.nav-hidden #nav{margin-left:calc(-1 * var(--nav-w))}
 #tabs a.active{background:linear-gradient(120deg,var(--gold),#a8814b);color:var(--ink);font-weight:600;box-shadow:0 1px 6px rgba(0,0,0,.25)}
 #tabs a.pending{opacity:.45}
 #tabs a.pending::after{content:'soon';margin-left:auto;font-size:9px;letter-spacing:.5px;text-transform:uppercase;color:#b7ad9c}
+/* collapsible nav groups */
+.navhead{cursor:pointer;user-select:none;display:flex;align-items:center;gap:3px;border-radius:6px}
+.navhead:hover{color:var(--gold)}
+.navhead .caret{display:inline-block;width:.85em;font-size:.8em;opacity:.55}
+.navgroup.collapsed .navitems{display:none}
+.navitems{display:flex;flex-direction:column;gap:2px}
 .navfoot{padding:12px 18px;border-top:1px solid #2b2622;font-size:11px;color:#b7ad9c}
 .dot::before{content:'';display:inline-block;width:8px;height:8px;border-radius:50%;background:#9aa;margin-right:7px;vertical-align:1px}
 .dot.ok::before{background:#5fbf7e}.dot.bad::before{background:#e07a5f}

← 33e8840 chore: v1.11.0 (session close) — vendor-amplify + Make-it-ou  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-25T11:28:19 (2 data files) — dat 81fe0b8 →