← 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
M public/app.jsA public/cmd-palette.jsM public/index.htmlM public/style.css
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 =>
+ ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 →