[object Object]

← back to Mfr Review Viewer Corruption

list form + full nav agent: convert card grid to one-row-per-record KV list (identity block: DW SKU + bad→suggested mfr + ACTIVE badge; inline label/value field chips) + wire /nav-agent + /kv-list drop-ins (2-into-1 with existing aside); candidates drill-down / suggested-mfr banner / foreign-marking / stage flow all preserved

84c6e57632d0a08ac2e9d53a1340ee14ab2c2aea · 2026-08-27 11:25:43 -0700 · Steve

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

Files touched

Diff

commit 84c6e57632d0a08ac2e9d53a1340ee14ab2c2aea
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 27 11:25:43 2026 -0700

    list form + full nav agent: convert card grid to one-row-per-record KV list (identity block: DW SKU + bad→suggested mfr + ACTIVE badge; inline label/value field chips) + wire /nav-agent + /kv-list drop-ins (2-into-1 with existing aside); candidates drill-down / suggested-mfr banner / foreign-marking / stage flow all preserved
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/index.html              | 125 ++++++---
 public/kv-list.css             |  35 +++
 public/kv-list.js              |  87 ++++++
 public/nav-agent/nav-agent.css | 133 +++++++++
 public/nav-agent/nav-agent.js  | 620 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 968 insertions(+), 32 deletions(-)

diff --git a/public/index.html b/public/index.html
index 2a14a0c..05d91fb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,6 +4,8 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>MFR Review — broken DW# == Mfr SKU</title>
+<link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<link rel="stylesheet" href="/kv-list.css"><!-- kv-list -->
 <style>
   :root {
     --bg:#0e1114; --panel:#171b20; --card:#1c2127; --line:#2a3038;
@@ -56,6 +58,36 @@
   .card .top { display:flex; align-items:flex-start; gap:8px; }
   .card .dw { font-weight:700; font-size:14px; word-break:break-all; }
   .card .chk { transform:scale(1.15); margin-top:2px; cursor:pointer; }
+
+  /* ---- KV List mode (one row per record) — the default record renderer ---- */
+  /* Feed kv-list.css the host theme vars so it inherits this dark palette. */
+  :root { --kv-ink:var(--ink); --kv-mut:var(--muted); --kv-line:var(--line);
+    --kv-hover:var(--card); --kv-acc:var(--teal); }
+  #list { display:flex; flex-direction:column; }
+  .lrow { border-bottom:1px solid var(--line); }
+  .lrow.sel .lrow-head { background:rgba(20,184,166,.08); }
+  .lrow-head { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; padding:9px 14px; }
+  .lrow-head:hover { background:var(--card); }
+  /* fixed identity block, left */
+  .lident { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; min-width:300px; flex:0 0 auto; }
+  .lident .chk { transform:scale(1.15); align-self:center; cursor:pointer; }
+  .lident .dw { font-weight:700; font-size:14px; color:var(--ink); word-break:break-all; }
+  .lident .mfr-transition { font-family:ui-monospace,monospace; font-size:12px; white-space:nowrap; }
+  .lident .mfr-transition .from { color:var(--red); text-decoration:line-through; }
+  .lident .mfr-transition .arrow { color:var(--muted); margin:0 4px; }
+  .lident .mfr-transition .to { color:var(--teal); font-weight:700; }
+  .lident .status-badge { font-size:10px; padding:1px 6px; border-radius:8px; background:#123024; color:#7fe0b0; font-weight:600; }
+  /* flowing field chips, right (uppercase micro-label + value) */
+  .lmeta { display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--lmeta-min,150px),1fr)); gap:5px 14px;
+    flex:1 1 320px; min-width:0; align-content:start; }
+  .lm { font-size:12px; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+  .lmk { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:5px; }
+  .lm .badmfr-chip { color:var(--red); text-decoration:line-through; }
+  .lm .real-chip { color:var(--teal); font-weight:600; }
+  .lm .guess-in { display:inline-block; width:auto; min-width:120px; }
+  .lrow .cand-toggle { padding:0 14px 8px; }
+  .lrow .cand-panel { padding:0 14px 10px; }
+  @media (max-width:900px){ .lident{ min-width:100%; } }
   .field { display:flex; gap:6px; font-size:12px; }
   .field .k { color:var(--muted); min-width:52px; }
   .badmfr { color:var(--red); text-decoration:line-through; font-variant-numeric:tabular-nums; }
@@ -214,7 +246,7 @@
       <span class="count" id="count"></span>
       <span class="filters-active" id="activeFilters"></span>
     </div>
-    <div class="grid" id="grid"></div>
+    <div id="list" data-nav-grid></div>
   </main>
 </div>
 
@@ -279,7 +311,7 @@ const state = {
 };
 
 const $ = (s)=>document.querySelector(s);
-const grid = $('#grid');
+const list = $('#list');
 
 function fmtWhen(iso){
   if(!iso) return '🕓 no date';
@@ -367,42 +399,64 @@ function sortRows(rows){
 function fieldOn(f){ return state.fields[f]!==false; }
 
 function apply(){
+  // Density slider repurposed for the list: controls the field-chip min column width.
   document.documentElement.style.setProperty('--cols', state.density);
+  const chipMin = Math.round(240 - state.density*13);   // denser = narrower chip cols
+  document.documentElement.style.setProperty('--lmeta-min', chipMin+'px');
   const rows = sortRows(filteredRows());
-  grid.innerHTML='';
+  list.innerHTML='';
   const frag = document.createDocumentFragment();
   for(const r of rows){
-    const card = document.createElement('div');
-    card.className='card'+(state.sel.has(r.dw_sku)?' sel':'');
+    const row = document.createElement('div');
+    row.className='lrow'+(state.sel.has(r.dw_sku)?' sel':'');
     const guessVal = (state.edits[r.dw_sku]!==undefined)?state.edits[r.dw_sku]:(r.best_guess_real_mfr||'');
     const confClass = 'conf-'+String(r.confidence||'na').replace(/[^a-z]/g,'-');
-    let html = `<div class="top">
-      <input type="checkbox" class="chk" ${state.sel.has(r.dw_sku)?'checked':''}>
-      <div class="dw">${r.dw_sku}</div></div>`;
-    if(fieldOn('badmfr')) html += `<div class="field"><span class="k">bad mfr</span><span class="badmfr">${r.bad_mfr}</span></div>`;
-    if(fieldOn('vendor')) html += `<div class="field"><span class="k">vendor</span><span class="vendor">${r.vendor||'—'}</span></div>`;
-    if(fieldOn('pattern')) html += `<div class="field"><span class="k">pattern</span><span class="pattern">${r.pattern_name||'—'}</span></div>`;
-    if(fieldOn('guess')) html += `<div class="field"><span class="k">real mfr</span><input class="guess-in" value="${(guessVal).replace(/"/g,'&quot;')}" placeholder="enter real mfr…"></div>`;
-    if(fieldOn('badges')) html += `<div class="badges">
-      <span class="b src">${r.source}</span>
-      <span class="b ${confClass}">${r.confidence}</span>
-      <span class="b tier-${r.tier}">${r.tier}</span>
-      ${r.peer?'<span class="b" style="color:#f0abfc">peer</span>':''}</div>`;
-    if(fieldOn('when')) html += `<div class="when" title="${r.created_at||''}">${fmtWhen(r.created_at)}</div>`;
-    html += `<button class="cand-toggle" data-sku="${r.dw_sku.replace(/"/g,'&quot;')}">▸ candidates</button>
+    const sugMfr = (guessVal||'').trim();
+
+    // ---- identity block (left): checkbox + DW SKU + bad→suggested mfr + status badge ----
+    let ident = `<span class="lident">
+      <input type="checkbox" class="chk" ${state.sel.has(r.dw_sku)?'checked':''} title="select for staging">
+      <span class="dw">${esc(r.dw_sku)}</span>
+      <span class="mfr-transition" title="bad DW#-placeholder mfr → suggested real mfr">
+        <span class="from">${esc(r.bad_mfr)}</span><span class="arrow">→</span><span class="to">${sugMfr?esc(sugMfr):'?'}</span>
+      </span>
+      <span class="status-badge">ACTIVE</span>
+    </span>`;
+
+    // ---- field chips (right): uppercase micro-label + value, honoring field toggles ----
+    let chips = '';
+    if(fieldOn('vendor')) chips += `<span class="lm" data-field="Vendor"><span class="lmk">Vendor</span>${esc(r.vendor||'—')}</span>`;
+    if(fieldOn('badmfr')) chips += `<span class="lm" data-field="Bad Mfr"><span class="lmk">Bad Mfr</span><span class="badmfr-chip">${esc(r.bad_mfr)}</span></span>`;
+    if(fieldOn('pattern')) chips += `<span class="lm" data-field="Pattern"><span class="lmk">Pattern</span>${esc(r.pattern_name||'—')}</span>`;
+    if(fieldOn('guess')) chips += `<span class="lm" data-field="Suggested Mfr"><span class="lmk">Suggested Mfr</span><input class="guess-in" value="${esc(guessVal)}" placeholder="enter real mfr…"></span>`;
+    if(fieldOn('badges')){
+      chips += `<span class="lm" data-field="Source"><span class="lmk">Source</span><span class="b src" style="border:none;padding:0">${esc(r.source)}</span></span>`;
+      chips += `<span class="lm" data-field="Tier"><span class="lmk">Tier</span><span class="b tier-${esc(r.tier)}" style="border:none;padding:0">${esc(r.tier)}</span></span>`;
+      chips += `<span class="lm" data-field="Confidence"><span class="lmk">Confidence</span><span class="b ${confClass}" style="border:none;padding:0">${esc(r.confidence)}</span></span>`;
+      if(r.peer) chips += `<span class="lm" data-field="Peer"><span class="lmk">Peer</span><span style="color:#f0abfc">✓</span></span>`;
+    }
+    if(fieldOn('when')) chips += `<span class="lm" data-field="Created" title="${esc(r.created_at||'')}"><span class="lmk">Created</span>${esc(fmtWhen(r.created_at).replace(/^🕓 /,''))}</span>`;
+
+    row.innerHTML = `<div class="lrow-head">${ident}<div class="lmeta">${chips}</div></div>
+      <button class="cand-toggle" data-sku="${esc(r.dw_sku)}">▸ candidates</button>
       <div class="cand-panel hidden"></div>`;
-    card.innerHTML = html;
 
-    const chk = card.querySelector('.chk');
+    const chk = row.querySelector('.chk');
     chk.onchange = ()=>{ chk.checked?state.sel.add(r.dw_sku):state.sel.delete(r.dw_sku);
-      card.classList.toggle('sel',chk.checked); updateSel(); };
-    const gi = card.querySelector('.guess-in');
-    if(gi) gi.oninput = ()=>{ state.edits[r.dw_sku]=gi.value; };
-    const tog = card.querySelector('.cand-toggle');
-    tog.onclick = ()=>toggleCandidates(r.dw_sku, tog, card.querySelector('.cand-panel'));
-    frag.appendChild(card);
+      row.classList.toggle('sel',chk.checked); updateSel(); };
+    const gi = row.querySelector('.guess-in');
+    if(gi){
+      gi.oninput = ()=>{ state.edits[r.dw_sku]=gi.value;
+        // keep the identity-block "→ suggested" preview in sync as they type
+        const to = row.querySelector('.mfr-transition .to');
+        if(to) to.textContent = gi.value.trim() || '?'; };
+      gi.onclick = (e)=>e.stopPropagation();   // don't toggle candidates when editing
+    }
+    const tog = row.querySelector('.cand-toggle');
+    tog.onclick = ()=>toggleCandidates(r.dw_sku, tog, row.querySelector('.cand-panel'));
+    frag.appendChild(row);
   }
-  grid.appendChild(frag);
+  list.appendChild(frag);
   $('#count').textContent = `${rows.length} of ${state.rows.length} rows`;
   const af = Object.entries(state.filters).map(([k,s])=>`${k}:${[...s].join(',')}`).join(' · ');
   $('#activeFilters').textContent = af?('▸ '+af):'';
@@ -465,12 +519,14 @@ function applySuggestion(sku, j){
 function chooseMfr(sku, value){
   state.edits[sku] = value;
   state.candPick[sku] = value;                       // remember pick (may be '' for none)
-  // Update the live input in the card WITHOUT a full re-render (keeps the panel open).
-  for(const card of document.querySelectorAll('.card')){
-    const t = card.querySelector('.cand-toggle');
+  // Update the live input in the row WITHOUT a full re-render (keeps the panel open).
+  for(const rowEl of document.querySelectorAll('.lrow')){
+    const t = rowEl.querySelector('.cand-toggle');
     if(t && t.dataset.sku===sku){
-      const gi = card.querySelector('.guess-in');
+      const gi = rowEl.querySelector('.guess-in');
       if(gi) gi.value = value;
+      const to = rowEl.querySelector('.mfr-transition .to');
+      if(to) to.textContent = (value||'').trim() || '?';
       break;
     }
   }
@@ -801,5 +857,10 @@ $('#sweepExecGo').onclick = ()=>{
 
 load();
 </script>
+<!-- kv-list (reference component; the record renderer uses the same .kv-* row model inline) -->
+<script src="/kv-list.js" defer></script><!-- kv-list -->
+<!-- nav-agent: full grid-controls nav bar (field-toggle / sort / group-by / density / movable panel).
+     Mounts on #list; collapses the host <aside> on load (2-into-1) and drops a ☰ toggle to reveal it. -->
+<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
 </body>
 </html>
diff --git a/public/kv-list.css b/public/kv-list.css
new file mode 100644
index 0000000..4120d41
--- /dev/null
+++ b/public/kv-list.css
@@ -0,0 +1,35 @@
+/* kv-list.css — compact key/value record list. Namespaced .kv-* so it never
+ * collides with a host page. Colors read the host page's own CSS vars with safe
+ * fallbacks, so the list inherits each page's theme (dark / liquid-glass / light)
+ * automatically. Override any --kv-* var to force specific colors.
+ * Standalone extraction of three-view.css's compact-list mode. */
+
+.kv-rows { display:flex; flex-direction:column; }
+
+.kv-lrow {
+  display:flex; align-items:baseline; gap:14px; flex-wrap:wrap;
+  padding:10px 16px;
+  border-bottom:1px solid var(--kv-line,var(--line,var(--glassb,rgba(255,255,255,.07))));
+  cursor:default; }
+.kv-lrow.kv-click { cursor:pointer; }
+.kv-lrow:hover { background:var(--kv-hover,var(--hover,rgba(120,160,255,.06))); }
+
+/* fixed identity block, left */
+.kv-lmain { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; min-width:210px; flex:0 0 auto; }
+.kv-la { font-weight:600; color:var(--kv-ink,var(--ink,#e6edf3)); }
+.kv-la a { color:inherit; text-decoration:none; }
+.kv-la a:hover { text-decoration:underline; }
+.kv-lsub { font-size:11.5px; color:var(--kv-mut,var(--mut,var(--muted,#8b949e))); }
+
+/* flowing field chips, right */
+/* full-width tight columns (left edge → right edge), not left-packed chips */
+.kv-lmeta { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:6px 16px; flex:1 1 320px; min-width:0; align-content:start; }
+.kv-lm { font-size:12.5px; color:var(--kv-ink,var(--ink,#e6edf3)); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.kv-lmk { color:var(--kv-mut,var(--mut,var(--muted,#8b949e))); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:5px; }
+.kv-lm a { color:var(--kv-acc,var(--blue,#58a6ff)); text-decoration:none; }
+.kv-lm a:hover { text-decoration:underline; }
+
+.kv-badge { font-size:11px; }
+.kv-empty { padding:50px; text-align:center; color:var(--kv-mut,var(--mut,var(--muted,#8b949e))); }
+
+@media (max-width:820px){ .kv-lmain{ min-width:100%; } }
diff --git a/public/kv-list.js b/public/kv-list.js
new file mode 100644
index 0000000..d066561
--- /dev/null
+++ b/public/kv-list.js
@@ -0,0 +1,87 @@
+/* kv-list.js — compact key/value record list. Zero dependencies.
+ *
+ * Renders each record as ONE scannable row: a fixed identity block on the left
+ * (title + subline + optional badge) and every visible field as inline
+ * uppercase-micro-label + value chips flowing to the right, wrapping responsively.
+ *
+ * Standalone extraction of three-view.js's `listCompact()` (CRCP TK-10526),
+ * renamed .kv-* so it drops into any page without dragging the rest of three-view.
+ *
+ * KVList.render({
+ *   mount:  '#kv',                       // selector | element (required)
+ *   rows:   [ {...}, ... ],              // record objects, in display order (required)
+ *   fields: [ {k,l,calc,money,num,pct,date,html,suf}, ... ],  // field metadata (required)
+ *   title:    row => ({name, sub?, href?}),   // left identity block
+ *   badgeHtml:row => '<span class="kv-badge">…</span>' | '',  // optional badge
+ *   fvis:     k => bool,                 // optional per-field visibility filter
+ *   rowClick: (row, el) => {},           // optional row click handler
+ *   empty:    'No matches.',             // optional empty message
+ * })
+ */
+(function (global) {
+  function esc(s) {
+    return String(s == null ? '' : s)
+      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+  }
+
+  function fmt(v, f) {
+    if (f.html) return v == null ? '—' : String(v);        // page-formatted HTML passes through
+    if (v == null || v === '') return '—';
+    if (f.money) return '$' + (+v).toLocaleString() + (f.suf || '');
+    if (f.pct)   return (+v).toFixed(2) + '%';
+    if (f.num)   return (+v).toLocaleString() + (f.suf || '');
+    if (f.date)  { try { return new Date(v).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { return '—'; } }
+    return esc(v) + (f.suf || '');
+  }
+
+  var fval = function (r, f) { return f.calc ? f.calc(r) : r[f.k]; };
+
+  function resolve(m) {
+    if (!m) return null;
+    return typeof m === 'string' ? document.querySelector(m) : m;
+  }
+
+  function render(O) {
+    var mount = resolve(O.mount);
+    if (!mount) { console.warn('[kv-list] mount not found:', O.mount); return; }
+
+    var rows   = O.rows || [];
+    var fields = (O.fields || []).filter(function (f) {
+      if (f.k === 'title' || f.badge) return false;
+      return O.fvis ? O.fvis(f.k) : true;
+    });
+
+    if (!rows.length) {
+      mount.innerHTML = '<div class="kv-empty">' + esc(O.empty || 'No matches.') + '</div>';
+      return;
+    }
+
+    mount.innerHTML = '<div class="kv-rows">' + rows.map(function (r, i) {
+      var t  = O.title ? O.title(r) : { name: r.name || r.title || '', sub: '' };
+      var nm = t.href
+        ? '<a href="' + esc(t.href) + '" target="_blank" rel="noopener noreferrer">' + esc(t.name) + '</a>'
+        : esc(t.name);
+      var badge = O.badgeHtml ? (O.badgeHtml(r) || '') : '';
+      return '<div class="kv-lrow' + (O.rowClick ? ' kv-click' : '') + '" data-kvri="' + i + '">'
+        + '<div class="kv-lmain"><span class="kv-la">' + nm + '</span>'
+        + (t.sub ? '<span class="kv-lsub">' + esc(t.sub) + '</span>' : '') + badge + '</div>'
+        + '<div class="kv-lmeta">'
+        + fields.map(function (f) {
+            return '<span class="kv-lm"><span class="kv-lmk">' + esc(f.l) + '</span>' + fmt(fval(r, f), f) + '</span>';
+          }).join('')
+        + '</div></div>';
+    }).join('') + '</div>';
+
+    if (O.rowClick) {
+      mount.querySelectorAll('.kv-lrow').forEach(function (el) {
+        el.addEventListener('click', function (e) {
+          if (e.target.closest('a')) return;
+          O.rowClick(rows[+el.dataset.kvri], el);
+        });
+      });
+    }
+  }
+
+  global.KVList = { render: render, fmt: fmt };
+})(typeof window !== 'undefined' ? window : this);
diff --git a/public/nav-agent/nav-agent.css b/public/nav-agent/nav-agent.css
new file mode 100644
index 0000000..d43a0d1
--- /dev/null
+++ b/public/nav-agent/nav-agent.css
@@ -0,0 +1,133 @@
+/* ============================================================================
+ * 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}
+
+/* ---- Grid / List view toggle ----------------------------------------- */
+.na-viewseg{display:inline-flex;margin:4px 0 10px;border:1px solid var(--na-line);border-radius:6px;overflow:hidden}
+.na-viewseg button{background:var(--na-bg2);color:var(--na-mut);border:0;border-right:1px solid var(--na-line);
+  padding:6px 12px;font-size:12px;cursor:pointer}
+.na-viewseg button:last-child{border-right:0}
+.na-viewseg button.on{background:var(--na-acc);color:#fff}
+
+/* ---- compact LIST view (the kv-list "third view") --------------------- */
+.na-list{width:100%}
+.na-lrows{display:flex;flex-direction:column}
+.na-lrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;padding:10px 14px;
+  border-bottom:1px solid var(--na-line)}
+.na-lrow:hover{background:rgba(255,255,255,.04)}
+.na-lmain{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;min-width:210px;flex:0 0 auto}
+.na-la{font-weight:600;color:var(--na-fg)}
+/* data spans the FULL row width in tight columns (left edge → right edge),
+   not left-packed chips — matches the CRCP "expanded to left and right" layout */
+.na-lmeta{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px 16px;flex:1 1 320px;min-width:0;align-content:start}
+.na-lm{font-size:12.5px;color:var(--na-fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.na-lmk{color:var(--na-mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px}
+.na-lempty{padding:40px;text-align:center;color:var(--na-mut)}
+
+/* host left-sidebar collapse (v1.3) — always collapsed on load, ☰ reveals */
+.na-side{transition:transform .18s ease}
+.na-side.na-side-collapsed{transform:translateX(-100%)!important;pointer-events:none}
+.na-side-main{transition:margin-left .18s ease}
+.na-side-main.na-side-shift{margin-left:0!important}
+.na-side-toggle{position:fixed;left:10px;top:10px;z-index:2147483002;width:38px;height:34px;
+  display:flex;align-items:center;justify-content:center;border-radius:8px;
+  border:1px solid var(--na-line);background:var(--na-bg2);color:var(--na-fg);
+  font-size:16px;line-height:1;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.28)}
+.na-side-toggle:hover,.na-side-toggle.on{border-color:var(--na-acc);color:var(--na-acc)}
+
+@media (max-width:640px){.na-panel{width:86vw}.na-lmain{min-width:100%}}
diff --git a/public/nav-agent/nav-agent.js b/public/nav-agent/nav-agent.js
new file mode 100644
index 0000000..d368cf4
--- /dev/null
+++ b/public/nav-agent/nav-agent.js
@@ -0,0 +1,620 @@
+/* ============================================================================
+ * Nav Agent — universal grid-controls drop-in  (v1.3)
+ *
+ * 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
+ *   • a Grid / List view toggle — List is the compact "kv-list" third view:
+ *     one scannable row per record, first visible field as the identity + the
+ *     rest as inline LABEL·value chips (default OFF; the native grid is default)
+ *   • 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) {
+      var dfCells = [].slice.call(card.querySelectorAll('[data-field]'));
+      dfCells.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++;
+      });
+      // coarse child-class fields ONLY when this card tags no [data-field] cells —
+      // otherwise a cell with both a class and a data-field yields a duplicate field.
+      if (dfCells.length) return;
+      [].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(); if (self.renderList && st.view === 'list') self.renderList(); }
+
+    /* 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]; }); }
+
+  /* -- Grid / List view toggle (shared by standalone + merged panels) ---- */
+  function viewToggle(agent) {
+    var st = agent.state, wrap = el('div', 'na-viewseg');
+    wrap.innerHTML = '<button data-v="grid">▦ Grid</button><button data-v="list">☰ List</button>';
+    function mark() { [].slice.call(wrap.children).forEach(function (b) { b.classList.toggle('on', (st.view || 'grid') === b.getAttribute('data-v')); }); }
+    [].slice.call(wrap.children).forEach(function (b) { b.onclick = function () { agent.setView(b.getAttribute('data-v')); mark(); }; });
+    mark(); return wrap;
+  }
+
+  /* -- 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);
+
+    // Grid / List view toggle (only meaningful when we have fields to lay out)
+    if (M.fields && M.fields.length) body.appendChild(viewToggle(agent));
+
+    // 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');
+    // ROOT FIX (Steve-approved 2026-08-17): a "removed" grid must NEVER stay hidden across reloads —
+    // that trap has no recovery and blanked whole pages (CRCP mls.html). Do NOT restore `removed` on
+    // load: the grid always reappears; in-session Remove still works, it just no longer persist-hides.
+    // (Float / max / collapsed still persist — those are safe, recoverable layout choices.)
+  }
+
+  /* -- 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);
+    }
+    if (M.fields && M.fields.length) box.appendChild(viewToggle(agent));
+    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;
+
+        // ---- compact LIST view (the kv-list "third view", v1.2) -----------
+        // Renders each live grid row as one scannable line: first visible field
+        // as the identity, the rest as inline LABEL·value chips. DOM-based (reads
+        // M.cellText), so sort/search/field-toggle all flow through for free.
+        // Default OFF — the page's native grid stays the default view.
+        function ensureListEl() {
+          if (agent._listEl) return agent._listEl;
+          var lel = el('div', 'na-list');
+          (target.parentNode || target).insertBefore(lel, target.nextSibling);
+          agent._listEl = lel; return lel;
+        }
+        agent.renderList = function () {
+          var lel = agent._listEl; if (!lel) return;
+          var M2 = agent.model, hidden = agent.state.hidden || [];
+          var vis = (M2.fields || []).filter(function (f) { return hidden.indexOf(f.key) < 0; });
+          if (!vis.length) vis = (M2.fields || []).slice();
+          if (!vis.length) { lel.innerHTML = '<div class="na-lempty">No structured fields detected on this grid.</div>'; return; }
+          var titleF = vis[0], chipF = vis.slice(1);
+          var rows = M2.rows().filter(function (r) { return r.style.display !== 'none'; });
+          if (!rows.length) { lel.innerHTML = '<div class="na-lempty">No matching rows.</div>'; return; }
+          lel.innerHTML = '<div class="na-lrows">' + rows.map(function (r) {
+            var tt = esc(M2.cellText(r, titleF) || '—');
+            var chips = chipF.map(function (f) {
+              var v = M2.cellText(r, f); if (v === '' || v == null) return '';
+              return '<span class="na-lm"><span class="na-lmk">' + esc(f.label) + '</span>' + esc(v) + '</span>';
+            }).join('');
+            return '<div class="na-lrow"><div class="na-lmain"><span class="na-la">' + tt + '</span></div>' +
+                   '<div class="na-lmeta">' + chips + '</div></div>';
+          }).join('') + '</div>';
+        };
+        agent.setView = function (v) {
+          agent.state.view = (v === 'list') ? 'list' : 'grid'; agent.save();
+          if (agent.state.view === 'list') { ensureListEl(); target.style.display = 'none'; agent._listEl.style.display = ''; agent.renderList(); }
+          else { if (agent._listEl) agent._listEl.style.display = 'none'; target.style.display = ''; }
+        };
+        // "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();
+        // restore a persisted List-view choice (default is Grid — nothing changes)
+        if (agent.state.view === 'list') agent.setView('list');
+
+        // 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;
+
+  /* -- host left-sidebar collapse (standard feature, v1.3, 2026-08-20) ----
+   * The Nav Agent collapses the host page's primary LEFT sidebar on load and
+   * adds a ☰ toggle to reveal/hide it. ALWAYS collapsed on load — the reveal is
+   * an in-session choice (deliberately NOT persisted), so every page load starts
+   * clean and collapsed. Runs even on pages with no data grid.
+   *   Opt out:  <body data-nav-sidebar="off">
+   *   Force:    <body data-nav-sidebar="#mySelector">  or  <aside data-nav-sidebar>
+   *   Content:  the shifted main column auto-detects; force with [data-nav-main]. */
+  function naIsSidebar(e) {
+    if (!e || e === document.body) return false;
+    if (e.classList && (e.classList.contains('na-panel') || e.classList.contains('na-list') ||
+        e.classList.contains('na-launch') || e.classList.contains('na-side-toggle'))) return false;
+    var cs; try { cs = getComputedStyle(e); } catch (x) { return false; }
+    if (['fixed', 'sticky', 'absolute'].indexOf(cs.position) < 0) return false;
+    var r = e.getBoundingClientRect();
+    var atLeft = r.left <= 6;
+    var tall = r.height >= Math.min(window.innerHeight * 0.55, 380);
+    var narrow = r.width > 20 && r.width <= 360;
+    var hasNav = e.querySelectorAll('a,button,nav,.nav').length >= 1;
+    return atLeft && tall && narrow && hasNav;
+  }
+  function naFindSidebar() {
+    var body = document.body, a = body.getAttribute('data-nav-sidebar');
+    if (a === 'off') return null;
+    if (a && a !== 'on' && a !== '') { var s = document.querySelector(a); if (s) return s; }
+    var marked = document.querySelector('[data-nav-sidebar]');
+    if (marked && marked !== body) return marked;
+    var sel = ['#side', '#sidebar', '.sidebar', '#side-nav', '.side-nav', '#leftnav', '.leftnav', 'aside'];
+    for (var i = 0; i < sel.length; i++) { var e = document.querySelector(sel[i]); if (naIsSidebar(e)) return e; }
+    var kids = [].slice.call(body.children);
+    for (var j = 0; j < kids.length; j++) { if (naIsSidebar(kids[j])) return kids[j]; }
+    return null;
+  }
+  function naFindMain(side) {
+    var m = document.querySelector('[data-nav-main]'); if (m) return m;
+    var w = side.getBoundingClientRect().width;
+    var pool = [].slice.call(document.body.children).filter(function (c) {
+      return c.nodeType === 1 && c !== side &&
+        !(c.classList && (c.classList.contains('na-panel') || c.classList.contains('na-launch') ||
+          c.classList.contains('na-side-toggle') || c.classList.contains('na-list')));
+    });
+    for (var i = 0; i < pool.length; i++) {
+      var ml = parseFloat(getComputedStyle(pool[i]).marginLeft) || 0;
+      if (ml > 40 && Math.abs(ml - w) <= 28) return pool[i];
+    }
+    return side.nextElementSibling || null;
+  }
+  function naMountSidebar() {
+    try {
+      if (document.body.getAttribute('data-nav-agent') === 'off') return;
+      if (document.querySelector('.na-side-toggle')) return;   // once per page
+      var side = naFindSidebar(); if (!side) return;
+      side.classList.add('na-side');
+      var main = naFindMain(side); if (main) main.classList.add('na-side-main');
+      var open = false;                                        // ALWAYS collapsed on load
+      var tog = el('button', 'na-side-toggle', '☰'); tog.type = 'button';
+      function apply() {
+        side.classList.toggle('na-side-collapsed', !open);
+        if (main) main.classList.toggle('na-side-shift', !open);
+        tog.classList.toggle('on', open);
+        tog.title = open ? 'Collapse sidebar' : 'Expand sidebar';
+        tog.setAttribute('aria-expanded', open ? 'true' : 'false');
+      }
+      tog.onclick = function () { open = !open; apply(); };
+      document.body.appendChild(tog);
+      apply();
+    } catch (e) { if (window.console) console.warn('[nav-agent] sidebar collapse skipped:', e); }
+  }
+  API.collapseSidebar = naMountSidebar;
+
+  function boot() {
+    try {
+      var body = document.body;
+      if (!body || body.getAttribute('data-nav-agent') === 'off') return;
+      naMountSidebar(); // collapse the host left sidebar on load (grid-independent)
+      // wait a tick for API-driven grids to render their first batch
+      var tries = 0;
+      (function attempt() {
+        naMountSidebar(); // idempotent — catches a late-rendered sidebar too
+        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) {
+          var _agent = API.mount(); // mount anyway after ~8s; observer fills fields when data arrives
+          // diagnosability (DTD C, 2026-08-13): a null mount is otherwise SILENT — md5/marker/200
+          // all pass while no panel/launcher appears. Emit ONE passive console.info (never warn/error,
+          // never throw, never alter flow) so a future silent-null-mount is greppable, not invisible.
+          // Only nudge when there is GENUINELY no target. mount() also returns null when it
+          // THREW mid-mount on a real grid (its own console.warn already fired) — re-check
+          // findTarget() so we never emit a false "no grid" on a page that actually has one.
+          if (!_agent) { try { if (findTarget() === null && window.console && console.info) console.info('[nav-agent] no grid target found on this marker-bearing page — add data-nav-grid to the real grid container, or data-nav-agent="off" to opt out (see _shared/nav-agent/README.md).'); } catch (e) {} }
+          return;
+        }
+        setTimeout(attempt, 250);
+      })();
+    } catch (e) {}
+  }
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
+  else boot();
+})();

← dcd7c8f scope to ACTIVE patterns only: default status='ACTIVE' in bu  ·  back to Mfr Review Viewer Corruption  ·  silence cosmetic /favicon.ico 404 (204 no-content) ee721d4 →