# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: counters.spec.js >> Counters — HTML structure >> Phase 4.17 — comments modal + RPCs referenced
- Location: tests/counters.spec.js:222:3

# Error details

```
Error: expect(received).toContain(expected) // indexOf

Expected substring: "💬 Comments"
Received string:    "<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,viewport-fit=cover\">
<title>Counters</title>
<link rel=\"manifest\" href=\"manifest.json\">
<link rel=\"stylesheet\" href=\"/shared/safe-bottom.css\">
<script src=\"/shared/tour.js\"></script>
<meta name=\"theme-color\" content=\"#0a0a0a\">
<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap\">
<style>
:root{
  --bg:#0a0a0a;--card:#141414;--card2:#1a1a1a;--border:#262626;--text:#f5f5f5;--muted:#a3a3a3;
  --accent:#ec4899;--accent2:#db2777;--g:#22c55e;--r:#ef4444;--y:#eab308;--b:#3b82f6;--p:#a855f7;
  --rad:12px;--rads:8px;--bnav-h:62px;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;min-height:100vh}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
a{color:inherit;text-decoration:none}·
.scr{display:none;flex-direction:column;min-height:100vh}
.scr.active{display:flex}·
.hdr{background:var(--card);border-bottom:1px solid var(--border);padding:12px 14px;display:flex;align-items:center;gap:8px;position:sticky;top:0;z-index:10}
.hdr h1{font-size:16px;font-weight:800;flex:1;min-width:0}
.hdr-sub{font-size:11px;color:var(--muted)}
.hdr-btn{background:rgba(255,255,255,.08);border:1px solid var(--border);font-size:12px;font-weight:700;padding:5px 10px;border-radius:14px;cursor:pointer;white-space:nowrap}
.hdr-btn:active{background:rgba(255,255,255,.16)}
.hdr-btn.p{background:var(--accent);border-color:var(--accent);color:#fff}
.back-btn{background:none;font-size:20px;font-weight:900;padding:4px 6px;color:var(--accent)}
.user-pill{background:rgba(255,255,255,.12);border:1px solid var(--border);color:var(--muted);font-size:10.5px;font-weight:600;padding:3px 9px;border-radius:12px;white-space:nowrap}
.user-pill b{color:var(--text)}
.role-tag{background:rgba(236,72,153,.15);color:#f9a8d4;font-size:9.5px;font-weight:800;padding:2px 6px;border-radius:8px;margin-left:4px;letter-spacing:.3px}·
.body{flex:1;padding:14px;display:flex;flex-direction:column;gap:12px}·
.tiles{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.tile{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:10px 5px;cursor:pointer;text-align:center}
.tile.sel{border-color:var(--accent);background:linear-gradient(135deg,#3a0f29,#1c0814)}
.tile-lbl{font-size:8.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px;font-weight:700}
.tile-val{font-size:16px;font-weight:800;margin-top:3px}·
.filter-chip{padding:4px 10px;border-radius:20px;border:1px solid var(--border);background:var(--card);color:var(--muted);font-size:.73rem;white-space:nowrap;cursor:pointer;font-family:inherit}
.epip{width:20px;height:6px;border-radius:3px;background:var(--border);transition:background .2s;flex-shrink:0}
.epip.done{background:var(--g)}.epip.active{background:var(--accent)}
.mbadge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.4px;padding:2px 7px;border-radius:10px;text-transform:uppercase}
.mbadge.exact{background:#14532d;color:#86efac}.mbadge.fuzzy{background:#713f12;color:#fde68a}.mbadge.newc{background:#1e3a5f;color:#93c5fd}
.enrich-diff-row{display:flex;align-items:baseline;gap:6px;font-size:11px;padding:2px 0}
.enrich-diff-row .ef-label{color:var(--muted);font-size:10px;min-width:70px;flex-shrink:0}
.enrich-diff-row .ef-old{color:var(--muted);text-decoration:line-through}
.enrich-diff-row .ef-new{color:var(--g);font-weight:600}
.filter-chip.active{background:var(--accent);border-color:var(--accent);color:#fff}
.filter-chip.unavailable{opacity:.32;pointer-events:none}
.filter-rows-wrap{display:flex;flex-direction:column;gap:5px}
.filter-section{}
.filter-label{font-size:.62rem;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
.filter-row{display:flex;gap:5px;overflow-x:auto;padding-bottom:2px}
.sum-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}
.sum-tile{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:7px 4px;text-align:center;line-height:1.3}·
.search-wrap{position:relative}
.search-wrap input{width:100%;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);color:var(--text);font-size:14px;padding:10px 12px 10px 38px;outline:none}
.search-wrap input:focus{border-color:var(--accent)}
.search-wrap .icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:16px}·
.sec-label{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin:6px 0 4px;display:flex;align-items:center;justify-content:space-between}·
.c-row{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:11px 13px;margin-bottom:6px;cursor:pointer}
.c-row:active{border-color:var(--accent)}
.c-row-top{display:flex;align-items:center;gap:8px}
.c-row-name{font-size:14px;font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.c-row-meta{font-size:11px;color:var(--muted);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
.c-row-meta .brands{display:flex;gap:4px;flex-wrap:wrap}
.brand-chip{font-size:9.5px;font-weight:700;padding:1px 7px;border-radius:10px;background:rgba(236,72,153,.18);color:#f9a8d4}
.status-chip{font-size:9px;font-weight:800;padding:1px 7px;border-radius:10px;text-transform:uppercase;letter-spacing:.3px}
.status-chip.active{background:#14532d;color:#86efac}
.status-chip.inactive{background:#374151;color:#9ca3af}
.status-chip.blacklisted{background:#7c2d12;color:#fca5a5}
.status-chip.pending_review{background:#713f12;color:#fde68a}·
.form-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:14px;display:grid;gap:10px}
.form-grid{display:grid;grid-template-columns:1fr;gap:10px}
@media (min-width:520px){.form-grid{grid-template-columns:1fr 1fr}}
.field{display:flex;flex-direction:column;gap:3px}
.field label{font-size:11px;font-weight:600;color:var(--muted)}
.field label .req{color:var(--r)}
.field input,.field textarea,.field select{background:#0b1220;border:1px solid var(--border);border-radius:6px;color:var(--text);padding:9px 11px;font-size:13px;font-family:inherit;outline:none}
.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--accent)}
.field .hint{font-size:10px;color:var(--muted)}
.field textarea{min-height:60px;resize:vertical}
.full{grid-column:1/-1}·
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:var(--rads);padding:11px 14px;font-size:14px;font-weight:700;cursor:pointer;width:100%}
.btn-p{background:var(--accent);color:#fff}
.btn-p:active{background:var(--accent2)}
.btn-p:disabled{opacity:.45;cursor:default}
.btn-s{background:var(--card);color:var(--text);border:1px solid var(--border)}
.btn-r{background:rgba(239,68,68,.15);color:#fca5a5;border:1px solid rgba(239,68,68,.35)}
.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}·
.k-row{background:var(--card);border:1px solid var(--border);border-radius:var(--rads);padding:10px 12px;margin-bottom:6px;display:flex;align-items:center;gap:8px}
.k-row .k-main{flex:1;min-width:0}
.k-title{font-size:13px;font-weight:700}
.k-meta{font-size:11px;color:var(--muted);margin-top:2px;display:flex;gap:8px;flex-wrap:wrap}
.k-badge{font-size:9px;font-weight:800;padding:1px 7px;border-radius:10px;text-transform:uppercase}
.k-badge.primary,.k-badge.default{background:#14532d;color:#86efac}
.k-badge.owner{background:#581c87;color:#d8b4fe}
.k-badge.manager{background:#082f49;color:#7dd3fc}
.k-badge.accountant{background:#365314;color:#bef264}
.k-badge.billing{background:#7c2d12;color:#fdba74}
.k-badge.shipping{background:#1e3a8a;color:#93c5fd}
.k-badge.site{background:#3b0764;color:#d8b4fe}
.k-badge.warehouse{background:#374151;color:#d1d5db}
.k-badge.other{background:#374151;color:#9ca3af}
.k-actions{display:flex;gap:4px;flex-shrink:0}
.k-actions button{background:rgba(255,255,255,.05);border:1px solid var(--border);font-size:11px;font-weight:700;padding:4px 8px;border-radius:6px;cursor:pointer}·
.tabs{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:8px;overflow-x:auto}
.tabs button{flex:1;padding:9px 8px;font-size:12px;font-weight:700;color:var(--muted);border-bottom:2px solid transparent;background:none;white-space:nowrap}
.tabs button.sel{color:var(--accent);border-bottom-color:var(--accent)}·
.empty{text-align:center;padding:32px 16px;color:var(--muted)}
.empty-icon{font-size:36px;margin-bottom:8px}·
/* dedup suggestions during new-counter create */
.dedup-panel{background:#221013;border:1px solid #7c2d12;border-radius:var(--rads);padding:9px 11px;font-size:11.5px;color:#fdba74;display:none}
.dedup-panel.show{display:block}
.dedup-panel .ttl{font-weight:800;margin-bottom:4px}
.dedup-panel button{background:rgba(255,255,255,.08);color:#fdba74;border:1px solid #7c2d12;border-radius:6px;padding:4px 8px;font-size:11px;font-weight:700;margin-top:3px;margin-right:5px}·
/* brand chips editor */
.bcset{display:flex;flex-wrap:wrap;gap:6px;align-items:center;background:#0b1220;border:1px solid var(--border);border-radius:6px;padding:7px;min-height:42px}
.bcset .b{background:rgba(236,72,153,.2);color:#f9a8d4;font-size:11.5px;font-weight:700;padding:3px 8px;border-radius:12px;display:inline-flex;align-items:center;gap:4px}
.bcset .b button{background:none;color:inherit;font-size:13px;line-height:1;padding:0 2px;cursor:pointer;font-weight:800}
.bcset input{background:none;border:none;color:var(--text);font-size:13px;padding:4px;flex:1;min-width:80px;outline:none}·
/* modal for contact / address */
.modal{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:200;display:none;align-items:flex-end;justify-content:center}
.modal.show{display:flex}
.modal-box{background:var(--card);border-radius:18px 18px 0 0;width:100%;max-width:560px;max-height:88vh;overflow-y:auto;padding:16px 14px calc(16px + env(safe-area-inset-bottom));border-top:1px solid var(--border)}
.modal-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.modal-ttl{font-size:15px;font-weight:800}
.modal-close{font-size:24px;color:var(--muted);background:none;border:none;cursor:pointer}·
/* toast */
#toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:10px 16px;border-radius:18px;font-size:13px;font-weight:600;box-shadow:0 4px 12px rgba(0,0,0,.5);opacity:0;transition:.2s;z-index:300;pointer-events:none;max-width:90vw;text-align:center}
#toast.show{opacity:1}
#toast.ok{background:#14532d;color:#86efac}
#toast.err{background:#7f1d1d;color:#fca5a5}·
/* loader */
.spin{display:inline-block;animation:spin 1s linear infinite}
@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}·
/* fixed bottom action bar inside detail/edit */
.bnav{position:fixed;bottom:0;left:0;right:0;background:var(--card);border-top:1px solid var(--border);padding:10px 14px calc(10px + env(safe-area-inset-bottom));display:flex;gap:8px;z-index:50}
.bnav .btn{padding:10px 12px;font-size:13px}·
/* update banner */
.upd-banner{display:none;background:#082f49;border-bottom:1px solid #0c4a6e;padding:7px 13px;font-size:12px;color:#7dd3fc;text-align:center;cursor:pointer}
.upd-banner.show{display:block}
.alloc-warn-banner{display:none;background:#431407;border-bottom:2px solid #c2410c;padding:8px 13px;font-size:12px;color:#fed7aa;align-items:center;gap:8px;justify-content:space-between}
.alloc-warn-banner.show{display:flex}
.alloc-warn-banner button{background:rgba(249,115,22,.25);border:1px solid rgba(249,115,22,.4);color:#fed7aa;font-size:11px;font-weight:700;padding:4px 10px;border-radius:10px;cursor:pointer;white-space:nowrap;flex-shrink:0}·
/* Phase 2: allocation modal employee tiles */
.alloc-tiles{display:grid;grid-template-columns:repeat(2,1fr);gap:6px}
.alloc-tile{background:#0b1220;border:1px solid var(--border);border-radius:var(--rads);padding:9px 11px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:8px;transition:.12s}
.alloc-tile:active{transform:scale(.98)}
.alloc-tile.sel{background:rgba(34,197,94,.18);border-color:#22c55e;color:#86efac}
.alloc-tile .cb{font-size:14px;line-height:1}
.alloc-tile .nm{font-weight:700;line-height:1.2}
.alloc-tile .ro{font-size:10px;color:var(--muted);margin-top:1px;line-height:1.2}
.alloc-tile.sel .ro{color:#86efac;opacity:.7}
.alloc-tile.all-tile{grid-column:1/-1;background:linear-gradient(135deg,#0c2e3a,#062a3a);border-color:var(--accent);color:#fff}
.alloc-tile.all-tile.sel{background:linear-gradient(135deg,#3a0f29,#1c0814);border-color:#22c55e}
.alloc-summary{font-size:11.5px;color:var(--muted);padding:6px 0;border-bottom:1px solid var(--border);margin-bottom:8px}·
/* Phase 2: upload queue rows */
.upload-row{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:11px 13px;margin-bottom:7px}
.upload-row.expanded{border-color:var(--accent)}
.upload-row .ur-top{display:flex;align-items:center;gap:8px;cursor:pointer}
.upload-row .ur-name{font-size:14px;font-weight:700;flex:1;min-width:0}
.upload-row .ur-meta{font-size:11px;color:var(--muted);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
.upload-row .ur-body{margin-top:10px;display:none}
.upload-row.expanded .ur-body{display:block}
.upload-rows-table{background:#0b1220;border:1px solid var(--border);border-radius:6px;padding:7px;font-size:11px;max-height:380px;overflow-y:auto;font-family:monospace;white-space:pre-wrap;word-break:break-word}
.upload-match-hint{font-size:11.5px;background:rgba(34,197,94,.12);border-left:3px solid #22c55e;padding:6px 9px;border-radius:4px;color:#86efac;margin-top:6px}
.upload-match-hint.partial{background:rgba(234,179,8,.12);border-left-color:#eab308;color:#fde68a}
.upload-conflicts{margin-top:5px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.25);border-radius:5px;padding:6px 8px}
.upload-conflicts .uc-hdr{font-size:10px;color:#fb923c;font-weight:700;margin-bottom:4px;letter-spacing:.2px}
.upload-conflicts .uc-row{display:flex;align-items:flex-start;gap:7px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer}
.upload-conflicts .uc-row:last-child{border-bottom:none}
.upload-conflicts .uc-row input[type=checkbox]{margin-top:1px;accent-color:#fb923c;cursor:pointer;flex-shrink:0}
.upload-conflicts .uc-content{flex:1;min-width:0}
.upload-conflicts .uc-label{font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.3px;display:block}
.upload-conflicts .uc-vals{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-top:1px}
.upload-conflicts .uc-old{font-size:10px;color:#6b7280;text-decoration:line-through}
.upload-conflicts .uc-new{font-size:10px;color:#fb923c;font-weight:600}·
/* Phase 3: Multi-Recce review */
.mrg-row{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:11px 13px;margin-bottom:7px}
.mrg-row.expanded{border-color:var(--accent)}
.mrg-row .mrg-top{display:flex;align-items:center;gap:8px;cursor:pointer}
.mrg-row .mrg-name{font-size:14px;font-weight:700;flex:1;min-width:0}
.mrg-row .mrg-meta{font-size:11px;color:var(--muted);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
.mrg-row .mrg-body{margin-top:12px;display:none}
.mrg-row.expanded .mrg-body{display:block}
.mrg-pick{font-size:9.5px;font-weight:800;padding:1px 7px;border-radius:10px;text-transform:uppercase}
.mrg-pick.has{background:#14532d;color:#86efac}
.mrg-pick.none{background:#7c2d12;color:#fdba74}
.recce-card{background:#0b1220;border:1px solid var(--border);border-radius:var(--rads);padding:10px 12px;margin-bottom:7px;display:flex;flex-direction:column;gap:5px}
.recce-card.official{border-color:#22c55e;background:linear-gradient(135deg,#0c2e1a,#062a14)}
.recce-card .rc-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
.recce-card .rc-date{font-size:13px;font-weight:700}
.recce-card .rc-meta{font-size:11px;color:var(--muted);display:flex;gap:8px;flex-wrap:wrap}
.recce-card .rc-actions{display:flex;gap:6px;margin-top:6px}
.recce-card .rc-actions button{flex:1;background:rgba(255,255,255,.05);border:1px solid var(--border);font-size:12px;font-weight:700;padding:7px 10px;border-radius:6px;cursor:pointer}
.recce-card .rc-actions .rc-pick{background:var(--accent);color:#fff;border-color:var(--accent)}
.recce-card .rc-actions .rc-pick:disabled{opacity:.45;cursor:default}
.rc-view{padding:4px 10px;border-radius:6px;border:1px solid var(--border);font-size:11px;cursor:pointer;font-family:inherit;background:var(--card);color:var(--text)}
.recce-card .rc-actions .rc-view{background:rgba(125,211,252,.1);color:#7dd3fc;border-color:rgba(125,211,252,.3)}
.recce-card .rc-official-badge{font-size:9.5px;font-weight:800;padding:1px 7px;border-radius:10px;background:#14532d;color:#86efac;text-transform:uppercase}·
/* Multi-Recce conflict resolver */
.mrg-ab-tabs{display:flex;gap:0;margin-bottom:8px}
.mrg-ab-tab{flex:1;padding:6px 8px;border:1px solid var(--border);background:transparent;color:var(--muted);font-size:11px;font-weight:700;cursor:pointer;border-radius:0;font-family:inherit}
.mrg-ab-tab:first-child{border-radius:var(--rads) 0 0 var(--rads)}
.mrg-ab-tab:last-child{border-radius:0 var(--rads) var(--rads) 0;border-left:none}
.mrg-ab-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.mrg-resolve-bar{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.mrg-resolve-bar button{flex:1;min-width:100px;padding:8px 8px;font-size:11.5px;font-weight:700;border-radius:var(--rads);cursor:pointer;font-family:inherit;border:1px solid var(--border)}
.mrg-override-panel{background:#0b1220;border:1px solid rgba(251,191,36,.3);border-radius:var(--rads);padding:12px;margin-top:8px;display:none}
.mrg-override-panel.open{display:block}
.diff-table{width:100%;border-collapse:collapse;font-size:11.5px;margin-top:4px}
.diff-table th{padding:5px 8px;background:var(--bg);color:var(--muted);text-align:left;font-size:10px;text-transform:uppercase;border-bottom:1px solid var(--border)}
.diff-table td{padding:5px 8px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:top;max-width:160px;word-break:break-word}
.diff-table tr.diff-row td:nth-child(1){color:var(--muted)}
.diff-table tr.diff-row td:nth-child(2){color:#fde68a;background:rgba(251,191,36,.06)}
.diff-table tr.diff-row td:nth-child(3){color:#93c5fd;background:rgba(147,197,253,.06)}
/* Past Recces tab filter */
.recce-filter-bar{display:flex;flex-direction:column;gap:5px;margin-bottom:10px;padding:8px 10px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads)}
.recce-filter-chips{display:flex;gap:5px;overflow-x:auto;padding-bottom:2px;flex-wrap:wrap}
.recce-date-row{display:flex;gap:6px;align-items:center}
.recce-date-row input[type=date]{background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:11.5px;padding:3px 7px;flex:1;min-width:0}
.recce-date-row label{font-size:11px;color:var(--muted);white-space:nowrap}
/* Phase 4.5 — Clients admin */
.cli-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:11px 13px;margin-bottom:8px}
.cli-card.expanded{border-color:var(--accent)}
.cli-top{display:flex;align-items:center;gap:8px;cursor:pointer}
.cli-name{flex:1;font-size:14px;font-weight:700;min-width:0}
.cli-name .name{display:block}
.cli-name .meta{display:flex;gap:6px;flex-wrap:wrap;margin-top:3px}
.cli-meta-chip{font-size:10px;font-weight:700;background:rgba(236,72,153,.16);color:#f9a8d4;padding:1px 7px;border-radius:9px}
.cli-meta-chip.users{background:rgba(34,197,94,.18);color:#86efac}
.cli-meta-chip.pending{background:rgba(234,179,8,.18);color:#fde68a}
.cli-meta-chip.inactive{background:rgba(107,114,128,.2);color:#9ca3af}
.cli-body{margin-top:10px;display:none}
.cli-card.expanded .cli-body{display:block}
.cli-section-title{font-size:10.5px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.3px;margin:8px 0 4px}
.cli-user{background:#0b1220;border:1px solid var(--border);border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;align-items:center;gap:8px}
.cli-user.revoked{opacity:.55}
.cli-user .u-main{flex:1;min-width:0}
.cli-user .u-email{font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cli-user .u-meta{font-size:10.5px;color:var(--muted);margin-top:1px;display:flex;gap:6px;flex-wrap:wrap}
.cli-user .role{font-size:9.5px;font-weight:800;padding:1px 6px;border-radius:7px;background:rgba(236,72,153,.18);color:#f9a8d4;text-transform:uppercase;letter-spacing:.3px}
.cli-user .scope{font-size:9.5px;font-weight:700;padding:1px 6px;border-radius:7px;background:rgba(125,211,252,.16);color:#7dd3fc;text-transform:uppercase;letter-spacing:.3px}
.cli-user .actions{display:flex;gap:4px;flex-shrink:0}
.cli-user .actions button{background:rgba(255,255,255,.05);border:1px solid var(--border);font-size:10.5px;font-weight:700;padding:4px 8px;border-radius:5px;cursor:pointer}
.cli-user .actions .danger{background:rgba(239,68,68,.16);color:#fca5a5;border-color:rgba(239,68,68,.3)}
.cli-invite{background:rgba(234,179,8,.08);border:1px dashed rgba(234,179,8,.4);border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;align-items:center;gap:8px;font-size:11.5px;color:#fde68a}
.cli-invite .actions{margin-left:auto}
.cli-invite .actions button{background:rgba(239,68,68,.15);color:#fca5a5;border:1px solid rgba(239,68,68,.3);font-size:10.5px;font-weight:700;padding:4px 8px;border-radius:5px;cursor:pointer}
.cli-row-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}
.cli-row-actions button{font-size:12px;font-weight:700;padding:9px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer}
.cli-row-actions .invite{background:var(--accent);border-color:var(--accent);color:#fff}·
/* Phase 4.6 — Notifications */
.notify-tabs{display:flex;gap:6px;margin:8px 0 10px}
.notify-tabs button{flex:1;background:var(--card);border:1px solid var(--border);color:var(--muted);font-size:12px;font-weight:700;padding:7px;border-radius:7px;cursor:pointer}
.notify-tabs button.on{background:var(--accent);border-color:var(--accent);color:#fff}
.notify-card{background:var(--card);border:1px solid var(--border);border-left:3px solid var(--muted);border-radius:var(--rad);padding:10px 12px;margin-bottom:8px;font-size:12.5px}
.notify-card.unseen{border-left-color:var(--accent);background:rgba(124,58,237,.06)}
.notify-card .nf-top{display:flex;align-items:center;gap:7px;margin-bottom:4px}
.notify-card .nf-icon{font-size:15px}
.notify-card .nf-title{flex:1;font-weight:800;min-width:0}
.notify-card .nf-when{font-size:10.5px;color:var(--muted);white-space:nowrap}
.notify-card .nf-body{font-size:11.5px;color:var(--text);margin:2px 0 6px;line-height:1.4;word-break:break-word}
.notify-card .nf-chips{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:5px}
.notify-card .nf-chip{font-size:9.5px;font-weight:700;padding:1px 6px;border-radius:7px;background:rgba(125,211,252,.16);color:#7dd3fc;text-transform:uppercase;letter-spacing:.3px}
.notify-card .nf-chip.evt-official{background:rgba(34,197,94,.18);color:#86efac}
.notify-card .nf-chip.evt-response{background:rgba(236,72,153,.16);color:#f9a8d4}
.notify-card .nf-chip.status-suppressed{background:rgba(107,114,128,.2);color:#9ca3af}
.notify-card .nf-actions{display:flex;justify-content:flex-end;gap:6px}
.notify-card .nf-actions button{background:rgba(255,255,255,.05);border:1px solid var(--border);font-size:10.5px;font-weight:700;padding:4px 10px;border-radius:5px;cursor:pointer;color:var(--text)}
.notify-card .nf-actions .seen{background:var(--accent);border-color:var(--accent);color:#fff}·
/* Phase 4.7 — Audit log */
.audit-filters{display:flex;gap:6px;margin:8px 0 10px;flex-wrap:wrap}
.audit-filters select,.audit-filters input{flex:1;min-width:120px;background:var(--card);border:1px solid var(--border);color:var(--text);font-size:12px;padding:7px 9px;border-radius:7px}
.audit-row{background:var(--card);border:1px solid var(--border);border-radius:var(--rad);padding:9px 12px;margin-bottom:6px;font-size:12px}
.audit-row .au-top{display:flex;align-items:center;gap:7px;margin-bottom:3px}
.audit-row .au-icon{font-size:14px}
.audit-row .au-action{font-weight:800;text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;color:var(--accent)}
.audit-row .au-action.login{color:#7dd3fc}
.audit-row .au-action.view{color:#86efac}
.audit-row .au-action.response{color:#f9a8d4}
.audit-row .au-when{font-size:10.5px;color:var(--muted);white-space:nowrap;margin-left:auto}
.audit-row .au-user{font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis}
.audit-row .au-body{font-size:11.5px;color:var(--muted);margin-top:3px;line-height:1.4;word-break:break-word}
.audit-row .au-body b{color:var(--text)}
.audit-row .au-meta{font-size:10px;color:var(--muted);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap}
.au-risk{font-size:9.5px;font-weight:800;padding:1px 6px;border-radius:7px;text-transform:uppercase;letter-spacing:.3px;margin-left:6px}
.au-risk.low{background:rgba(34,197,94,.16);color:#86efac}
.au-risk.medium{background:rgba(234,179,8,.18);color:#fde68a}
.au-risk.high{background:rgba(239,68,68,.2);color:#fca5a5}·
/* Phase 4.14 — stats cards */
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.stat-card{background:var(--card2);border:1px solid var(--border);border-radius:8px;padding:10px 12px}
.stat-card.full{grid-column:1 / -1}
.stat-card h4{font-size:10.5px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.3px;margin:0 0 6px}
.stat-card .big{font-size:22px;font-weight:800;line-height:1}
.stat-card .sub{font-size:10.5px;color:var(--muted);margin-top:3px}
.bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:#0b1220;margin-top:6px}
.bar > div{height:100%}
.bar .low{background:#22c55e}
.bar .medium{background:#eab308}
.bar .high{background:#ef4444}
.bar .approved{background:#22c55e}
.bar .approved_with_changes{background:#eab308}
.bar .rejected{background:#ef4444}
.bar .pending{background:#6b7280}
.legend{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;font-size:10.5px;color:var(--muted)}
.legend span{display:inline-flex;align-items:center;gap:4px}
.legend span::before{content:'';width:8px;height:8px;border-radius:2px;display:inline-block;background:currentColor;opacity:.5}
.top-user{display:flex;align-items:center;gap:8px;padding:5px 0;border-top:1px solid var(--border);font-size:11.5px}
.top-user:first-child{border-top:none}
.top-user .tu-email{flex:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.top-user .tu-count{font-size:10.5px;color:var(--muted)}·
/* Phase 4.17 — supervisor comment thread */
.cmt-msg{display:flex;flex-direction:column;gap:3px;padding:8px 10px;border-radius:8px;font-size:12px;max-width:88%}
.cmt-msg.client_user{background:rgba(124,58,237,.10);border:1px solid rgba(124,58,237,.3);align-self:flex-start}
.cmt-msg.supervisor{background:rgba(34,197,94,.10);border:1px solid rgba(34,197,94,.3);align-self:flex-end}
.cmt-msg .ch{font-size:10px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.3px;display:flex;justify-content:space-between;gap:8px}
.cmt-msg .cb{font-size:12.5px;color:var(--text);white-space:pre-wrap;word-break:break-word}
</style>
</head>
<body>·
<!-- ── SCREEN: LIST ── -->
<div id=\"s-list\" class=\"scr\">
  <div class=\"upd-banner\" id=\"upd-banner\" onclick=\"reload()\">↻ New version available — tap to reload</div>
  <div class=\"alloc-warn-banner\" id=\"alloc-warn-banner\">
    <span id=\"alloc-warn-text\">⚠️ Inactive employees have allocated counters</span>
    <div style=\"display:flex;gap:6px\">
      <button onclick=\"filterByInactiveAlloc()\">View Counters</button>
      <button onclick=\"document.getElementById('alloc-warn-banner').classList.remove('show')\" style=\"background:none;border:none;color:#fed7aa;font-size:16px;cursor:pointer;padding:0 2px\">×</button>
    </div>
  </div>
  <div class=\"hdr\">
    <h1>🏪 Counters</h1>
    <span class=\"user-pill\" id=\"user-pill\"></span>
    <button class=\"hdr-btn\" onclick=\"window.location.href='/hub/'\">🏡 Hub</button>
    <button class=\"hdr-btn\" onclick=\"window.location.href='/learn/?pwa=counters'\" title=\"Tutorials\">🎓 Tutorials</button>
    <button class=\"hdr-btn\" onclick=\"logout()\" title=\"Log Out\">🔒</button>
    <button class=\"hdr-btn\" id=\"btn-coverage\"   onclick=\"openCoverageReport()\" style=\"display:none\">📊 Coverage</button>
    <button class=\"hdr-btn\" id=\"btn-notify\"     onclick=\"openNotify()\"     style=\"display:none\">🔔 <span id=\"notify-badge\">Notify</span></button>
    <button class=\"hdr-btn\" id=\"btn-audit\"      onclick=\"openAudit()\"      style=\"display:none\">📋 <span id=\"audit-badge\">Audit</span></button>
    <button class=\"hdr-btn\" id=\"btn-clients\"    onclick=\"openClients()\"    style=\"display:none\">🏷 <span id=\"clients-badge\">Clients</span></button>
    <button class=\"hdr-btn\" id=\"btn-multirecce\" onclick=\"openMultiRecce()\" style=\"display:none\">📌 <span id=\"multirecce-badge\">Multi-Recce</span></button>
    <button class=\"hdr-btn\" id=\"btn-uploads\"    onclick=\"openUploads()\"    style=\"display:none\">📥 <span id=\"upload-badge\">Uploads</span></button>
    <button class=\"hdr-btn\" id=\"btn-sync-list\" onclick=\"loadList()\" title=\"Refresh counter list\">↻</button>
    <button class=\"hdr-btn\" id=\"btn-tour\" onclick=\"window._tour&&window._tour.show()\" title=\"Supervisor guide\">? Help</button>
    <button class=\"hdr-btn\" id=\"btn-bulk\"       onclick=\"openBulkEdit()\"  style=\"display:none\">⚡ Bulk</button>
    <button class=\"hdr-btn\" id=\"btn-geofill\" onclick=\"openGeoFill()\" style=\"display:none\" title=\"AI suggests state_code and city_tier from city/state names\">✨ Geo-fill</button>
    <button class=\"hdr-btn p\" id=\"btn-new\"       onclick=\"openNew()\"       style=\"display:none\">+ New</button>
  </div>
  <div class=\"body\">
    <div class=\"tiles\">
      <div class=\"tile\" data-filter=\"all\"        onclick=\"setTile(event,'all')\">       <div class=\"tile-lbl\">All</div>          <div class=\"tile-val\" id=\"t-all\">—</div></div>
      <div class=\"tile\" data-filter=\"active\"     onclick=\"setTile(event,'active')\">    <div class=\"tile-lbl\">Active</div>       <div class=\"tile-val\" id=\"t-active\">—</div></div>
      <div class=\"tile\" data-filter=\"pending\"    onclick=\"setTile(event,'pending')\">   <div class=\"tile-lbl\">Pending</div>      <div class=\"tile-val\" id=\"t-pending\">—</div></div>
      <div class=\"tile\" data-filter=\"norecce\"    onclick=\"setTile(event,'norecce')\">   <div class=\"tile-lbl\">No Recce</div>     <div class=\"tile-val\" id=\"t-norecce\">—</div></div>
      <div class=\"tile\" data-filter=\"stale90\"    onclick=\"setTile(event,'stale90')\">   <div class=\"tile-lbl\">Stale 90d</div>    <div class=\"tile-val\" id=\"t-stale90\">—</div></div>
      <div class=\"tile\" data-filter=\"inactive\"   onclick=\"setTile(event,'inactive')\">  <div class=\"tile-lbl\">Closed</div>       <div class=\"tile-val\" id=\"t-inactive\">—</div></div>
    </div>
    <div class=\"search-wrap\">
      <span class=\"icon\">🔍</span>
      <input id=\"srch\" placeholder=\"Search name, GSTIN, city, pincode…\" oninput=\"renderList()\">
      <button onclick=\"loadList()\" title=\"Refresh list\" style=\"background:none;border:none;font-size:16px;cursor:pointer;padding:0 4px;color:var(--muted)\">↻</button>
    </div>
    <div class=\"sum-tiles\" id=\"list-sum-tiles\"></div>
    <div style=\"display:flex;gap:6px;flex-wrap:wrap;padding:2px 0 4px\">
      <select id=\"list-filter-brand\" onchange=\"renderList()\" style=\"font-size:11px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:'DM Sans',sans-serif;flex:1;min-width:0\">
        <option value=\"\">All Brands</option>
      </select>
      <select id=\"list-filter-territory\" onchange=\"renderList()\" style=\"font-size:11px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:'DM Sans',sans-serif;flex:1;min-width:0\">
        <option value=\"\">All Territories</option>
      </select>
      <select id=\"list-filter-rep\" onchange=\"onRepFilterChange()\" style=\"font-size:11px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:'DM Sans',sans-serif;flex:1;min-width:0;display:none\">
        <option value=\"\">All Reps</option>
      </select>
    </div>
    <div id=\"list-chip-rows\"></div>
    <div class=\"sec-label\">
      <span>Counters</span>
      <span style=\"display:flex;align-items:center;gap:8px\">
        <select id=\"list-sort\" onchange=\"renderList()\" style=\"font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:'DM Sans',sans-serif\">
          <option value=\"name\">A–Z</option>
          <option value=\"recce_desc\">Most Recces</option>
          <option value=\"recce_asc\">Least Recces</option>
          <option value=\"recent\">Recent Recce</option>
          <option value=\"stale\">Stale first</option>
        </select>
        <span id=\"list-count\" style=\"font-weight:600;color:var(--muted);text-transform:none;letter-spacing:0;font-size:11px\"></span>
        <button onclick=\"exportCounterCSV()\" title=\"Export filtered list to CSV\" style=\"background:none;border:none;font-size:13px;cursor:pointer;padding:0 2px;color:var(--muted)\" id=\"btn-export\">⬇ CSV</button>
      </span>
    </div>
    <div id=\"list-out\"></div>
  </div>
</div>·
<!-- ── SCREEN: DETAIL ── -->
<div id=\"s-detail\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1 id=\"d-title\">Counter</h1>
    <button class=\"hdr-btn\" id=\"btn-allocate\" onclick=\"openAllocate()\" style=\"display:none\">👥 Allocate</button>
    <button class=\"hdr-btn\" id=\"btn-edit\"     onclick=\"openEdit()\"     style=\"display:none\">✏ Edit</button>
  </div>
  <div class=\"body\" style=\"padding-bottom:88px\">
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Identity</div>
      <div id=\"d-identity\"></div>
    </div>
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Address</div>
      <div id=\"d-address\"></div>
    </div>
    <div class=\"form-card\" id=\"d-notes\" style=\"display:none\"></div>
    <div class=\"tabs\">
      <button class=\"sel\" data-tab=\"brands\"   onclick=\"setTab('brands')\">Brands</button>
      <button         data-tab=\"contacts\" onclick=\"setTab('contacts')\">Contacts</button>
      <button         data-tab=\"addrs\"    onclick=\"setTab('addrs')\">More Addresses</button>
      <button         data-tab=\"recces\"   onclick=\"setTab('recces')\">Past Recces</button>
    </div>
    <div id=\"d-brands\"></div>
    <div id=\"d-contacts\" style=\"display:none\"></div>
    <div id=\"d-addrs\" style=\"display:none\"></div>
    <div id=\"d-recces\" style=\"display:none\"></div>
  </div>
</div>·
<!-- ── SCREEN: NEW / EDIT ── -->
<div id=\"s-form\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"cancelForm()\">‹</button>
    <h1 id=\"f-title\">New Counter</h1>
  </div>
  <div class=\"body\" style=\"padding-bottom:88px\">
    <div class=\"dedup-panel\" id=\"dedup\">
      <div class=\"ttl\">⚠ Possible existing counter</div>
      <div id=\"dedup-list\"></div>
    </div>·
    <div class=\"form-card\">
      <div class=\"form-grid\">
        <div class=\"field full\">
          <label>Display name <span class=\"req\">*</span></label>
          <input id=\"f-display_name\" oninput=\"dedupCheck()\" maxlength=\"160\">
        </div>
        <div class=\"field\">
          <label>Legal name</label>
          <input id=\"f-legal_name\" maxlength=\"180\">
        </div>
        <div class=\"field\">
          <label>Proprietor</label>
          <input id=\"f-proprietor_name\" maxlength=\"120\">
        </div>
        <div class=\"field full\">
          <label>Aliases <span class=\"hint\">(comma-separated)</span></label>
          <input id=\"f-aliases\" placeholder=\"e.g. Saini Electronics, Saini Elect.\">
        </div>
      </div>
    </div>·
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Primary address</div>
      <div class=\"form-grid\">
        <div class=\"field full\">
          <label>Line 1 <span class=\"req\">*</span></label>
          <input id=\"f-address_line1\" oninput=\"dedupCheck()\" maxlength=\"240\">
        </div>
        <div class=\"field full\">
          <label>Line 2</label>
          <input id=\"f-address_line2\" maxlength=\"200\">
        </div>
        <div class=\"field\">
          <label>City</label>
          <input id=\"f-city\" oninput=\"dedupCheck()\" maxlength=\"80\">
        </div>
        <div class=\"field\">
          <label>State</label>
          <input id=\"f-state\" maxlength=\"60\">
        </div>
        <div class=\"field\">
          <label>State code <span class=\"hint\">(2-digit GST)</span></label>
          <input id=\"f-state_code\" maxlength=\"2\" pattern=\"[0-9]{2}\">
        </div>
        <div class=\"field\">
          <label>Pincode</label>
          <input id=\"f-pincode\" oninput=\"dedupCheck();pincodeAutoFill(this.value)\" maxlength=\"6\" pattern=\"[0-9]{6}\">
        </div>
        <div class=\"field\">
          <label>GPS lat</label>
          <input id=\"f-gps_lat\" type=\"number\" step=\"0.0000001\">
        </div>
        <div class=\"field\">
          <label>GPS lng</label>
          <input id=\"f-gps_lng\" type=\"number\" step=\"0.0000001\">
        </div>
        <div class=\"field full\">
          <button type=\"button\" onclick=\"fillGpsFromBrowser()\" style=\"font-size:12px;padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:var(--card);cursor:pointer;color:var(--accent)\">📍 Use my current location</button>
          <div id=\"gps-status\" style=\"font-size:11px;color:var(--muted);margin-top:4px\"></div>
        </div>
      </div>
    </div>·
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Tax / Legal</div>
      <div class=\"form-grid\">
        <div class=\"field\">
          <label>GSTIN</label>
          <input id=\"f-gstin\" maxlength=\"15\" oninput=\"upper(this);dedupCheck()\" placeholder=\"15 chars\">
        </div>
        <div class=\"field\">
          <label>PAN</label>
          <input id=\"f-pan\" maxlength=\"10\" oninput=\"upper(this)\">
        </div>
        <div class=\"field\">
          <label>FSSAI</label>
          <input id=\"f-fssai\" maxlength=\"20\">
        </div>
        <div class=\"field\">
          <label>Drug licence</label>
          <input id=\"f-drug_lic\" maxlength=\"40\">
        </div>
      </div>
    </div>·
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Commercial</div>
      <div class=\"form-grid\">
        <div class=\"field\">
          <label>Counter type</label>
          <select id=\"f-counter_type\">
            <option value=\"\"></option>
            <option value=\"sub_dealer\">Sub-dealer</option>
            <option value=\"dealer\">Dealer</option>
            <option value=\"distributor\">Distributor</option>
            <option value=\"modern_trade\">Modern trade</option>
            <option value=\"standalone\">Standalone</option>
            <option value=\"other\">Other</option>
          </select>
        </div>
        <div class=\"field\">
          <label>Channel</label>
          <select id=\"f-channel\">
            <option value=\"\"></option>
            <option value=\"general_trade\">General trade</option>
            <option value=\"modern_trade\">Modern trade</option>
            <option value=\"institution\">Institution</option>
            <option value=\"e_commerce\">E-commerce</option>
            <option value=\"other\">Other</option>
          </select>
        </div>
        <div class=\"field\">
          <label>Customer group</label>
          <input id=\"f-customer_group\" maxlength=\"60\">
        </div>
        <div class=\"field\">
          <label>Territory</label>
          <input id=\"f-territory\" maxlength=\"60\">
        </div>
        <div class=\"field\">
          <label>Geo Cluster <span style=\"color:var(--muted);font-weight:400;font-size:10px\">(tricity, ncr…)</span></label>
          <input id=\"f-geo_cluster\" maxlength=\"60\" placeholder=\"e.g. tricity, ncr, doaba\">
        </div>
        <div class=\"field\">
          <label>City Tier</label>
          <select id=\"f-city_tier\">
            <option value=\"\"></option>
            <option value=\"tier1\">Tier 1 (Metro)</option>
            <option value=\"tier2\">Tier 2</option>
            <option value=\"tier3\">Tier 3</option>
            <option value=\"tier4\">Tier 4 (Rural)</option>
          </select>
        </div>
        <div class=\"field\">
          <label>Store Type</label>
          <select id=\"f-store_type\">
            <option value=\"\"></option>
            <option value=\"les\">LES – Large Electronics Store</option>
            <option value=\"ses\">SES – Small Electronics Store</option>
            <option value=\"mbo\">MBO – Multi-Brand Outlet</option>
            <option value=\"ebo\">EBO – Exclusive Brand Outlet</option>
            <option value=\"modern_trade\">Modern Trade</option>
            <option value=\"other\">Other</option>
          </select>
        </div>
        <div class=\"field\">
          <label>Distributor name</label>
          <input id=\"f-distributor_name\" maxlength=\"120\">
        </div>
        <div class=\"field\">
          <label>Status</label>
          <select id=\"f-status\">
            <option value=\"active\">Active</option>
            <option value=\"inactive\">Inactive</option>
            <option value=\"blacklisted\">Blacklisted</option>
            <option value=\"pending_review\">Pending review</option>
          </select>
        </div>
        <div class=\"field full\">
          <label>Brands</label>
          <div class=\"bcset\" id=\"bcset\" onclick=\"document.getElementById('brand-inp').focus()\">
            <input id=\"brand-inp\" placeholder=\"Type brand + Enter\" onkeydown=\"brandKey(event)\">
          </div>
        </div>
      </div>
    </div>·
    <div class=\"form-card\">
      <div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700\">Notes</div>
      <div class=\"field\">
        <textarea id=\"f-notes\" maxlength=\"2000\"></textarea>
      </div>
    </div>·
    <div class=\"btn-row\">
      <button class=\"btn btn-s\" onclick=\"cancelForm()\">Cancel</button>
      <button class=\"btn btn-p\" id=\"btn-save\" onclick=\"saveForm()\">Save</button>
    </div>
  </div>
</div>·
<!-- ── SCREEN: CLIENTS ADMIN (supervisor only) ── -->
<div id=\"s-clients\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1>🏷 Clients</h1>
    <button class=\"hdr-btn\" onclick=\"loadClients()\">↻</button>
    <button class=\"hdr-btn p\" onclick=\"openNewClient()\">+ Client</button>
  </div>
  <div class=\"body\">
    <div class=\"tiles\" id=\"cli-tiles\">
      <div class=\"tile sel\" data-cfilter=\"all\"      onclick=\"setCFilter(event,'all')\">      <div class=\"tile-lbl\">All</div>       <div class=\"tile-val\" id=\"cf-all\">—</div></div>
      <div class=\"tile\"     data-cfilter=\"active\"   onclick=\"setCFilter(event,'active')\">   <div class=\"tile-lbl\">Active</div>    <div class=\"tile-val\" id=\"cf-active\">—</div></div>
      <div class=\"tile\"     data-cfilter=\"inactive\" onclick=\"setCFilter(event,'inactive')\"> <div class=\"tile-lbl\">Inactive</div>  <div class=\"tile-val\" id=\"cf-inactive\">—</div></div>
      <div class=\"tile\"     data-cfilter=\"users\"    onclick=\"setCFilter(event,'users')\">    <div class=\"tile-lbl\">With users</div><div class=\"tile-val\" id=\"cf-users\">—</div></div>
    </div>
    <div class=\"search-wrap\">
      <span class=\"icon\">🔍</span>
      <input id=\"cli-srch\" placeholder=\"Search name or brand code…\" oninput=\"renderClients()\">
    </div>
    <div class=\"sec-label\">
      <span>Brand-side clients</span>
      <span id=\"clients-count\" style=\"font-weight:600;color:var(--muted);text-transform:none;letter-spacing:0;font-size:11px\"></span>
    </div>
    <div style=\"font-size:11.5px;color:var(--muted);padding:8px 11px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);margin-bottom:6px\">
      Each client is a brand entity. Tap to expand and manage their users, invitations, role + scope. Users sign in at <span style=\"color:var(--accent)\">/recce-client/</span>.
    </div>
    <div id=\"clients-out\"></div>
  </div>
</div>·
<!-- ── SCREEN: NOTIFICATIONS (supervisor only) ── -->
<div id=\"s-notify\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1>🔔 Notifications</h1>
    <button class=\"hdr-btn\" onclick=\"loadNotify()\">↻</button>
    <button class=\"hdr-btn\" id=\"btn-mark-all-seen\" onclick=\"markAllNotifySeen()\">Mark all seen</button>
  </div>
  <div class=\"body\">
    <div class=\"notify-tabs\">
      <button id=\"nf-tab-unseen\" class=\"on\" onclick=\"setNotifyTab('unseen')\">Unseen</button>
      <button id=\"nf-tab-all\"           onclick=\"setNotifyTab('all')\">All</button>
    </div>
    <div style=\"font-size:11.5px;color:var(--muted);padding:8px 11px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);margin-bottom:8px\">
      Events that warrant a supervisor's attention: Recces flipped to <span style=\"color:#86efac\">Official</span>, client <span style=\"color:#f9a8d4\">Approved / Changes-requested</span> responses, and other internal alerts. Emails to client_users are dispatched automatically.
    </div>
    <div id=\"notify-out\"></div>
  </div>
</div>·
<!-- ── SCREEN: AUDIT LOG (supervisor only) ── -->
<div id=\"s-audit\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1>📋 Audit log</h1>
    <button class=\"hdr-btn\" onclick=\"loadAudit()\">↻</button>
    <button class=\"hdr-btn\" onclick=\"openAuditStats()\">📊 Stats</button>
  </div>
  <div class=\"body\">
    <div class=\"audit-filters\">
      <select id=\"audit-action\" onchange=\"loadAudit()\">
        <option value=\"\">All actions</option>
        <option value=\"login\">Logins</option>
        <option value=\"recce_view\">Recce views</option>
        <option value=\"response_submit\">Responses</option>
        <option value=\"session_revoke\">Session revokes</option>
      </select>
      <select id=\"audit-risk\" onchange=\"renderAudit()\">
        <option value=\"\">All risk levels</option>
        <option value=\"high\">High risk only</option>
        <option value=\"medium\">Medium+ risk</option>
      </select>
      <input id=\"audit-user\" placeholder=\"client_user_id (optional)\" oninput=\"loadAuditDebounced()\">
    </div>
    <div class=\"audit-filters\">
      <input id=\"audit-from\" type=\"date\" title=\"From date\" onchange=\"loadAudit()\">
      <input id=\"audit-to\"   type=\"date\" title=\"To date (exclusive)\" onchange=\"loadAudit()\">
      <button class=\"hdr-btn\" onclick=\"exportAuditCsv()\">⤓ Export CSV</button>
      <button class=\"hdr-btn\" onclick=\"pruneAuditNow()\">🧹 Prune &gt; 180d</button>
    </div>
    <div style=\"font-size:11.5px;color:var(--muted);padding:8px 11px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);margin-bottom:8px\">
      Every client_user action on the Recce Portal — magic-link logins, Recce viewer opens, accept / reject / approved_with_changes responses. IP + UA captured for forensics.<br>
      <span style=\"color:#fde68a\">Retention: rows older than 180 days are auto-pruned nightly by the mail relay worker.</span>
    </div>
    <div id=\"audit-out\"></div>
  </div>
</div>·
<!-- ── MODAL: NEW CLIENT ── -->
<div class=\"modal\" id=\"modal-newclient\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">🏷 New client</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-newclient')\">×</button>
    </div>
    <div class=\"form-grid\">
      <div class=\"field full\">
        <label>Client name <span class=\"req\">*</span></label>
        <input id=\"ncf-name\" placeholder=\"e.g. HP India\" maxlength=\"120\">
      </div>
      <div class=\"field full\">
        <label>Brand codes <span class=\"hint\">(comma-separated; these gate Recce visibility)</span></label>
        <input id=\"ncf-brand_codes\" placeholder=\"HP, HP_INK\" maxlength=\"240\">
      </div>
      <div class=\"field\">
        <label>Contact name</label>
        <input id=\"ncf-contact_name\" maxlength=\"120\">
      </div>
      <div class=\"field\">
        <label>Contact email</label>
        <input id=\"ncf-contact_email\" type=\"email\" maxlength=\"160\">
      </div>
      <div class=\"field full\">
        <label>Notes</label>
        <textarea id=\"ncf-notes\" maxlength=\"2000\"></textarea>
      </div>
    </div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-newclient')\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"saveNewClient()\">Create client</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: INVITE USER ── -->
<div class=\"modal\" id=\"modal-invite\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"invite-ttl\">✉ Invite user</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-invite')\">×</button>
    </div>
    <div class=\"form-grid\">
      <div class=\"field full\">
        <label>Email <span class=\"req\">*</span></label>
        <input id=\"invf-email\" type=\"email\" placeholder=\"user@brand.com\" maxlength=\"160\">
      </div>
      <div class=\"field full\">
        <label>Display name</label>
        <input id=\"invf-display_name\" placeholder=\"Optional; will use email if blank\" maxlength=\"120\">
      </div>
      <div class=\"field\">
        <label>Role <span class=\"req\">*</span></label>
        <select id=\"invf-role\">
          <option value=\"Viewer\">Viewer</option>
          <option value=\"SM\">SM</option>
          <option value=\"ASM\">ASM</option>
          <option value=\"Marketing\">Marketing</option>
          <option value=\"Admin\">Admin</option>
        </select>
      </div>
      <div class=\"field\">
        <label>Scope</label>
        <select id=\"invf-scope_kind\">
          <option value=\"all\">All client's Recces</option>
          <option value=\"territory\">By territory</option>
          <option value=\"job_id\">By job_id</option>
          <option value=\"recce_id\">By recce_id</option>
          <option value=\"brand_subset\">Brand subset</option>
        </select>
      </div>
      <div class=\"field full\">
        <label>Scope values <span class=\"hint\">(comma-separated; ignored when scope = \"All\")</span></label>
        <input id=\"invf-scope_values\" placeholder=\"e.g. NORTH, WEST or JOB-100, JOB-101\">
      </div>
    </div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-invite')\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"saveInvite()\">Send invitation</button>
    </div>
    <div style=\"margin-top:10px;font-size:11px;color:var(--muted);text-align:center\">
      A magic-link email will be queued. Worker delivers within 30s.
    </div>
  </div>
</div>·
<!-- ── MODAL: RECCE COMMENTS (Phase 4.17) ── -->
<div class=\"modal\" id=\"modal-comments\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">💬 Recce comments · <span id=\"comments-sub-label\"></span></div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-comments')\">×</button>
    </div>
    <div id=\"comments-out\" style=\"display:flex;flex-direction:column;gap:8px;max-height:50vh;overflow-y:auto;padding:4px 0\"></div>
    <div style=\"display:flex;flex-direction:column;gap:6px;margin-top:12px\">
      <textarea id=\"comment-body\" maxlength=\"4000\" placeholder=\"Type a reply to send to the client…\" style=\"width:100%;background:#0b1220;border:1px solid var(--border);color:var(--text);font-size:12.5px;padding:8px 10px;border-radius:8px;min-height:60px;font-family:inherit\"></textarea>
      <div class=\"btn-row\">
        <button class=\"btn btn-s\" onclick=\"closeModal('modal-comments')\">Close</button>
        <button class=\"btn btn-p\" id=\"comment-post\" onclick=\"postSupervisorComment()\">Post reply</button>
      </div>
    </div>
  </div>
</div>·
<!-- ── MODAL: AUDIT STATS (supervisor only, Phase 4.14) ── -->
<div class=\"modal\" id=\"modal-stats\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">📊 Audit stats · <span id=\"stats-window\">last 7 days</span></div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-stats')\">×</button>
    </div>
    <div id=\"stats-out\"><div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div></div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-stats')\">Close</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: ALLOCATION COVERAGE REPORT ── -->
<div class=\"modal\" id=\"modal-coverage\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">📊 Allocation Coverage</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-coverage')\">×</button>
    </div>
    <div id=\"coverage-out\" style=\"padding:0 2px;max-height:65vh;overflow-y:auto\"><div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div></div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-coverage')\">Close</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: BULK EDIT COUNTERS (supervisor only) ── -->
<div class=\"modal\" id=\"modal-bulk-edit\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">⚡ Bulk Edit Counters</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-bulk-edit')\">×</button>
    </div>
    <div style=\"font-size:12px;color:var(--muted);margin-bottom:12px\">
      Apply a change to all counters matching the <b>current list filter</b>. Only blank fields are filled; existing values are never overwritten — except brand codes which are merged additively.
    </div>
    <div id=\"bulk-scope-line\" style=\"font-size:12px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 12px;margin-bottom:14px;color:var(--muted)\">
      Scope: <b id=\"bulk-scope-count\">—</b> counters selected by current filter
    </div>
    <div class=\"form-grid\" style=\"gap:10px\">
      <div class=\"field\">
        <label>Brand Code to Add</label>
        <input id=\"bulk-brand\" placeholder=\"e.g. HP\" maxlength=\"40\" oninput=\"this.value=this.value.toUpperCase().replace(/[^A-Z0-9_]/g,'')\">
        <div style=\"font-size:10.5px;color:var(--muted);margin-top:2px\">Merged with existing brands — never removes existing codes</div>
      </div>
      <div class=\"field\">
        <label>Counter Type</label>
        <select id=\"bulk-counter-type\">
          <option value=\"\">— don't change —</option>
          <option value=\"sub_dealer\">Sub-dealer</option>
          <option value=\"dealer\">Dealer</option>
          <option value=\"distributor\">Distributor</option>
          <option value=\"modern_trade\">Modern trade</option>
          <option value=\"standalone\">Standalone</option>
          <option value=\"other\">Other</option>
        </select>
        <div style=\"font-size:10.5px;color:var(--muted);margin-top:2px\">Only applied to counters with no counter type set</div>
      </div>
      <div class=\"field\">
        <label>Territory</label>
        <input id=\"bulk-territory\" placeholder=\"e.g. North-HP\" maxlength=\"80\">
        <div style=\"font-size:10.5px;color:var(--muted);margin-top:2px\">Only applied to counters with no territory set</div>
      </div>
      <div class=\"field\">
        <label>Channel</label>
        <select id=\"bulk-channel\">
          <option value=\"\">— don't change —</option>
          <option value=\"general_trade\">general_trade</option>
          <option value=\"modern_trade\">modern_trade</option>
          <option value=\"institution\">institution</option>
          <option value=\"e_commerce\">e_commerce</option>
          <option value=\"other\">other</option>
        </select>
        <div style=\"font-size:10.5px;color:var(--muted);margin-top:2px\">Only applied to counters with no channel set</div>
      </div>
    </div>
    <div id=\"bulk-status\" style=\"font-size:12px;min-height:18px;margin:10px 0;color:var(--muted)\"></div>
    <div class=\"btn-row\" style=\"margin-top:4px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-bulk-edit')\">Cancel</button>
      <button class=\"btn btn-p\" id=\"btn-bulk-apply\" onclick=\"runBulkEdit()\">Apply to <span id=\"bulk-apply-count\">?</span> counters</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: USER SESSIONS (supervisor only) ── -->
<div class=\"modal\" id=\"modal-sessions\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"sess-ttl\">🔑 Sessions</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-sessions')\">×</button>
    </div>
    <div id=\"sess-modal-out\"></div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-sessions')\">Close</button>
    </div>
  </div>
</div>·
<!-- ── SCREEN: MULTI-RECCE REVIEW (supervisor only) ── -->
<div id=\"s-multirecce\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1>📌 Multi-Recce Review</h1>
    <button class=\"hdr-btn\" onclick=\"loadMultiRecce()\">↻</button>
  </div>
  <div class=\"body\">
    <div class=\"sec-label\">
      <span>Counters with multiple Recces</span>
      <span id=\"mrg-count\" style=\"font-weight:600;color:var(--muted);text-transform:none;letter-spacing:0;font-size:11px\"></span>
    </div>
    <div style=\"font-size:11.5px;color:var(--muted);padding:8px 11px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);margin-bottom:6px\">
      Each group has 2+ Recces for the same counter + campaign (usually due to reassignment).
      Compare A vs B field-by-field, pick one as official, or use Supervisor Override to record
      the reason for your decision. The unchosen Recce is marked superseded. Tap \"View Counter →\"
      to see the full Recce history for any counter across all campaigns.
    </div>
    <div id=\"mrg-out\"></div>
  </div>
</div>·
<!-- ── SCREEN: UPLOADS QUEUE (supervisor only) ── -->
<div id=\"s-uploads\" class=\"scr\">
  <div class=\"hdr\">
    <button class=\"back-btn\" onclick=\"showScreen('s-list')\">‹</button>
    <h1>📥 Counter Uploads</h1>
    <button class=\"hdr-btn\" onclick=\"downloadUploadTemplate()\" title=\"Download CSV template with all fields\">⬇ Template</button>
    <button class=\"hdr-btn p\" onclick=\"document.getElementById('upload-file-input').click()\" title=\"Upload CSV file\">⬆ Upload CSV</button>
    <input type=\"file\" id=\"upload-file-input\" accept=\".csv\" style=\"display:none\" onchange=\"handleUploadFileSelect(this)\">
    <button class=\"hdr-btn\" id=\"btn-ai-import\" onclick=\"openAiImport()\" title=\"Paste any store list — AI converts it to upload format\" style=\"display:none\">🤖 AI Import</button>
    <button class=\"hdr-btn\" onclick=\"loadUploads()\">↻</button>
  </div>
  <div class=\"body\">
    <div class=\"sec-label\">
      <span>Awaiting supervisor review</span>
      <span id=\"uploads-count\" style=\"font-weight:600;color:var(--muted);text-transform:none;letter-spacing:0;font-size:11px\"></span>
    </div>
    <div id=\"uploads-out\"></div>
  </div>
</div>·
<!-- ── MODAL: CSV UPLOAD ── -->
<div class=\"modal\" id=\"modal-csvup\">
  <div class=\"modal-box\" style=\"max-width:420px\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">⬆ Upload Counter List</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-csvup')\">×</button>
    </div>
    <div class=\"field\">
      <label>List Name <span class=\"req\">*</span></label>
      <input id=\"csvup-name\" placeholder=\"e.g. HP Punjab Q3 2026\" maxlength=\"120\">
    </div>
    <div class=\"field\">
      <label>Job ID <span class=\"hint\">(optional — links upload to a specific campaign)</span></label>
      <input id=\"csvup-job\" placeholder=\"leave blank for generic upload\" maxlength=\"40\">
    </div>
    <div id=\"csvup-preview\" style=\"font-size:12px;color:var(--muted);margin:8px 0;line-height:1.5\"></div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-csvup')\">Cancel</button>
      <button class=\"btn btn-p\" id=\"csvup-submit\" onclick=\"submitUploadRows()\">Submit for Review</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: AI IMPORT ── -->
<div class=\"modal\" id=\"modal-ai-import\">
  <div class=\"modal-box\" style=\"max-width:580px;max-height:92vh;display:flex;flex-direction:column\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"ai-import-ttl\">🤖 AI Store Import</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-ai-import')\">×</button>
    </div>·
    <!-- Phase 1+2: paste + AI convert (existing) -->
    <div id=\"enrich-p12\" style=\"display:flex;flex-direction:column;flex:1;overflow:hidden\">
      <div style=\"font-size:12px;color:var(--muted);margin-bottom:10px;line-height:1.5\">
        Paste any raw store list — WhatsApp forward, typed list, Excel copy-paste, anything. AI converts it, then matches to existing counters and enriches geo fields.
      </div>
      <div class=\"field\" style=\"flex:1\">
        <label>Raw Store List <span class=\"req\">*</span></label>
        <textarea id=\"ai-raw-input\" rows=\"8\" placeholder=\"Paste your store list here…
Example:
Ganesh Medical Store
Near Bus Stand, Pune 411001
Owner: Ramesh Patil, 9876543210·
Sharma Electronics, 45 MG Road, Bangalore 560001
Contact: Anita 9988776655\" style=\"width:100%;resize:vertical;font-size:12px;font-family:monospace;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;box-sizing:border-box\" oninput=\"aiDetectStores(this.value)\"></textarea>
      </div>
      <div id=\"ai-detect-hint\" style=\"font-size:11px;color:var(--muted);text-align:right;margin-top:2px;min-height:14px\"></div>
      <div id=\"ai-import-status\" style=\"font-size:12px;color:var(--muted);margin:6px 0;min-height:18px\"></div>
      <div id=\"ai-import-preview\" style=\"display:none;margin-top:8px\">
        <div style=\"font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px\">
          Converted — <span id=\"ai-preview-count\"></span>
        </div>
        <div id=\"ai-preview-table\" style=\"background:#0b1220;border:1px solid var(--border);border-radius:6px;padding:8px;font-size:11px;max-height:160px;overflow-y:auto;font-family:monospace\"></div>
      </div>
      <div class=\"btn-row\" style=\"margin-top:14px\">
        <button class=\"btn btn-s\" onclick=\"closeModal('modal-ai-import')\">Cancel</button>
        <button class=\"btn btn-s\" id=\"btn-ai-convert\" onclick=\"runAiConvert()\">Convert with AI</button>
        <button class=\"btn btn-p\" id=\"btn-ai-use-rows\" onclick=\"startEnrichment()\" style=\"display:none\">Match &amp; Enrich →</button>
      </div>
    </div>·
    <!-- Phase 3: matching + geo enrichment in progress -->
    <div id=\"enrich-p3\" style=\"display:none;flex-direction:column;align-items:center;justify-content:center;padding:30px 10px;gap:10px\">
      <div class=\"spin\" style=\"font-size:28px\">⟳</div>
      <div id=\"enrich-p3-status\" style=\"font-size:13px;color:var(--muted);text-align:center\"></div>
    </div>·
    <!-- Phase 4: row-by-row guided review -->
    <div id=\"enrich-p4\" style=\"display:none;flex-direction:column;flex:1;overflow:hidden\">
      <div style=\"font-size:11px;color:var(--muted);margin-bottom:8px\">Review each store — accept enrichment, edit, or skip</div>
      <!-- Pip bar -->
      <div id=\"enrich-pips\" style=\"display:flex;gap:3px;flex-wrap:wrap;margin-bottom:10px\"></div>
      <div id=\"enrich-pip-label\" style=\"font-size:10px;color:var(--muted);margin-bottom:8px\"></div>
      <!-- Review card -->
      <div id=\"enrich-card\" style=\"background:var(--card2);border:1px solid var(--border);border-radius:8px;padding:12px;overflow-y:auto;flex:1\"></div>
      <!-- Decision buttons -->
      <div id=\"enrich-actions\" style=\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center\">
        <button class=\"btn btn-p\" id=\"btn-enrich-accept\" onclick=\"enrichDecide('accept')\" style=\"flex:1\">✓ Accept</button>
        <button class=\"btn btn-s\" id=\"btn-enrich-skip\"   onclick=\"enrichDecide('skip')\"   style=\"flex:1\">✗ Skip</button>
        <button class=\"btn btn-s\" id=\"btn-enrich-apply\"  onclick=\"applyEnrichment()\"      style=\"display:none;flex:2;font-weight:700\">Apply accepted changes</button>
      </div>
      <div id=\"enrich-apply-status\" style=\"font-size:11px;color:var(--muted);min-height:16px;margin-top:6px\"></div>
    </div>
  </div>
</div>·
<!-- ── MODAL: BULK REASSIGN (inactive employees) ── -->
<div class=\"modal\" id=\"modal-bulk-reassign\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">⇄ Bulk Reassign</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-bulk-reassign')\">×</button>
    </div>
    <div id=\"bulk-reassign-from\" style=\"font-size:13px;color:var(--muted);margin-bottom:8px\"></div>
    <div class=\"field\" style=\"margin-bottom:10px\">
      <label>Reassign to</label>
      <select id=\"bulk-reassign-to\" style=\"width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'DM Sans',sans-serif;font-size:14px\">
        <option value=\"\">— Select active employee —</option>
      </select>
    </div>
    <div id=\"bulk-reassign-summary\" style=\"font-size:12px;color:var(--muted);margin-bottom:12px\"></div>
    <div style=\"display:flex;gap:8px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-bulk-reassign')\" style=\"flex:1\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"confirmBulkReassign()\" style=\"flex:2;background:#ea580c\" id=\"btn-do-bulk-reassign\">⇄ Reassign All</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: ALLOCATE ── -->
<div class=\"modal\" id=\"modal-alloc\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"alloc-ttl\">👥 Allocate counter</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-alloc')\">×</button>
    </div>
    <div class=\"alloc-summary\" id=\"alloc-summary\">Loading employees…</div>
    <div class=\"field\" style=\"margin-bottom:10px\">
      <label>Optional Job ID <span class=\"hint\">(scope this allocation to one job)</span></label>
      <input id=\"alloc-job_id\" placeholder=\"leave blank for all jobs\" maxlength=\"40\">
    </div>
    <div class=\"alloc-tiles\" id=\"alloc-tiles\"></div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-alloc')\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"saveAlloc()\">Apply allocation</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: CONTACT ── -->
<div class=\"modal\" id=\"modal-contact\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"contact-ttl\">Add contact</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-contact')\">×</button>
    </div>
    <div class=\"form-grid\">
      <div class=\"field\">
        <label>Name <span class=\"req\">*</span></label>
        <input id=\"cf-name\" maxlength=\"120\">
      </div>
      <div class=\"field\">
        <label>Role</label>
        <select id=\"cf-role\">
          <option value=\"owner\">Owner</option>
          <option value=\"manager\">Manager</option>
          <option value=\"accountant\">Accountant</option>
          <option value=\"primary\">Primary</option>
          <option value=\"site\">Site</option>
          <option value=\"other\">Other</option>
        </select>
      </div>
      <div class=\"field\">
        <label>Designation</label>
        <input id=\"cf-designation\" maxlength=\"80\">
      </div>
      <div class=\"field\">
        <label>Phone</label>
        <input id=\"cf-phone\" maxlength=\"20\">
      </div>
      <div class=\"field\">
        <label>WhatsApp</label>
        <input id=\"cf-whatsapp\" maxlength=\"20\">
      </div>
      <div class=\"field\">
        <label>Email</label>
        <input id=\"cf-email\" maxlength=\"120\">
      </div>
      <div class=\"field full\">
        <label style=\"display:flex;align-items:center;gap:6px;cursor:pointer\">
          <input type=\"checkbox\" id=\"cf-is_primary\">
          <span>Mark as primary contact</span>
        </label>
      </div>
    </div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-contact')\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"saveContact()\">Save</button>
    </div>
  </div>
</div>·
<!-- ── MODAL: ADDRESS ── -->
<div class=\"modal\" id=\"modal-address\">
  <div class=\"modal-box\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\" id=\"address-ttl\">Add address</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-address')\">×</button>
    </div>
    <div class=\"form-grid\">
      <div class=\"field\">
        <label>Type</label>
        <select id=\"af-type\">
          <option value=\"billing\">Billing</option>
          <option value=\"shipping\">Shipping</option>
          <option value=\"site\">Site</option>
          <option value=\"warehouse\">Warehouse</option>
          <option value=\"other\">Other</option>
        </select>
      </div>
      <div class=\"field\">
        <label>Pincode</label>
        <input id=\"af-pincode\" maxlength=\"6\">
      </div>
      <div class=\"field full\">
        <label>Line 1 <span class=\"req\">*</span></label>
        <input id=\"af-line1\" maxlength=\"240\">
      </div>
      <div class=\"field full\">
        <label>Line 2</label>
        <input id=\"af-line2\" maxlength=\"200\">
      </div>
      <div class=\"field\">
        <label>City</label>
        <input id=\"af-city\" maxlength=\"80\">
      </div>
      <div class=\"field\">
        <label>State</label>
        <input id=\"af-state\" maxlength=\"60\">
      </div>
      <div class=\"field full\">
        <label style=\"display:flex;align-items:center;gap:6px;cursor:pointer\">
          <input type=\"checkbox\" id=\"af-is_default\">
          <span>Mark as default</span>
        </label>
      </div>
    </div>
    <div class=\"btn-row\" style=\"margin-top:14px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-address')\">Cancel</button>
      <button class=\"btn btn-p\" onclick=\"saveAddress()\">Save</button>
    </div>
  </div>
</div>·
<div id=\"toast\"></div>·
<script>
'use strict';·
const CACHE_VER = 'counters-v40';
const DB        = '/db';·
// ── Session + auth ────────────────────────────────────────
let session = null, myId = '', myName = '';
const SUPERVISORS = ['rakesh'];                     // role-derived
const ALWAYS_SUPERVISOR = ['harish', 'pramod'];     // hardcoded fallback per user note
function isSupervisor() { return SUPERVISORS.includes(myId) || ALWAYS_SUPERVISOR.includes(myId); }·
// ADR-112: always-visible Logout (was missing entirely — only Hub navigation existed)
function logout() { localStorage.removeItem('lm360-session'); window.location.href = '/hub/'; }·
function bootSession() {
  try { session = JSON.parse(localStorage.getItem('lm360-session') || 'null'); } catch (_) {}
  if (!session || !session.empId) {
    const next = encodeURIComponent(location.pathname + location.search);
    location.replace(`/hub/?next=${next}`);
    return false;
  }
  myId   = session.empId;
  myName = session.name || session.empId;
  document.getElementById('user-pill').innerHTML =
    `<b>${esc(myName)}</b>${isSupervisor() ? '<span class=\"role-tag\">SUPERVISOR</span>' : ''}`;
  // All users can access uploads (to submit CSVs); supervisors get extra tools
  document.getElementById('btn-uploads').style.display = '';
  if (isSupervisor()) {
    document.getElementById('btn-new').style.display = '';
    document.getElementById('btn-bulk').style.display = '';
    document.getElementById('btn-geofill').style.display = '';
    document.getElementById('btn-multirecce').style.display = '';
    document.getElementById('btn-clients').style.display = '';
    document.getElementById('btn-notify').style.display = '';
    document.getElementById('btn-audit').style.display = '';
    document.getElementById('btn-coverage').style.display = '';
    document.getElementById('btn-ai-import').style.display = '';
  }
  return true;
}·
// ── DB helpers ────────────────────────────────────────────
async function api(path, opts = {}) {
  const headers = {
    'Accept-Profile':  'counters',
    'Content-Profile': 'counters',
    'Accept':          'application/json',
    ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
    ...(opts.headers || {}),
  };
  const r = await fetch(DB + path, { ...opts, headers });
  if (!r.ok) {
    const text = await r.text().catch(() => '');
    throw new Error(`${r.status} ${text.slice(0, 200)}`);
  }
  if (r.status === 204) return null;
  const ct = r.headers.get('content-type') || '';
  return ct.includes('json') ? r.json() : r.text();
}·
// ID/Name policy (ADR-130 §5): counters.allocation/upload and recce.submissions
// all carry a genuine job_id FK to sales.jobs — resolve it to job_name via a
// single batched cross-schema lookup (never per-row), cached for the session.
// Returns whether anything was actually newly cached (false on a failed/offline
// fetch), so callers that re-render on \"true\" don't retry-storm when offline.
const _jobNameCache = {};
async function _ensureJobNames(jobIds) {
  const missing = [...new Set(jobIds)].filter(Boolean).filter(id => !_jobNameCache[id]);
  if (!missing.length) return false;
  let added = false;
  try {
    const r = await fetch(`${DB}/jobs?job_id=in.(${missing.map(encodeURIComponent).join(',')})&select=job_id,job_name`,
      { headers: { 'Accept-Profile': 'sales', 'Accept': 'application/json' } });
    const rows = r.ok ? await r.json() : [];
    rows.forEach(j => { _jobNameCache[j.job_id] = j.job_name; added = true; });
  } catch (_) { /* leave uncached — falls back to raw id this render */ }
  return added;
}·
// ── State ─────────────────────────────────────────────────
let allCounters = [];     // counter_v rows
let activeFilter = 'active';
const _listChips = { state: new Set(), tier: new Set(), store_type: new Set(), geo_cluster: new Set(), channel: new Set(), type: new Set() };
let detailCounter = null; // currently-open counter row
let detailContacts = [];
let detailAddresses = [];
let detailRecces = [];
let _recceFilter = { brands: new Set(), campaigns: new Set(), from: '', to: '' };
let detailBrands = [];        // counter_brand rows (available after v19 migration)
let detailAllocations = [];   // allocation rows for current counter
let editingId = null;     // null = creating; counter_id = editing
let editingContact = null;
let editingAddress = null;
let brandSet = new Set();
let _toastTimer;
let _dedupTimer;
let _dedupMatches = [];·
// ── Utilities ─────────────────────────────────────────────
function esc(s) { return (s == null ? '' : String(s)).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c])); }
function toast(msg, type = '') {
  const el = document.getElementById('toast');
  el.textContent = msg;
  el.className = 'show ' + type;
  clearTimeout(_toastTimer);
  _toastTimer = setTimeout(() => el.className = type, 2500);
}
function showScreen(id) {
  document.querySelectorAll('.scr').forEach(s => s.classList.remove('active'));
  document.getElementById(id).classList.add('active');
  window.scrollTo(0, 0);
}
function upper(el) { el.value = (el.value || '').toUpperCase(); }
function reload() { location.reload(); }
function slugify(s) { return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40); }
function newId(displayName) {
  const slug = slugify(displayName) || 'counter';
  const suffix = Math.floor(Math.random() * 0xffffff).toString(36).padStart(4, '0');
  return `cntr-${slug}-${suffix}`;
}
function fmtDate(d) { if (!d) return ''; try { return new Date(d).toLocaleDateString('en-IN'); } catch { return d; } }
function daysAgo(d) {
  if (!d) return '';
  const diff = Math.floor((Date.now() - new Date(d).getTime()) / 86400000);
  if (diff === 0) return 'today';
  if (diff === 1) return '1 day ago';
  if (diff < 30) return `${diff} days ago`;
  if (diff < 60) return '~1 month ago';
  const months = Math.floor(diff / 30);
  return `${months} months ago`;
}
function levenshtein(a, b) {
  if (!a || !b) return Math.max((a||'').length, (b||'').length);
  const m = a.length, n = b.length;
  const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0));
  for (let i = 0; i <= m; i++) dp[i][0] = i;
  for (let j = 0; j <= n; j++) dp[0][j] = j;
  for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) {
    dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] : 1 + Math.min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]);
  }
  return dp[m][n];
}·
// ── List screen ───────────────────────────────────────────
async function loadList() {
  try {
    allCounters = await api('/counter_v?order=display_name.asc&limit=2000');
  } catch (e) {
    toast('Load failed: ' + e.message, 'err');
    allCounters = [];
  }
  renderList();
  if (isSupervisor()) {
    checkInactiveAllocBanner();
    loadEmployees().then(() => _populateListFilters()); // pre-load so rep filter dropdown is populated
  }
}·
// Checks if any inactive employees have counters allocated; shows warning banner if so
let _inactiveAllocEmps = []; // [{id, name, count}] — populated by the banner check
async function checkInactiveAllocBanner() {
  try {
    const [allocRows, empRows] = await Promise.all([
      api('/allocation?select=employee_id&limit=2000'),
      fetch(`${DB}/employees?active=is.false&select=id,name`, {
        headers: { 'Accept-Profile': 'expense', 'Accept': 'application/json' },
      }).then(r => r.ok ? r.json() : []),
    ]);
    if (!empRows.length) return;
    const inactiveIds = new Set(empRows.map(e => e.id));
    const countsByEmp = {};
    allocRows.forEach(a => {
      if (inactiveIds.has(a.employee_id)) {
        countsByEmp[a.employee_id] = (countsByEmp[a.employee_id] || 0) + 1;
      }
    });
    const affected = Object.entries(countsByEmp).map(([id, count]) => {
      const emp = empRows.find(e => e.id === id);
      return { id, name: emp?.name || id, count };
    });
    _inactiveAllocEmps = affected;
    const banner = document.getElementById('alloc-warn-banner');
    if (!banner) return;
    if (!affected.length) { banner.classList.remove('show'); return; }
    const txt = affected.map(e => `${esc(e.name)} (${e.count})`).join(', ');
    document.getElementById('alloc-warn-text').innerHTML =
      `⚠️ Inactive: ${txt} — these counters have no active field rep`;
    banner.classList.add('show');
  } catch (_) {}
}·
let _inactiveAllocCounterIds = new Set();
async function filterByInactiveAlloc() {
  if (!_inactiveAllocEmps.length) return;
  // Fetch counter_ids for inactive employees
  try {
    const inactiveIds = _inactiveAllocEmps.map(e => e.id);
    const rows = await api(`/allocation?employee_id=in.(${inactiveIds.map(encodeURIComponent).join(',')})&select=counter_id,employee_id&limit=2000`);
    _inactiveAllocCounterIds = new Set(rows.map(r => r.counter_id));
    // Store full rows for bulk reassign
    _inactiveAllocRows = rows;
  } catch { return; }
  // Switch to \"all\" tile and set a custom filter mode
  activeFilter = 'inactive_alloc';
  document.querySelectorAll('.tile').forEach(t => t.classList.remove('sel'));
  renderList();
  document.getElementById('alloc-warn-banner').classList.remove('show');
  toast(`Showing ${_inactiveAllocCounterIds.size} counters from inactive employees`, 'info');
}·
let _inactiveAllocRows = []; // full allocation rows for inactive employees·
function onRepFilterChange() {
  const repId = document.getElementById('list-filter-rep')?.value || '';
  renderList();
  if (repId) {
    const emp = _allEmps.find(e => e.id === repId);
    const cnt = allCounters.filter(c => (c.allocated_emp_ids || []).includes(repId)).length;
    if (emp) toast(`Showing ${cnt} counters for ${emp.name}`, 'info');
  }
}·
async function openBulkReassign() {
  if (!_inactiveAllocEmps.length) return;
  await loadEmployees();
  const modal = document.getElementById('modal-bulk-reassign');
  const fromEl = document.getElementById('bulk-reassign-from');
  const toEl = document.getElementById('bulk-reassign-to');
  const summEl = document.getElementById('bulk-reassign-summary');
  // Build from text
  fromEl.innerHTML = _inactiveAllocEmps.map(e =>
    `<span style=\"background:rgba(249,115,22,.15);border:1px solid rgba(249,115,22,.3);border-radius:6px;padding:3px 8px;font-size:12px;font-weight:700\">${esc(e.name)} · ${e.count} counter${e.count !== 1 ? 's' : ''}</span>`
  ).join(' ');
  // Populate active employees in the dropdown
  toEl.innerHTML = '<option value=\"\">— Select active employee —</option>' +
    _allEmps.map(e => `<option value=\"${esc(e.id)}\">${esc(e.name)}${e.role ? ' · ' + esc(e.role) : ''}</option>`).join('');
  summEl.textContent = `${_inactiveAllocCounterIds.size} counter${_inactiveAllocCounterIds.size !== 1 ? 's' : ''} will be reassigned.`;
  modal.classList.add('show');
}·
async function confirmBulkReassign() {
  const toEmpId = document.getElementById('bulk-reassign-to').value;
  if (!toEmpId) { toast('Select an employee to reassign to', 'err'); return; }
  const btn = document.getElementById('btn-do-bulk-reassign');
  btn.disabled = true;
  btn.textContent = 'Reassigning…';
  try {
    // Group by from-employee so we can delete their allocations
    const fromIds = _inactiveAllocEmps.map(e => e.id);
    const counterIds = [..._inactiveAllocCounterIds];
    // Delete allocations for each inactive employee
    for (const fromId of fromIds) {
      await api(`/allocation?employee_id=eq.${encodeURIComponent(fromId)}&counter_id=in.(${counterIds.map(encodeURIComponent).join(',')})`, {
        method: 'DELETE', headers: { Prefer: 'return=minimal' },
      });
    }
    // Create new allocations for the target employee
    const newRows = counterIds.map(cid => ({ employee_id: toEmpId, counter_id: cid }));
    await api('/allocation', {
      method: 'POST',
      body: JSON.stringify(newRows),
      headers: { Prefer: 'return=minimal,resolution=ignore-duplicates' },
    });
    closeModal('modal-bulk-reassign');
    // Reset filter and reload
    activeFilter = 'all';
    _inactiveAllocEmps = [];
    _inactiveAllocCounterIds = new Set();
    _inactiveAllocRows = [];
    await loadList();
    const toEmp = _allEmps.find(e => e.id === toEmpId);
    toast(`✓ ${counterIds.length} counters reassigned to ${toEmp?.name || toEmpId}`, 'success');
  } catch (e) {
    toast('Reassign failed: ' + e.message, 'err');
  } finally {
    btn.disabled = false;
    btn.textContent = '⇄ Reassign All';
  }
}·
// ── Chip filter engine (ADR-027 / ADR-061) ───────────────────────────────·
function computeCounterAvail(subset) {
  const c = { state:{}, tier:{}, store_type:{}, geo_cluster:{}, channel:{}, type:{} };
  for (const r of subset) {
    if (r.state_code)   c.state[r.state_code]          = (c.state[r.state_code]||0)+1;
    if (r.city_tier)    c.tier[r.city_tier]             = (c.tier[r.city_tier]||0)+1;
    if (r.store_type)   c.store_type[r.store_type]      = (c.store_type[r.store_type]||0)+1;
    if (r.geo_cluster)  c.geo_cluster[r.geo_cluster]    = (c.geo_cluster[r.geo_cluster]||0)+1;
    if (r.channel)      c.channel[r.channel]            = (c.channel[r.channel]||0)+1;
    if (r.counter_type) c.type[r.counter_type]          = (c.type[r.counter_type]||0)+1;
  }
  return c;
}·
function listChipToggle(dim, val) {
  const s = _listChips[dim];
  if (s.has(val)) s.delete(val); else s.add(val);
  renderList();
}·
function _renderListChips(list) {
  const avail    = computeCounterAvail(list);
  const anyActive = Object.values(_listChips).some(s => s.size);·
  const FIXED = {
    state:      [{val:'PB',label:'Punjab'},{val:'HP',label:'Himachal'},{val:'JK',label:'J&K'},
                 {val:'HR',label:'Haryana'},{val:'CH',label:'Chd'},{val:'DL',label:'Delhi'}],
    tier:       [{val:'tier1',label:'Tier 1'},{val:'tier2',label:'Tier 2'},
                 {val:'tier3',label:'Tier 3'},{val:'tier4',label:'Tier 4'}],
    store_type: [{val:'les',label:'LES'},{val:'ses',label:'SES'},{val:'mbo',label:'MBO'},
                 {val:'ebo',label:'EBO'},{val:'modern_trade',label:'Modern Trade'},{val:'other',label:'Other'}],
    geo_cluster:[{val:'tricity',label:'Tricity'},{val:'ncr',label:'NCR'},{val:'majha',label:'Majha'},
                 {val:'doaba',label:'Doaba'},{val:'malwa',label:'Malwa'},{val:'kashmir_valley',label:'Kashmir Valley'}],
  };
  const LABELS = { state:'State', tier:'City Tier', store_type:'Store Type', geo_cluster:'Geo Cluster',
                   channel:'Channel', type:'Counter Type' };·
  function chipRow(dim, chips, activeSet, dimAvail) {
    let html = '';
    for (const {val, label} of chips) {
      const n       = dimAvail[val] || 0;
      const active  = activeSet.has(val);
      const unavail = n === 0 && !active && anyActive;
      const badge   = n > 0 ? ` <span style=\"font-size:9px;opacity:.6\">(${n})</span>` : '';
      html += `<button class=\"filter-chip${active?' active':''}${unavail?' unavailable':''}\" onclick=\"listChipToggle('${dim}','${esc(val)}')\">${esc(label)}${badge}</button>`;
    }
    if (activeSet.size) html += `<button class=\"filter-chip\" style=\"color:var(--err);border-color:var(--err)\" onclick=\"listClearDim('${dim}')\">✕</button>`;
    return html;
  }·
  let html = '<div class=\"filter-rows-wrap\">';
  // Fixed dimensions
  for (const [dim, chips] of Object.entries(FIXED)) {
    // Only show geo_cluster row if any counter has a geo_cluster set
    if (dim === 'geo_cluster' && !allCounters.some(c => c.geo_cluster)) continue;
    html += `<div class=\"filter-section\"><div class=\"filter-label\">${LABELS[dim]}</div><div class=\"filter-row\">${chipRow(dim, chips, _listChips[dim], avail[dim])}</div></div>`;
  }
  // Dynamic: Channel (show if ≥2 unique values in full dataset)
  const channels = [...new Set(allCounters.map(c => c.channel).filter(Boolean))].sort();
  if (channels.length >= 2) {
    html += `<div class=\"filter-section\"><div class=\"filter-label\">${LABELS.channel}</div><div class=\"filter-row\">${chipRow('channel', channels.map(v=>({val:v,label:v})), _listChips.channel, avail.channel)}</div></div>`;
  }
  // Dynamic: Counter Type (show if ≥2 unique values)
  const types = [...new Set(allCounters.map(c => c.counter_type).filter(Boolean))].sort();
  if (types.length >= 2) {
    html += `<div class=\"filter-section\"><div class=\"filter-label\">${LABELS.type}</div><div class=\"filter-row\">${chipRow('type', types.map(v=>({val:v,label:v})), _listChips.type, avail.type)}</div></div>`;
  }
  html += '</div>';
  const el = document.getElementById('list-chip-rows');
  if (el) el.innerHTML = html;
}·
function listClearDim(dim) {
  _listChips[dim].clear();
  renderList();
}·
function setTile(ev, key) {
  activeFilter = key;
  document.querySelectorAll('.tile').forEach(t => t.classList.toggle('sel', t.dataset.filter === key));
  // Auto-set sort to \"Stale first\" when switching to No Recce or Stale 90d tile
  if (key === 'norecce' || key === 'stale90') {
    const sortEl = document.getElementById('list-sort');
    if (sortEl && sortEl.value === 'name') sortEl.value = 'stale';
  }
  renderList();
}·
// All non-status filters (text, brand, territory, rep, chips). Used by filterCounters()
// and renderList() for tile counts/availability.
function _applyNonStatusFilters(source) {
  const q          = (document.getElementById('srch').value || '').trim().toLowerCase();
  const fBrand     = (document.getElementById('list-filter-brand')     ||{}).value || '';
  const fTerritory = (document.getElementById('list-filter-territory') ||{}).value || '';
  const fRep       = (document.getElementById('list-filter-rep')       ||{}).value || '';
  return source.filter(c => {
    if (fBrand     && !(c.brand_codes||[]).includes(fBrand))                        return false;
    if (fTerritory && (c.territory||'').toLowerCase() !== fTerritory.toLowerCase()) return false;
    if (fRep       && !(c.allocated_emp_ids||[]).includes(fRep))                    return false;
    if (_listChips.state.size      && !_listChips.state.has(c.state_code))          return false;
    if (_listChips.tier.size       && !_listChips.tier.has(c.city_tier))            return false;
    if (_listChips.store_type.size && !_listChips.store_type.has(c.store_type))     return false;
    if (_listChips.geo_cluster.size && !_listChips.geo_cluster.has(c.geo_cluster))  return false;
    if (_listChips.channel.size    && !_listChips.channel.has(c.channel))           return false;
    if (_listChips.type.size       && !_listChips.type.has(c.counter_type))         return false;
    if (!q) return true;
    const hay = [
      c.display_name, c.legal_name, c.proprietor_name,
      ...(c.aliases||[]), ...(c.brand_codes||[]),
      c.gstin, c.pan, c.address_line1, c.city, c.state, c.pincode, c.territory, c.distributor_name,
      c.channel, c.customer_group, c.counter_type,
      c.primary_contact_phone, c.primary_contact_name,
    ].filter(Boolean).join(' ').toLowerCase();
    return hay.includes(q);
  });
}·
function filterCounters() {
  const _s90cut = new Date(Date.now() - 90 * 86400000).toISOString().slice(0, 10);
  let list = _applyNonStatusFilters(allCounters).filter(c => {
    if (activeFilter === 'all')            { /* keep */ }
    else if (activeFilter === 'pending')        { if (c.approval_status !== 'pending') return false; }
    else if (activeFilter === 'inactive')       { if (c.is_active) return false; }
    else if (activeFilter === 'active')         { if (!c.is_active || c.approval_status === 'pending') return false; }
    else if (activeFilter === 'norecce')        { if (!c.is_active || c.approval_status === 'pending' || c.recce_count > 0) return false; }
    else if (activeFilter === 'stale90')        { if (!c.is_active || c.approval_status === 'pending' || !(c.recce_count > 0) || !c.last_recce_date || c.last_recce_date >= _s90cut) return false; }
    else if (activeFilter === 'inactive_alloc') { if (!_inactiveAllocCounterIds.has(c.counter_id)) return false; }
    return true;
  });
  const sort = (document.getElementById('list-sort')||{}).value || 'name';
  if (sort === 'recce_desc') list.sort((a, b) => (b.recce_count||0) - (a.recce_count||0));
  else if (sort === 'recce_asc')  list.sort((a, b) => (a.recce_count||0) - (b.recce_count||0));
  else if (sort === 'recent') list.sort((a, b) => {
    if (!a.last_recce_date && !b.last_recce_date) return 0;
    if (!a.last_recce_date) return 1; if (!b.last_recce_date) return -1;
    return b.last_recce_date.localeCompare(a.last_recce_date);
  });
  else if (sort === 'stale') list.sort((a, b) => {
    const ra = a.recce_count||0, rb = b.recce_count||0;
    if (ra === 0 && rb > 0) return -1; if (ra > 0 && rb === 0) return 1;
    if (ra === 0 && rb === 0) return (a.display_name||'').localeCompare(b.display_name||'');
    const da = a.last_recce_date||'9999', db = b.last_recce_date||'9999';
    return da < db ? -1 : da > db ? 1 : 0;
  });
  return list;
}·
function _populateListFilters() {
  // Brand and Territory: too many/dynamic options → keep as <select>
  const brands      = [...new Set(allCounters.flatMap(c => c.brand_codes||[]))].sort();
  const territories = [...new Set(allCounters.map(c => c.territory).filter(Boolean))].sort();
  const sb = document.getElementById('list-filter-brand');
  const st = document.getElementById('list-filter-territory');
  if (sb) {
    const prev = sb.value;
    sb.innerHTML = '<option value=\"\">All Brands</option>' +
      brands.map(b => `<option value=\"${esc(b)}\"${b===prev?' selected':''}>${esc(b)}</option>`).join('');
  }
  if (st) {
    const prev = st.value;
    st.innerHTML = '<option value=\"\">All Territories</option>' +
      territories.map(t => `<option value=\"${esc(t)}\"${t===prev?' selected':''}>${esc(t)}</option>`).join('');
    st.style.display = territories.length ? '' : 'none';
  }
  // Rep: supervisor-only. Build from allCounters allocations (includes inactive)
  // merged with _allEmps (active only) so every name that ever had an allocation shows.
  const sr = document.getElementById('list-filter-rep');
  if (sr && isSupervisor()) {
    const prev = sr.value;
    // Collect all (id → name) from allCounters allocation arrays
    const empMap = new Map(); // id → { name, active }
    allCounters.forEach(c => {
      const ids   = c.allocated_emp_ids || [];
      const names = c.allocated_to      || [];
      ids.forEach((id, i) => { if (id && !empMap.has(id)) empMap.set(id, { name: names[i] || id, active: false }); });
    });
    // Mark active employees (from _allEmps)
    _allEmps.forEach(e => {
      if (empMap.has(e.id)) empMap.get(e.id).active = true;
      else empMap.set(e.id, { name: e.name, active: true });
    });
    // Sort: active first (alphabetical), then inactive (alphabetical)
    const sorted = [...empMap.entries()]
      .map(([id, d]) => ({ id, name: d.name, active: d.active }))
      .sort((a, b) => {
        if (a.active !== b.active) return a.active ? -1 : 1;
        return a.name.localeCompare(b.name);
      });
    sr.innerHTML = '<option value=\"\">All Reps</option>' +
      sorted.map(e => `<option value=\"${esc(e.id)}\"${e.id===prev?' selected':''}>${esc(e.name)}${e.active?'':' (inactive)'}</option>`).join('');
    sr.style.display = '';
  } else if (sr) {
    sr.style.display = 'none';
  }
}·
function renderList() {
  const stale90cutoff = new Date(Date.now() - 90 * 86400000).toISOString().slice(0, 10);·
  // Tile counts from pre-status subset (cross-filtered by text/brand/territory/rep/chips)
  const preStatus = _applyNonStatusFilters(allCounters);
  const counts = {
    all:      preStatus.length,
    active:   preStatus.filter(c => c.is_active && c.approval_status !== 'pending').length,
    pending:  preStatus.filter(c => c.approval_status === 'pending').length,
    norecce:  preStatus.filter(c => c.is_active && c.approval_status !== 'pending' && !(c.recce_count > 0)).length,
    stale90:  preStatus.filter(c => c.is_active && c.approval_status !== 'pending' && c.recce_count > 0 && c.last_recce_date && c.last_recce_date < stale90cutoff).length,
    inactive: preStatus.filter(c => !c.is_active).length,
  };
  document.getElementById('t-all').textContent      = counts.all;
  document.getElementById('t-active').textContent   = counts.active;
  document.getElementById('t-pending').textContent  = counts.pending;
  document.getElementById('t-norecce').textContent  = counts.norecce;
  const stale90El = document.getElementById('t-stale90');
  if (stale90El) stale90El.textContent = counts.stale90;
  document.getElementById('t-inactive').textContent = counts.inactive;
  document.querySelectorAll('.tile').forEach(t => t.classList.toggle('sel', t.dataset.filter === activeFilter));·
  // Summary tiles (from pre-status — before status filter but after all other filters)
  const gpsCount   = preStatus.filter(c => c.gps_lat && c.gps_lng).length;
  const recceCount = preStatus.filter(c => c.recce_count > 0).length;
  const anyChip    = Object.values(_listChips).some(s => s.size);
  const tilesEl    = document.getElementById('list-sum-tiles');
  if (tilesEl) {
    tilesEl.innerHTML = [
      { icon:'🏪', val: anyChip ? `${preStatus.length}/${allCounters.length}` : preStatus.length, label:'Counters' },
      { icon:'✅', val: counts.active,  label:'Active' },
      { icon:'📍', val: gpsCount,       label:'GPS Ready' },
      { icon:'📸', val: recceCount,     label:'Recce\\'d' },
    ].map(t => `<div class=\"sum-tile\">
      <div style=\"font-size:14px\">${t.icon}</div>
      <div style=\"font-size:14px;font-weight:800;color:var(--accent)\">${t.val}</div>
      <div style=\"font-size:8.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px\">${t.label}</div>
    </div>`).join('');
  }·
  _populateListFilters();·
  const list = filterCounters();
  _renderListChips(list);
  document.getElementById('list-count').textContent = `${list.length} of ${allCounters.length}`;
  const out = document.getElementById('list-out');
  if (!list.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">🏪</div><div>No counters yet${isSupervisor() ? '. Tap + New to add one.' : '.'}</div></div>`;
    return;
  }
  const approveAllBanner = (activeFilter === 'pending' && isSupervisor() && list.length > 0)
    ? `<div style=\"display:flex;align-items:center;justify-content:space-between;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12.5px\">
        <span>${list.length} counter${list.length !== 1 ? 's' : ''} awaiting approval</span>
        <button class=\"btn btn-p\" onclick=\"approveAllPending()\" style=\"font-size:11px;padding:5px 10px\">✓ Approve All</button>
       </div>` : '';
  const inactiveAllocBanner = (activeFilter === 'inactive_alloc' && isSupervisor() && _inactiveAllocEmps.length)
    ? `<div style=\"display:flex;align-items:center;justify-content:space-between;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.3);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12.5px;gap:8px\">
        <span>👥 ${list.length} counter${list.length !== 1 ? 's' : ''} from inactive field reps — reassign to an active rep</span>
        <button class=\"btn btn-p\" onclick=\"openBulkReassign()\" style=\"font-size:11px;padding:5px 10px;background:#ea580c;flex-shrink:0\">⇄ Reassign</button>
       </div>` : '';
  const noRecceBanner = (activeFilter === 'norecce' && isSupervisor() && list.length > 0)
    ? `<div style=\"display:flex;align-items:center;justify-content:space-between;background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.3);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12.5px;gap:8px\">
        <span>📸 ${list.length} counter${list.length !== 1 ? 's' : ''} never visited — see who's responsible in Coverage Report</span>
        <button onclick=\"openCoverageReport()\" style=\"background:rgba(59,130,246,.2);border:1px solid rgba(59,130,246,.4);color:var(--text);font-size:11px;font-weight:700;padding:4px 10px;border-radius:10px;cursor:pointer;white-space:nowrap;flex-shrink:0\">📊 Coverage</button>
       </div>` : '';
  const stale90Banner = (activeFilter === 'stale90' && isSupervisor() && list.length > 0)
    ? `<div style=\"display:flex;align-items:center;justify-content:space-between;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12.5px;gap:8px\">
        <span>⏰ ${list.length} counter${list.length !== 1 ? 's' : ''} not visited in 90+ days — needs a re-Recce</span>
        <button onclick=\"openCoverageReport(90)\" style=\"background:rgba(245,158,11,.2);border:1px solid rgba(245,158,11,.4);color:var(--text);font-size:11px;font-weight:700;padding:4px 10px;border-radius:10px;cursor:pointer;white-space:nowrap;flex-shrink:0\">📊 Coverage</button>
       </div>` : '';
  out.innerHTML = approveAllBanner + inactiveAllocBanner + noRecceBanner + stale90Banner + list.map(c => {
    const brands = (c.brand_codes || []).slice(0, 4).map(b => `<span class=\"brand-chip\">${esc(b)}</span>`).join('');
    const more   = (c.brand_codes || []).length > 4 ? `<span class=\"brand-chip\">+${(c.brand_codes||[]).length - 4}</span>` : '';
    const statusClass = !c.is_active ? 'inactive' : c.approval_status === 'pending' ? 'pending_review' : c.status;
    return `
      <div class=\"c-row\" onclick=\"openDetail('${esc(c.counter_id)}')\">
        <div class=\"c-row-top\">
          <div class=\"c-row-name\">${esc(c.display_name)}</div>
          <span class=\"status-chip ${statusClass}\">${statusClass === 'pending_review' ? 'pending' : statusClass}</span>
          ${isSupervisor() ? `<button onclick=\"event.stopPropagation();quickAllocate('${esc(c.counter_id)}')\" title=\"Quick allocate\" style=\"background:none;border:none;font-size:15px;cursor:pointer;padding:2px 4px;color:var(--muted);flex-shrink:0\">👥</button>` : ''}
        </div>
        <div class=\"c-row-meta\">
          ${c.city ? `<span>📍 ${esc(c.city)}${c.pincode ? ' · ' + esc(c.pincode) : ''}${c.territory ? ' · ' + esc(c.territory) : ''}</span>` : ''}
          ${c.primary_contact_phone ? `<span onclick=\"event.stopPropagation()\" style=\"display:inline-flex;gap:4px;align-items:center\"><a href=\"tel:${esc(c.primary_contact_phone.replace(/\\D/g,''))}\" style=\"color:inherit;text-decoration:none\">☎ ${esc(c.primary_contact_phone)}</a><a href=\"https://wa.me/91${esc(c.primary_contact_phone.replace(/\\D/g,'').slice(-10))}\" target=\"_blank\" style=\"font-size:10px;background:#dcfce7;color:#15803d;padding:1px 5px;border-radius:8px;text-decoration:none;font-weight:600\">WA</a></span>` : ''}
          ${c.channel ? `<span style=\"font-size:10px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:1px 6px;color:var(--muted)\">${esc(c.channel)}</span>` : ''}
          ${c.gstin ? `<span>GST ${esc(c.gstin.slice(0, 6))}…</span>` : ''}
          ${c.recce_count > 0
            ? `<span style=\"color:${c.last_recce_date && (Date.now()-new Date(c.last_recce_date).getTime())>90*86400000?'var(--or)':'var(--accent)'}\">📸 ${c.recce_count} Recce${c.recce_count !== 1 ? 's' : ''}${c.last_recce_date ? ' · ' + daysAgo(c.last_recce_date) : ''}</span>`
            : activeFilter === 'norecce' ? `<span style=\"color:var(--muted);font-size:11px\">Never visited</span>` : ''}
          ${isSupervisor() && (c.allocated_to || []).length ? `<span style=\"font-size:10px;background:rgba(99,102,241,.15);border:1px solid rgba(99,102,241,.3);border-radius:8px;padding:1px 6px;color:var(--accent)\">👤 ${esc((c.allocated_to || []).join(', '))}</span>` : ''}
        </div>
        ${brands || more ? `<div class=\"c-row-meta\" style=\"margin-top:4px\"><span class=\"brands\">${brands}${more}</span></div>` : ''}
      </div>`;
  }).join('');
}·
async function approveAllPending() {
  if (!isSupervisor()) return;
  const list = allCounters.filter(c => c.approval_status === 'pending');
  if (!list.length) return;
  if (!confirm(`Approve ${list.length} pending counter${list.length !== 1 ? 's' : ''}?\\nThis sets all to approved immediately.`)) return;
  const ids = list.map(c => c.counter_id);
  const now = new Date().toISOString();
  try {
    await api(`/counter?counter_id=in.(${ids.join(',')})`, {
      method: 'PATCH',
      body: JSON.stringify({ approval_status: 'approved', approved_by: myId, approved_at: now }),
      headers: { Prefer: 'return=minimal' },
    });
    toast(`Approved ${ids.length} counter${ids.length !== 1 ? 's' : ''} ✓`, 'ok');
    await loadList();
  } catch (e) {
    toast('Approve failed: ' + e.message, 'err');
  }
}·
// ── Detail screen ─────────────────────────────────────────
async function openDetail(id) {
  showScreen('s-detail');
  document.getElementById('d-title').textContent = 'Loading…';
  document.getElementById('btn-edit').style.display     = isSupervisor() ? '' : 'none';
  document.getElementById('btn-allocate').style.display = isSupervisor() ? '' : 'none';
  try {
    const [row]  = await api(`/counter_v?counter_id=eq.${encodeURIComponent(id)}&limit=1`);
    if (!row) throw new Error('Not found');
    detailCounter = row;
    const empLoad = isSupervisor() ? loadEmployees() : Promise.resolve();
    [detailContacts, detailAddresses] = await Promise.all([
      api(`/contact?counter_id=eq.${encodeURIComponent(id)}&order=is_primary.desc,contact_id.asc`),
      api(`/address?counter_id=eq.${encodeURIComponent(id)}&order=is_default.desc,address_id.asc`),
      empLoad,
    ]);
    // Recce history — cross-schema; counter_id populated once Recce PWA uses DB-backed picker
    try {
      detailRecces = await fetch(`${DB}/submissions?counter_id=eq.${encodeURIComponent(id)}&order=visit_date.desc&limit=200`, {
        headers: { 'Accept-Profile': 'recce', 'Accept': 'application/json' },
      }).then(r => r.ok ? r.json() : []);
    } catch { detailRecces = []; }
    // Brand association history (available after migrate_counters_v19.sql is run)
    try {
      detailBrands = await api(`/counter_brand?counter_id=eq.${encodeURIComponent(id)}&order=added_at.desc&limit=100`);
    } catch { detailBrands = []; }
    // Current allocation list
    try {
      detailAllocations = await api(`/allocation?counter_id=eq.${encodeURIComponent(id)}&select=employee_id,job_id&order=employee_id.asc&limit=50`);
    } catch { detailAllocations = []; }
    renderDetail();
  } catch (e) {
    toast('Detail failed: ' + e.message, 'err');
    showScreen('s-list');
  }
}·
function renderDetail() {
  const c = detailCounter;
  document.getElementById('d-title').textContent = c.display_name;·
  // Identity card
  const idHtml = [
    row('Display', c.display_name),
    row('Legal',   c.legal_name),
    row('Proprietor', c.proprietor_name),
    row('GSTIN',   c.gstin),
    row('PAN',     c.pan),
    row('Counter type', c.counter_type),
    row('Channel',  c.channel),
    row('Group',    c.customer_group),
    row('Status',  `<span class=\"status-chip ${!c.is_active ? 'inactive' : c.status}\">${!c.is_active ? 'INACTIVE' : c.status}</span>`),
    row('Approval', c.approval_status === 'pending' ? '<span class=\"status-chip pending_review\">PENDING REVIEW</span>' : '✓ Approved'),
    c.aliases?.length ? row('Aliases', c.aliases.join(', ')) : '',
    row('Owner',   c.owner_user),
    row('Recce visits', c.recce_count > 0
      ? `${c.recce_count}${c.last_recce_date ? ' · last ' + daysAgo(c.last_recce_date) + ' <span style=\"color:var(--muted);font-size:10px\">(' + fmtDate(c.last_recce_date) + ')</span>' : ''}`
      : '<span style=\"color:var(--muted)\">None yet</span>'),
    detailAllocations.length
      ? row('Allocated to', detailAllocations.map(a => {
          const emp = _allEmps.find(e => e.id === a.employee_id);
          const nm = emp ? esc(emp.name) : esc(a.employee_id);
          return nm + (a.job_id ? ` <span style=\"color:var(--muted);font-size:10px\">(${esc(a.job_id)})</span>` : '');
        }).join(', '))
      : isSupervisor() ? row('Allocated to', '<span style=\"color:var(--muted)\">Nobody — tap 👥 Allocate to assign</span>') : '',
  ].filter(Boolean).join('');
  // Primary contact quick-actions row (phone + WhatsApp) — if a primary contact exists
  const primaryContact = detailContacts.find(ct => ct.is_primary) || detailContacts[0];
  const ctPhone = primaryContact?.phone?.replace(/\\D/g, '').slice(-10) || '';
  const ctName  = primaryContact?.name || '';
  const contactActionBar = ctPhone ? `
    <div style=\"display:flex;gap:8px;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)\">
      <div style=\"font-size:11px;color:var(--muted);font-weight:600;align-self:center;flex:1\">${esc(ctName)}</div>
      <a href=\"tel:${esc(ctPhone)}\" style=\"display:flex;align-items:center;gap:4px;padding:6px 12px;background:var(--card);border:1px solid var(--border);border-radius:8px;text-decoration:none;color:var(--text);font-size:12px;font-weight:700;flex-shrink:0\">☎ Call</a>
      <a href=\"https://wa.me/91${esc(ctPhone)}\" target=\"_blank\" style=\"display:flex;align-items:center;gap:4px;padding:6px 12px;background:#dcfce7;border:1px solid #86efac;border-radius:8px;text-decoration:none;color:#14532d;font-size:12px;font-weight:700;flex-shrink:0\">💬 WA</a>
    </div>` : '';
  document.getElementById('d-identity').innerHTML = idHtml + contactActionBar;·
  // Address card
  const addrParts = [c.address_line1, c.address_line2, c.city, c.state, c.pincode].filter(Boolean).join(', ');
  const mapsUrl = (c.gps_lat && c.gps_lng)
    ? `https://www.google.com/maps?q=${c.gps_lat},${c.gps_lng}`
    : addrParts ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(addrParts)}` : '';
  const mapsLink = mapsUrl ? ` · <a href=\"${mapsUrl}\" target=\"_blank\" style=\"color:#7dd3fc\">🗺 Map</a>` : '';
  const addrHtml = [
    `<div style=\"font-size:13px;font-weight:600\">${esc(addrParts)}${mapsLink}</div>`,
    c.country && c.country !== 'India' ? `<div style=\"font-size:11px;color:var(--muted);margin-top:2px\">${esc(c.country)}</div>` : '',
    c.distributor_name ? row('Distributor', c.distributor_name) : '',
    c.territory ? row('Territory', c.territory) : '',
    c.gps_lat && c.gps_lng ? row('GPS', `<span style=\"font-size:11px;font-family:monospace\">${esc(c.gps_lat)}, ${esc(c.gps_lng)}</span>`) : '',
  ].filter(Boolean).join('');
  document.getElementById('d-address').innerHTML = addrHtml;
  // Notes card (separate section below address)
  const notesEl = document.getElementById('d-notes');
  if (notesEl) {
    if (c.notes) {
      notesEl.style.display = '';
      notesEl.innerHTML = `<div style=\"font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;font-weight:700;margin-bottom:6px\">Notes</div>
        <div style=\"font-size:13px;white-space:pre-wrap;line-height:1.5\">${esc(c.notes)}</div>`;
    } else {
      notesEl.style.display = 'none';
    }
  }·
  // Brands tab — combine brand_codes[] with counter_brand history when available
  const brands = c.brand_codes || [];
  const brandsHtml = brands.length
    ? brands.map(b => {
        const hist = detailBrands.filter(r => r.brand_code === b);
        const first = hist.length ? hist[hist.length - 1] : null;
        const last  = hist.length ? hist[0] : null;
        const histLine = first
          ? `Added ${fmtDate(first.added_at)} by ${esc(first.added_by || '—')}${hist.length > 1 ? ` · ${hist.length} entries` : ''}`
          : brandStatsLine(b);
        return `<div class=\"k-row\"><div class=\"k-main\">
          <div class=\"k-title\">${esc(b)}</div>
          <div class=\"k-meta\"><span>${histLine}</span>${last && last !== first ? `<span>Last: ${fmtDate(last.added_at)}</span>` : ''}</div>
        </div></div>`;
      }).join('')
    : `<div class=\"empty\"><div class=\"empty-icon\">🏷</div><div>No brands recorded yet</div></div>`;
  // ADR-143 — master first, then whatever the data already carries
  const knownBrands = [...new Set([...(window.MASTER_BRAND_CODES || []),
                                   ...allCounters.flatMap(c => c.brand_codes || [])])].sort();
  const brandDatalist = knownBrands.length
    ? `<datalist id=\"brand-suggestions\">${knownBrands.map(b => `<option value=\"${esc(b)}\">`).join('')}</datalist>` : '';
  const addBrandBtn = isSupervisor()
    ? `${brandDatalist}<div style=\"margin-top:10px;display:flex;gap:6px;align-items:center\" id=\"brand-add-row\">
        <input id=\"brand-add-inp\" list=\"brand-suggestions\" placeholder=\"Add brand code…\"
          style=\"flex:1;padding:7px 10px;border:1.5px solid var(--border);border-radius:8px;font-family:'DM Sans',sans-serif;font-size:13px;background:var(--card);color:var(--text)\"
          onkeydown=\"if(event.key==='Enter'){event.preventDefault();saveBrandToCounter('${esc(c.counter_id)}')}\"
          oninput=\"this.value=this.value.toUpperCase().replace(/[^A-Z0-9_]/g,'')\">
        <button class=\"btn btn-p\" onclick=\"saveBrandToCounter('${esc(c.counter_id)}')\" style=\"padding:7px 14px;font-size:12px\">+ Add</button>
       </div>` : '';
  document.getElementById('d-brands').innerHTML = brandsHtml + addBrandBtn;·
  // Contacts tab
  const contactsHtml = detailContacts.length
    ? detailContacts.map(ct => `
        <div class=\"k-row\">
          <div class=\"k-main\">
            <div class=\"k-title\">${esc(ct.name)}${ct.is_primary ? ' <span class=\"k-badge primary\">PRIMARY</span>' : ''}</div>
            <div class=\"k-meta\">
              <span class=\"k-badge ${ct.role}\">${esc(ct.role)}</span>
              ${ct.designation ? `<span>${esc(ct.designation)}</span>` : ''}
              ${ct.phone ? `<span><a href=\"tel:${esc(ct.phone.replace(/\\D/g,''))}\" style=\"color:inherit;text-decoration:none\">☎ ${esc(ct.phone)}</a></span>` : ''}
              ${ct.whatsapp ? `<span><a href=\"https://wa.me/91${esc(ct.whatsapp.replace(/\\D/g,'').slice(-10))}\" target=\"_blank\" style=\"color:#15803d;text-decoration:none\">💬 WA</a></span>` : ''}
              ${ct.email ? `<span>✉ ${esc(ct.email)}</span>` : ''}
            </div>
          </div>
          ${isSupervisor() ? `<div class=\"k-actions\"><button onclick=\"openContact(${ct.contact_id})\">Edit</button><button onclick=\"deleteContact(${ct.contact_id})\">×</button></div>` : ''}
        </div>`).join('')
    : `<div class=\"empty\"><div class=\"empty-icon\">📇</div><div>No contacts yet</div></div>`;
  document.getElementById('d-contacts').innerHTML = contactsHtml +
    (isSupervisor() ? `<button class=\"btn btn-s\" onclick=\"openContact()\" style=\"margin-top:6px\">+ Add contact</button>` : '');·
  // Secondary addresses tab
  const addrsHtml = detailAddresses.length
    ? detailAddresses.map(a => `
        <div class=\"k-row\">
          <div class=\"k-main\">
            <div class=\"k-title\">${esc(a.line1)}</div>
            <div class=\"k-meta\">
              <span class=\"k-badge ${a.type}\">${esc(a.type)}</span>
              ${a.is_default ? '<span class=\"k-badge default\">DEFAULT</span>' : ''}
              ${a.city ? `<span>${esc(a.city)}</span>` : ''}
              ${a.pincode ? `<span>${esc(a.pincode)}</span>` : ''}
            </div>
          </div>
          ${isSupervisor() ? `<div class=\"k-actions\"><button onclick=\"openAddress(${a.address_id})\">Edit</button><button onclick=\"deleteAddress(${a.address_id})\">×</button></div>` : ''}
        </div>`).join('')
    : `<div class=\"empty\"><div class=\"empty-icon\">📍</div><div>No secondary addresses</div></div>`;
  document.getElementById('d-addrs').innerHTML = addrsHtml +
    (isSupervisor() ? `<button class=\"btn btn-s\" onclick=\"openAddress()\" style=\"margin-top:6px\">+ Add address</button>` : '');·
  // Past Recces tab — filter state reset + render
  _recceFilter = { brands: new Set(), campaigns: new Set(), from: '', to: '' };
  renderRecceTab();
}·
async function saveBrandToCounter(counterId) {
  const inp = document.getElementById('brand-add-inp');
  if (!inp) return;
  const code = inp.value.trim().toUpperCase().replace(/[^A-Z0-9_]/g, '');
  if (!code) { toast('Enter a brand code', 'err'); inp.focus(); return; }
  const c = allCounters.find(x => x.counter_id === counterId);
  if (!c) return;
  const existing = new Set(c.brand_codes || []);
  if (existing.has(code)) { toast(`${code} already on this counter`, 'info'); return; }
  const merged = [...existing, code];
  try {
    await api(`/counter?counter_id=eq.${encodeURIComponent(counterId)}`, {
      method: 'PATCH',
      body: JSON.stringify({ brand_codes: merged }),
      headers: { Prefer: 'return=minimal' },
    });
    await api('/counter_brand', {
      method: 'POST',
      body: JSON.stringify([{ counter_id: counterId, brand_code: code, source: 'manual', added_by: myId }]),
      headers: { Prefer: 'return=minimal,resolution=ignore-duplicates' },
    }).catch(() => {});
    toast(`${code} added ✓`, 'ok');
    inp.value = '';
    await openDetail(counterId);
    setTab('brands');
  } catch (e) {
    toast('Save failed: ' + e.message, 'err');
  }
}·
function brandStatsLine(brand) {
  const recsForBrand = detailRecces.filter(r => (r.brand || '').toUpperCase() === brand.toUpperCase());
  if (!recsForBrand.length) return 'No past Recce yet';
  const last = recsForBrand[0];
  return `${recsForBrand.length} past Recce${recsForBrand.length > 1 ? 's' : ''} · last ${fmtDate(last.visit_date)}`;
}·
function row(label, val) {
  if (val == null || val === '') return '';
  return `<div style=\"display:flex;justify-content:space-between;gap:8px;font-size:12.5px;padding:3px 0\"><span style=\"color:var(--muted)\">${esc(label)}</span><span style=\"font-weight:600;text-align:right\">${val}</span></div>`;
}·
function setTab(tab) {
  document.querySelectorAll('.tabs button').forEach(b => b.classList.toggle('sel', b.dataset.tab === tab));
  document.getElementById('d-brands').style.display   = tab === 'brands'   ? '' : 'none';
  document.getElementById('d-contacts').style.display = tab === 'contacts' ? '' : 'none';
  document.getElementById('d-addrs').style.display    = tab === 'addrs'    ? '' : 'none';
  document.getElementById('d-recces').style.display   = tab === 'recces'   ? '' : 'none';
}·
// ── New / Edit ────────────────────────────────────────────
function openNew(prefill) {
  if (!isSupervisor()) { toast('Supervisor access required', 'err'); return; }
  editingId = null;
  document.getElementById('f-title').textContent = 'New Counter';
  clearForm();
  if (prefill) {
    if (prefill.name)    document.getElementById('f-display_name').value = prefill.name;
    if (prefill.address) document.getElementById('f-address_line1').value = prefill.address;
    if (prefill.city)    document.getElementById('f-city').value = prefill.city;
    // mobile: put in notes since contacts are created separately
    if (prefill.mobile || prefill.notes) {
      const notesParts = [];
      if (prefill.mobile) notesParts.push('Contact: ' + prefill.mobile);
      if (prefill.notes) notesParts.push(prefill.notes);
      document.getElementById('f-notes').value = notesParts.join('\\n');
    }
  }
  document.getElementById('dedup').classList.remove('show');
  if (prefill?.name) dedupCheck();
  showScreen('s-form');
}·
function openEdit() {
  if (!isSupervisor() || !detailCounter) return;
  editingId = detailCounter.counter_id;
  document.getElementById('f-title').textContent = 'Edit Counter';
  fillForm(detailCounter);
  document.getElementById('dedup').classList.remove('show');
  showScreen('s-form');
}·
function clearForm() {
  ['f-display_name','f-legal_name','f-proprietor_name','f-aliases','f-address_line1','f-address_line2','f-city','f-state','f-state_code','f-pincode','f-gps_lat','f-gps_lng','f-gstin','f-pan','f-fssai','f-drug_lic','f-counter_type','f-channel','f-customer_group','f-territory','f-distributor_name','f-notes','f-city_tier','f-store_type','f-geo_cluster']
    .forEach(id => document.getElementById(id).value = '');
  document.getElementById('f-status').value = 'active';
  brandSet = new Set();
  renderBrandChips();
}·
function fillForm(c) {
  const set = (id, v) => document.getElementById(id).value = (v == null ? '' : v);
  set('f-display_name',     c.display_name);
  set('f-legal_name',       c.legal_name);
  set('f-proprietor_name',  c.proprietor_name);
  set('f-aliases',          (c.aliases || []).join(', '));
  set('f-address_line1',    c.address_line1);
  set('f-address_line2',    c.address_line2);
  set('f-city',             c.city);
  set('f-state',            c.state);
  set('f-state_code',       c.state_code);
  set('f-pincode',          c.pincode);
  set('f-gps_lat',          c.gps_lat);
  set('f-gps_lng',          c.gps_lng);
  set('f-gstin',            c.gstin);
  set('f-pan',              c.pan);
  set('f-fssai',            c.fssai);
  set('f-drug_lic',         c.drug_lic);
  set('f-counter_type',     c.counter_type || '');
  set('f-channel',          c.channel);
  set('f-customer_group',   c.customer_group);
  set('f-territory',        c.territory);
  set('f-city_tier',        c.city_tier  || '');
  set('f-store_type',       c.store_type || '');
  set('f-geo_cluster',      c.geo_cluster  || '');
  set('f-distributor_name', c.distributor_name);
  set('f-status',           c.status || 'active');
  set('f-notes',            c.notes);
  brandSet = new Set(c.brand_codes || []);
  renderBrandChips();
}·
function cancelForm() {
  if (editingId) showScreen('s-detail'); else showScreen('s-list');
}·
function fillGpsFromBrowser() {
  const st = document.getElementById('gps-status');
  if (!navigator.geolocation) { if (st) st.textContent = 'Geolocation not supported by this browser.'; return; }
  if (st) st.textContent = '⏳ Getting location…';
  navigator.geolocation.getCurrentPosition(
    pos => {
      document.getElementById('f-gps_lat').value = pos.coords.latitude.toFixed(7);
      document.getElementById('f-gps_lng').value = pos.coords.longitude.toFixed(7);
      if (st) st.textContent = `✓ Accuracy ±${Math.round(pos.coords.accuracy)}m`;
    },
    err => { if (st) st.textContent = 'Location error: ' + err.message; },
    { enableHighAccuracy: true, timeout: 10000 }
  );
}·
function brandKey(ev) {
  const inp = ev.target;
  if (ev.key === 'Enter' || ev.key === ',') {
    ev.preventDefault();
    const v = inp.value.trim().toUpperCase();
    if (v) { brandSet.add(v); renderBrandChips(); }
    inp.value = '';
  } else if (ev.key === 'Backspace' && !inp.value && brandSet.size) {
    const arr = [...brandSet]; arr.pop(); brandSet = new Set(arr); renderBrandChips();
  }
}·
function renderBrandChips() {
  const box = document.getElementById('bcset');
  // remove existing chips (keep the input)
  box.querySelectorAll('.b').forEach(b => b.remove());
  const inp = document.getElementById('brand-inp');
  [...brandSet].forEach(b => {
    const el = document.createElement('span');
    el.className = 'b';
    el.innerHTML = `${esc(b)}<button type=\"button\" aria-label=\"Remove\">×</button>`;
    el.querySelector('button').addEventListener('click', () => { brandSet.delete(b); renderBrandChips(); });
    box.insertBefore(el, inp);
  });
}·
function pincodeAutoFill(pin) {
  if (!pin || pin.length < 6) return;
  const cityEl  = document.getElementById('f-city');
  const stateEl = document.getElementById('f-state');
  // Only auto-fill if fields are currently empty
  if (cityEl.value.trim() && stateEl.value.trim()) return;
  const match = allCounters.find(c => c.pincode === pin && c.city);
  if (!match) return;
  if (!cityEl.value.trim() && match.city)   { cityEl.value  = match.city;  }
  if (!stateEl.value.trim() && match.state) { stateEl.value = match.state; }
}·
function dedupCheck() {
  if (editingId) return; // dedup only for new counters
  clearTimeout(_dedupTimer);
  _dedupTimer = setTimeout(() => {
    const name = (document.getElementById('f-display_name').value || '').trim().toLowerCase();
    const pincode = (document.getElementById('f-pincode').value || '').trim();
    const city    = (document.getElementById('f-city').value || '').trim().toLowerCase();
    const gstin   = (document.getElementById('f-gstin').value || '').trim();
    if (name.length < 3) { document.getElementById('dedup').classList.remove('show'); return; }
    const matches = allCounters.filter(c => {
      if (gstin && c.gstin && c.gstin.toUpperCase() === gstin.toUpperCase()) return true;
      const cName = (c.display_name || '').toLowerCase();
      const sameRegion = (pincode && c.pincode === pincode) || (city && (c.city || '').toLowerCase() === city);
      if (!sameRegion) return false;
      return cName.includes(name) || name.includes(cName) || levenshtein(name, cName) <= 3;
    }).slice(0, 5);
    _dedupMatches = matches;
    const panel = document.getElementById('dedup');
    if (!matches.length) { panel.classList.remove('show'); return; }
    document.getElementById('dedup-list').innerHTML = matches.map(m => `
      <div style=\"margin-top:6px;font-size:12px\">
        <b>${esc(m.display_name)}</b>${m.city ? ' · ' + esc(m.city) : ''}${m.pincode ? ' · ' + esc(m.pincode) : ''}
        <button onclick=\"openDetail('${esc(m.counter_id)}')\">Open</button>
      </div>`).join('');
    panel.classList.add('show');
  }, 250);
}·
async function saveForm() {
  const display_name = document.getElementById('f-display_name').value.trim();
  const address_line1 = document.getElementById('f-address_line1').value.trim();
  if (!display_name) { toast('Display name is required', 'err'); return; }
  if (!address_line1) { toast('Address line 1 is required', 'err'); return; }
  const btn = document.getElementById('btn-save');
  btn.disabled = true; btn.innerHTML = '<span class=\"spin\">↻</span> Saving';
  const payload = {
    display_name,
    legal_name:       trim('f-legal_name'),
    proprietor_name:  trim('f-proprietor_name'),
    aliases:          (document.getElementById('f-aliases').value || '').split(',').map(s => s.trim()).filter(Boolean),
    address_line1,
    address_line2:    trim('f-address_line2'),
    city:             trim('f-city'),
    state:            trim('f-state'),
    state_code:       trim('f-state_code'),
    pincode:          trim('f-pincode'),
    gps_lat:          numOrNull('f-gps_lat'),
    gps_lng:          numOrNull('f-gps_lng'),
    gstin:            (trim('f-gstin') || '').toUpperCase() || null,
    pan:              (trim('f-pan')   || '').toUpperCase() || null,
    fssai:            trim('f-fssai'),
    drug_lic:         trim('f-drug_lic'),
    counter_type:     document.getElementById('f-counter_type').value || null,
    channel:          trim('f-channel'),
    city_tier:        document.getElementById('f-city_tier').value  || null,
    store_type:       document.getElementById('f-store_type').value || null,
    geo_cluster:      trim('f-geo_cluster') || null,
    customer_group:   trim('f-customer_group'),
    territory:        trim('f-territory'),
    distributor_name: trim('f-distributor_name'),
    brand_codes:      [...brandSet],
    status:           document.getElementById('f-status').value,
    notes:            trim('f-notes'),
    updated_by:       myId,
    updated_at:       new Date().toISOString(),
  };
  try {
    if (editingId) {
      await api(`/counter?counter_id=eq.${encodeURIComponent(editingId)}`, {
        method:  'PATCH',
        body:    JSON.stringify(payload),
        headers: { Prefer: 'return=minimal' },
      });
      toast('Saved ✓', 'ok');
      await loadList();
      const fresh = allCounters.find(c => c.counter_id === editingId);
      if (fresh) { detailCounter = fresh; await openDetail(editingId); }
      else showScreen('s-list');
    } else {
      payload.counter_id   = newId(display_name);
      payload.created_by   = myId;
      payload.created_from = 'manual';
      await api('/counter', {
        method:  'POST',
        body:    JSON.stringify(payload),
        headers: { Prefer: 'return=minimal' },
      });
      toast('Created ✓', 'ok');
      await loadList();
      await openDetail(payload.counter_id);
    }
  } catch (e) {
    toast('Save failed: ' + e.message, 'err');
  } finally {
    btn.disabled = false; btn.innerHTML = 'Save';
  }
}·
function trim(id)       { return (document.getElementById(id).value || '').trim() || null; }
function numOrNull(id)  { const v = document.getElementById(id).value; return v === '' ? null : Number(v); }·
// ── Contacts ──────────────────────────────────────────────
function openContact(contactId) {
  if (!isSupervisor() || !detailCounter) return;
  editingContact = contactId || null;
  const ct = contactId ? detailContacts.find(c => c.contact_id === contactId) : {};
  document.getElementById('contact-ttl').textContent = contactId ? 'Edit contact' : 'Add contact';
  document.getElementById('cf-name').value         = ct?.name         || '';
  document.getElementById('cf-role').value         = ct?.role         || 'primary';
  document.getElementById('cf-designation').value  = ct?.designation  || '';
  document.getElementById('cf-phone').value        = ct?.phone        || '';
  document.getElementById('cf-whatsapp').value     = ct?.whatsapp     || '';
  document.getElementById('cf-email').value        = ct?.email        || '';
  document.getElementById('cf-is_primary').checked = !!ct?.is_primary;
  document.getElementById('modal-contact').classList.add('show');
}·
async function saveContact() {
  const name = document.getElementById('cf-name').value.trim();
  if (!name) { toast('Name required', 'err'); return; }
  const payload = {
    counter_id:  detailCounter.counter_id,
    name,
    role:        document.getElementById('cf-role').value,
    designation: document.getElementById('cf-designation').value.trim() || null,
    phone:       document.getElementById('cf-phone').value.trim() || null,
    whatsapp:    document.getElementById('cf-whatsapp').value.trim() || null,
    email:       document.getElementById('cf-email').value.trim() || null,
    is_primary:  document.getElementById('cf-is_primary').checked,
    updated_at:  new Date().toISOString(),
  };
  try {
    // demote other primaries first if we're making this one primary
    if (payload.is_primary) {
      await api(`/contact?counter_id=eq.${encodeURIComponent(payload.counter_id)}&is_primary=eq.true`, {
        method: 'PATCH', body: JSON.stringify({ is_primary: false }), headers: { Prefer: 'return=minimal' },
      });
    }
    if (editingContact) {
      await api(`/contact?contact_id=eq.${editingContact}`, {
        method: 'PATCH', body: JSON.stringify(payload), headers: { Prefer: 'return=minimal' },
      });
    } else {
      await api('/contact', { method: 'POST', body: JSON.stringify(payload), headers: { Prefer: 'return=minimal' } });
    }
    closeModal('modal-contact');
    await openDetail(detailCounter.counter_id);
    toast('Saved ✓', 'ok');
  } catch (e) {
    toast('Save failed: ' + e.message, 'err');
  }
}·
async function deleteContact(contactId) {
  if (!confirm('Delete this contact?')) return;
  try {
    await api(`/contact?contact_id=eq.${contactId}`, { method: 'DELETE', headers: { Prefer: 'return=minimal' } });
    await openDetail(detailCounter.counter_id);
    toast('Deleted', 'ok');
  } catch (e) { toast('Delete failed: ' + e.message, 'err'); }
}·
// ── Secondary addresses ───────────────────────────────────
function openAddress(addressId) {
  if (!isSupervisor() || !detailCounter) return;
  editingAddress = addressId || null;
  const a = addressId ? detailAddresses.find(x => x.address_id === addressId) : {};
  document.getElementById('address-ttl').textContent = addressId ? 'Edit address' : 'Add address';
  document.getElementById('af-type').value       = a?.type       || 'billing';
  document.getElementById('af-line1').value      = a?.line1      || '';
  document.getElementById('af-line2').value      = a?.line2      || '';
  document.getElementById('af-city').value       = a?.city       || '';
  document.getElementById('af-state').value      = a?.state      || '';
  document.getElementById('af-pincode').value    = a?.pincode    || '';
  document.getElementById('af-is_default').checked = !!a?.is_default;
  document.getElementById('modal-address').classList.add('show');
}·
async function saveAddress() {
  const line1 = document.getElementById('af-line1').value.trim();
  if (!line1) { toast('Line 1 required', 'err'); return; }
  const payload = {
    counter_id: detailCounter.counter_id,
    type:       document.getElementById('af-type').value,
    line1,
    line2:      document.getElementById('af-line2').value.trim() || null,
    city:       document.getElementById('af-city').value.trim() || null,
    state:      document.getElementById('af-state').value.trim() || null,
    pincode:    document.getElementById('af-pincode').value.trim() || null,
    is_default: document.getElementById('af-is_default').checked,
    updated_at: new Date().toISOString(),
  };
  try {
    if (payload.is_default) {
      await api(`/address?counter_id=eq.${encodeURIComponent(payload.counter_id)}&is_default=eq.true`, {
        method: 'PATCH', body: JSON.stringify({ is_default: false }), headers: { Prefer: 'return=minimal' },
      });
    }
    if (editingAddress) {
      await api(`/address?address_id=eq.${editingAddress}`, {
        method: 'PATCH', body: JSON.stringify(payload), headers: { Prefer: 'return=minimal' },
      });
    } else {
      await api('/address', { method: 'POST', body: JSON.stringify(payload), headers: { Prefer: 'return=minimal' } });
    }
    closeModal('modal-address');
    await openDetail(detailCounter.counter_id);
    toast('Saved ✓', 'ok');
  } catch (e) { toast('Save failed: ' + e.message, 'err'); }
}·
async function deleteAddress(addressId) {
  if (!confirm('Delete this address?')) return;
  try {
    await api(`/address?address_id=eq.${addressId}`, { method: 'DELETE', headers: { Prefer: 'return=minimal' } });
    await openDetail(detailCounter.counter_id);
    toast('Deleted', 'ok');
  } catch (e) { toast('Delete failed: ' + e.message, 'err'); }
}·
function closeModal(id) { document.getElementById(id).classList.remove('show'); }·
// ══════════════════════════════════════════════════════════
// PHASE 2 — ALLOCATION (per-employee visibility)
// ══════════════════════════════════════════════════════════
let _allEmps    = [];   // active employees from expense.employees
let _allocSet   = new Set();    // selected emp_ids in the modal
let _existingAlloc = new Map(); // emp_id → allocation_id for current counter
let _lastCoverageShareText = ''; // last generated coverage report share text·
async function shareCoverageReport() {
  const text = _lastCoverageShareText;
  if (!text) { toast('Open Coverage Report first', 'info'); return; }
  try {
    await navigator.clipboard.writeText(text);
    toast('Coverage summary copied! Paste into WhatsApp.', 'success');
  } catch {
    // Clipboard API blocked — open WhatsApp directly
    window.open('https://wa.me/?text=' + encodeURIComponent(text), '_blank');
  }
}·
async function quickAllocate(counterId) {
  // Open the allocation screen for a counter directly from the list (no need to open detail first)
  const c = allCounters.find(x => x.counter_id === counterId);
  if (!c) return;
  detailCounter = c;
  await openAllocate();
}·
function openBulkEdit() {
  if (!isSupervisor()) return;
  // Compute scope from current filtered list
  const filtered = _filteredCounters();
  document.getElementById('bulk-scope-count').textContent = filtered.length;
  document.getElementById('bulk-apply-count').textContent = filtered.length;
  document.getElementById('bulk-brand').value       = '';
  document.getElementById('bulk-counter-type').value = '';
  document.getElementById('bulk-territory').value   = '';
  document.getElementById('bulk-channel').value     = '';
  document.getElementById('bulk-status').textContent = '';
  document.getElementById('btn-bulk-apply').disabled = false;
  document.getElementById('modal-bulk-edit').classList.add('show');
}·
// Returns the counters currently visible in the list (same as filterCounters())
function _filteredCounters() { return filterCounters(); }·
async function runBulkEdit() {
  if (!isSupervisor()) return;
  const brandCode   = document.getElementById('bulk-brand').value.trim();
  const counterType = document.getElementById('bulk-counter-type').value;
  const territory   = document.getElementById('bulk-territory').value.trim();
  const channel     = document.getElementById('bulk-channel').value;
  if (!brandCode && !counterType && !territory && !channel) {
    toast('Set at least one field to apply', 'err'); return;
  }
  const targets = _filteredCounters();
  if (!targets.length) { toast('No counters match the current filter', 'err'); return; }
  if (!confirm(`Apply changes to ${targets.length} counter${targets.length !== 1 ? 's' : ''}?\\n\\nThis will:\\n${brandCode ? `• Add brand \"${brandCode}\" (merge with existing)\\n` : ''}${counterType ? `• Set counter type \"${counterType}\" (blank fields only)\\n` : ''}${territory  ? `• Set territory \"${territory}\" (blank fields only)\\n` : ''}${channel    ? `• Set channel \"${channel}\" (blank fields only)\\n` : ''}`)) return;·
  const btn    = document.getElementById('btn-bulk-apply');
  const status = document.getElementById('bulk-status');
  btn.disabled = true;
  status.style.color = 'var(--muted)';
  status.textContent = `⏳ Updating ${targets.length} counters…`;·
  let ok = 0, fail = 0;
  const brandAssocRows = [];
  for (const c of targets) {
    const patch = {};
    // Brand merge
    if (brandCode) {
      const existing = new Set(c.brand_codes || []);
      if (!existing.has(brandCode)) {
        patch.brand_codes = [...existing, brandCode];
        brandAssocRows.push({ counter_id: c.counter_id, brand_code: brandCode, source: 'manual', added_by: myId });
      }
    }
    // Fill-blank fields
    if (counterType && !c.counter_type) patch.counter_type = counterType;
    if (territory  && !c.territory)     patch.territory    = territory;
    if (channel    && !c.channel)       patch.channel      = channel;
    if (!Object.keys(patch).length) { ok++; continue; }
    try {
      await api(`/counter?counter_id=eq.${encodeURIComponent(c.counter_id)}`, {
        method: 'PATCH', body: JSON.stringify(patch), headers: { Prefer: 'return=minimal' },
      });
      ok++;
    } catch (_) { fail++; }
  }
  // Write brand associations
  if (brandAssocRows.length) {
    await api('/counter_brand', {
      method: 'POST', body: JSON.stringify(brandAssocRows),
      headers: { Prefer: 'return=minimal,resolution=ignore-duplicates' },
    }).catch(() => {});
  }
  status.style.color = fail ? 'var(--err)' : '#4ade80';
  status.textContent = fail
    ? `✗ ${ok} updated, ${fail} failed`
    : `✓ ${ok} counter${ok !== 1 ? 's' : ''} updated successfully`;
  btn.disabled = false;
  if (!fail) {
    toast(`Bulk update done — ${ok} counters`, 'ok');
    setTimeout(() => { closeModal('modal-bulk-edit'); loadList(); }, 1500);
  }
}·
async function openCoverageReport(days) {
  days = days || 30;
  document.getElementById('modal-coverage').classList.add('show');
  const out = document.getElementById('coverage-out');
  out.innerHTML = '<div class=\"empty\"><span class=\"spin\">↻</span> Loading…</div>';
  try {
    await loadEmployees();
    const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
    // Fetch allocations AND recce submissions in parallel
    const [allocs, recces] = await Promise.all([
      api('/allocation?select=employee_id,counter_id&limit=5000'),
      fetch(`${DB}/submissions?visit_date=gte.${cutoff}&select=counter_id,submitted_by,visit_date&limit=5000`, {
        headers: { 'Accept-Profile': 'recce', 'Accept': 'application/json' },
      }).then(r => r.ok ? r.json() : []),
    ]);
    // Build visit map: employee_id → Set of counter_ids visited in window
    const visitMap = {};    // employee_id → Map<counter_id, max_visit_date>
    recces.forEach(r => {
      if (!r.submitted_by || !r.counter_id) return;
      if (!visitMap[r.submitted_by]) visitMap[r.submitted_by] = {};
      const cur = visitMap[r.submitted_by][r.counter_id];
      if (!cur || r.visit_date > cur) visitMap[r.submitted_by][r.counter_id] = r.visit_date;
    });
    // Group allocations by employee
    const byEmp = {};
    allocs.forEach(a => {
      if (!byEmp[a.employee_id]) byEmp[a.employee_id] = new Set();
      byEmp[a.employee_id].add(a.counter_id);
    });
    const rows = _allEmps.map(e => {
      const allocCids = byEmp[e.id] ? [...byEmp[e.id]] : [];
      const empVisits = visitMap[e.id] || {};
      const visitedCids = allocCids.filter(cid => empVisits[cid]);
      const unvisitedCounters = allocCids
        .filter(cid => !empVisits[cid])
        .map(cid => allCounters.find(x => x.counter_id === cid))
        .filter(Boolean);
      // Last visit by this rep across all their allocated counters
      const lastVisit = allocCids.reduce((best, cid) => {
        const d = empVisits[cid] || '';
        return d > best ? d : best;
      }, '');
      return { id: e.id, name: e.name, role: e.role, allocated: allocCids.length, visited: visitedCids.length, lastVisit, unvisited: unvisitedCounters };
    }).sort((a, b) => b.allocated - a.allocated);·
    // Build territory-level coverage: territory → {total allocated counters (unique), visited (any rep)}
    const terrMap = {}; // territory → { alloc: Set<counter_id>, visited: Set<counter_id> }
    allocs.forEach(a => {
      const c = allCounters.find(x => x.counter_id === a.counter_id);
      const t = (c?.territory || 'Unknown');
      if (!terrMap[t]) terrMap[t] = { alloc: new Set(), visited: new Set() };
      terrMap[t].alloc.add(a.counter_id);
    });
    // Mark visited: any rep visited this counter in the window
    Object.values(visitMap).forEach(empVisits => {
      Object.keys(empVisits).forEach(cid => {
        const c = allCounters.find(x => x.counter_id === cid);
        const t = (c?.territory || 'Unknown');
        if (terrMap[t]) terrMap[t].visited.add(cid);
      });
    });
    const terrRows = Object.entries(terrMap)
      .map(([t, v]) => ({ territory: t, total: v.alloc.size, visited: v.visited.size }))
      .sort((a, b) => b.total - a.total);·
    const totalAllocs = allocs.length;
    const repCount    = Object.keys(byEmp).length;
    const dayLabels = { 30: '30 days', 60: '60 days', 90: '90 days' };
    const dayBtns = [30, 60, 90].map(d =>
      `<button onclick=\"openCoverageReport(${d})\" style=\"padding:3px 10px;border-radius:12px;border:1px solid var(--border);background:${d===days?'var(--accent)':'var(--card)'};color:${d===days?'#fff':'var(--text)'};font-size:11px;font-weight:600;cursor:pointer;font-family:inherit\">${dayLabels[d]}</button>`
    ).join('');·
    // Pre-build share text so the button works inline
    const shareLines = ['📊 *Counter Coverage Report*', `Last ${days} days · ${new Date().toLocaleDateString('en-IN', { day:'numeric', month:'short', year:'numeric' })}`, ''];
    rows.filter(r => r.allocated > 0).forEach(r => {
      const pct = r.allocated ? Math.round(r.visited / r.allocated * 100) : 0;
      const bar = pct === 100 ? '✅' : pct >= 50 ? '🟡' : '🔴';
      shareLines.push(`${bar} *${r.name}*: ${r.visited}/${r.allocated} (${pct}%)`);
    });
    if (terrRows.length) {
      shareLines.push('', '*By Territory:*');
      terrRows.forEach(tr => {
        const tPct = tr.total ? Math.round(tr.visited / tr.total * 100) : 0;
        const bar = tPct === 100 ? '✅' : tPct >= 50 ? '🟡' : '🔴';
        shareLines.push(`${bar} ${tr.territory}: ${tr.visited}/${tr.total} (${tPct}%)`);
      });
    }
    _lastCoverageShareText = shareLines.join('\\n');·
    out.innerHTML = `
      <div style=\"display:flex;align-items:center;gap:6px;padding:10px 0 8px;flex-wrap:wrap\">
        <span style=\"font-size:11px;color:var(--muted)\">Window:</span>${dayBtns}
        <span style=\"font-size:11px;color:var(--muted);margin-left:auto\">${totalAllocs} alloc${totalAllocs !== 1 ? 's' : ''} · ${repCount} rep${repCount !== 1 ? 's' : ''}</span>
        <button onclick=\"shareCoverageReport()\" title=\"Copy summary to clipboard / share via WhatsApp\"
          style=\"background:none;border:1px solid var(--border);border-radius:8px;padding:3px 8px;font-size:11px;cursor:pointer;color:var(--text);font-family:inherit;display:flex;align-items:center;gap:4px\">
          📤 Share
        </button>
      </div>` +
      rows.map(r => {
        const pct = r.allocated ? Math.round(r.visited / r.allocated * 100) : 0;
        const clr = pct === 100 ? '#22c55e' : pct >= 50 ? '#f59e0b' : '#ef4444';
        const uvHtml = r.unvisited.length
          ? r.unvisited.map(c => {
              const loc = [c.city, c.territory].filter(Boolean).join(', ');
              return `<button onclick=\"closeModal('modal-coverage');openDetail('${esc(c.counter_id)}')\"
                style=\"background:rgba(255,255,255,.07);border:1px solid var(--border);border-radius:8px;padding:4px 10px;font-size:11px;font-weight:600;cursor:pointer;color:var(--fg);font-family:inherit;text-align:left\"
                title=\"${esc(loc)}\">
                ${esc(c.display_name)}${loc ? `<span style=\"font-weight:400;color:var(--muted);margin-left:4px\">${esc(loc)}</span>` : ''}
              </button>`;}).join('')
          : r.allocated
            ? `<span style=\"font-size:11px;color:#22c55e;font-weight:600\">✓ All counters visited this ${days === 30 ? 'month' : days + ' days'}</span>`
            : `<span style=\"font-size:11px;color:var(--muted)\">No allocations</span>`;
        return `
        <div style=\"border-bottom:1px solid var(--border)\">
          <div onclick=\"const uv=this.nextElementSibling;uv.style.display=uv.style.display==='flex'?'none':'flex'\" style=\"display:flex;align-items:center;gap:10px;padding:9px 0;cursor:pointer\">
            <div style=\"flex:1;min-width:0\">
              <div style=\"font-size:13px;font-weight:700\">${esc(r.name)}</div>
              <div style=\"font-size:10.5px;color:var(--muted)\">${esc(r.role || '')}${r.lastVisit ? ' · last Recce ' + daysAgo(r.lastVisit) : ` · no Recces in ${days} days`}</div>
            </div>
            <div style=\"text-align:right;flex-shrink:0\">
              <div style=\"font-size:14px;font-weight:700;color:${clr}\">${r.visited}/${r.allocated}</div>
              <div style=\"font-size:9.5px;color:var(--muted)\">in ${days}d</div>
            </div>
            <div style=\"width:44px;flex-shrink:0\">
              <div style=\"height:5px;background:var(--border);border-radius:3px;overflow:hidden\">
                <div style=\"height:100%;width:${pct}%;background:${clr};border-radius:3px\"></div>
              </div>
              <div style=\"font-size:9px;color:var(--muted);text-align:center;margin-top:2px\">${pct}%</div>
            </div>
          </div>
          <div style=\"display:none;padding:6px 0 10px;flex-wrap:wrap;gap:6px\">
            ${uvHtml}
          </div>
        </div>`;
      }).join('') +
      (terrRows.length ? `
      <div style=\"margin-top:16px;padding-top:12px;border-top:2px solid var(--border)\">
        <div style=\"font-size:11px;font-weight:700;color:var(--muted);letter-spacing:.06em;margin-bottom:8px\">BY TERRITORY</div>
        ${terrRows.map(tr => {
          const tPct = tr.total ? Math.round(tr.visited / tr.total * 100) : 0;
          const tClr = tPct === 100 ? '#22c55e' : tPct >= 50 ? '#f59e0b' : '#ef4444';
          return `<div style=\"display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)\">
            <div style=\"flex:1;font-size:12px;font-weight:600\">${esc(tr.territory)}</div>
            <div style=\"font-size:12px;font-weight:700;color:${tClr};flex-shrink:0\">${tr.visited}/${tr.total}</div>
            <div style=\"width:60px;flex-shrink:0\">
              <div style=\"height:4px;background:var(--border);border-radius:3px;overflow:hidden\">
                <div style=\"height:100%;width:${tPct}%;background:${tClr};border-radius:3px\"></div>
              </div>
              <div style=\"font-size:9px;color:var(--muted);text-align:right;margin-top:1px\">${tPct}%</div>
            </div>
          </div>`;
        }).join('')}
      </div>` : '');
  } catch (e) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⚠️</div><div>Failed: ${esc(e.message)}</div></div>`;
  }
}·
async function loadEmployees() {
  if (_allEmps.length) return _allEmps;
  try {
    _allEmps = await fetch(`${DB}/employees?active=is.true&select=id,name,role&order=name.asc`, {
      headers: { 'Accept-Profile': 'expense', 'Accept': 'application/json' },
    }).then(r => r.ok ? r.json() : []);
  } catch { _allEmps = []; }
  return _allEmps;
}·
async function openAllocate() {
  if (!isSupervisor() || !detailCounter) return;
  await loadEmployees();
  // Fetch existing allocations for this counter
  let existing = [];
  try {
    existing = await api(`/allocation?counter_id=eq.${encodeURIComponent(detailCounter.counter_id)}&select=allocation_id,employee_id,job_id`);
  } catch (e) { /* ignore */ }
  _existingAlloc = new Map();
  _allocSet = new Set();
  existing.forEach(a => {
    _existingAlloc.set(a.employee_id + '__' + (a.job_id || ''), a.allocation_id);
    _allocSet.add(a.employee_id);
  });
  document.getElementById('alloc-job_id').value = '';
  document.getElementById('alloc-ttl').textContent = `👥 Who sees ${detailCounter.display_name}?`;
  renderAllocTiles();
  document.getElementById('modal-alloc').classList.add('show');
}·
function renderAllocTiles() {
  const activeIds  = new Set(_allEmps.map(e => e.id));
  const total      = _allEmps.length;
  const selectedN  = [..._allocSet].filter(id => activeIds.has(id)).length;
  document.getElementById('alloc-summary').textContent =
    `${selectedN} of ${total} employees · ${selectedN === total ? '✓ All' : selectedN === 0 ? 'Nobody' : 'Custom'}`;
  const tiles = document.getElementById('alloc-tiles');
  const allSel = selectedN === _allEmps.length && _allEmps.length > 0;
  // Inactive employees who still have an allocation (can be unchecked to remove)
  const inactiveAllocated = [..._allocSet].filter(id => !activeIds.has(id));
  const inactiveHtml = inactiveAllocated.length
    ? `<div style=\"font-size:10px;color:var(--muted);letter-spacing:.3px;text-transform:uppercase;font-weight:700;padding:8px 0 4px;grid-column:1/-1\">Former team (inactive)</div>` +
      inactiveAllocated.map(id => `
        <div class=\"alloc-tile sel\" style=\"opacity:.6\" onclick=\"toggleAlloc('${esc(id)}')\">
          <span class=\"cb\">☑</span>
          <div><div class=\"nm\">${esc(id)}</div><div class=\"ro\">Inactive · uncheck to remove</div></div>
        </div>`).join('')
    : '';
  tiles.innerHTML = `
    <div class=\"alloc-tile all-tile ${allSel ? 'sel' : ''}\" onclick=\"toggleAllAlloc()\">
      <span class=\"cb\">${allSel ? '✓' : '⌂'}</span>
      <div><div class=\"nm\">All Employees</div><div class=\"ro\">${_allEmps.length} active · click to ${allSel ? 'deselect' : 'select'} all</div></div>
    </div>` +
    _allEmps.map(e => {
      const sel = _allocSet.has(e.id);
      return `
        <div class=\"alloc-tile ${sel ? 'sel' : ''}\" onclick=\"toggleAlloc('${esc(e.id)}')\">
          <span class=\"cb\">${sel ? '☑' : '☐'}</span>
          <div><div class=\"nm\">${esc(e.name)}</div><div class=\"ro\">${esc(e.role || '')}</div></div>
        </div>`;
    }).join('') + inactiveHtml;
}·
function toggleAlloc(empId) {
  if (_allocSet.has(empId)) _allocSet.delete(empId); else _allocSet.add(empId);
  renderAllocTiles();
}·
function toggleAllAlloc() {
  if (_allocSet.size === _allEmps.length) _allocSet = new Set();
  else _allocSet = new Set(_allEmps.map(e => e.id));
  renderAllocTiles();
}·
async function saveAlloc() {
  const counterId = detailCounter.counter_id;
  const jobId     = (document.getElementById('alloc-job_id').value || '').trim() || null;
  // 1. Insert rows for newly-checked employees
  const toAdd = [...(_allocSet)].filter(emp => !_existingAlloc.has(emp + '__' + (jobId || '')));
  // 2. Delete rows for unchecked employees (only matching this job_id scope)
  const toRemove = [...(_existingAlloc.entries())]
    .filter(([key, _]) => {
      const [emp, j] = key.split('__');
      return j === (jobId || '') && !_allocSet.has(emp);
    })
    .map(([_, allocId]) => allocId);
  try {
    if (toAdd.length) {
      const rows = toAdd.map(emp => ({
        employee_id: emp, counter_id: counterId, job_id: jobId,
        allocated_by: myId, source: _allocSet.size === _allEmps.length ? 'all_employees_tile' : 'supervisor_manual',
      }));
      await api('/allocation', {
        method: 'POST', body: JSON.stringify(rows),
        headers: { Prefer: 'return=minimal' },
      });
    }
    if (toRemove.length) {
      const idList = toRemove.join(',');
      await api(`/allocation?allocation_id=in.(${idList})`, {
        method: 'DELETE', headers: { Prefer: 'return=minimal' },
      });
    }
    closeModal('modal-alloc');
    toast(`Allocated to ${_allocSet.size} employee${_allocSet.size !== 1 ? 's' : ''} ✓`, 'ok');
  } catch (e) {
    toast('Allocation failed: ' + e.message, 'err');
  }
}·
// ══════════════════════════════════════════════════════════
// PHASE 4.5 — CLIENTS ADMIN (manage brand-side users)
// ══════════════════════════════════════════════════════════
let _clients         = [];                     // list from client_admin_v
let _clientFilter    = 'all';                  // current filter tile
let _clientUsersMap  = new Map();              // client_id → array of users
let _clientInvitesMap = new Map();             // client_id → array of pending invites
let _expandedClient  = null;                   // currently-expanded client_id
let _inviteClientId  = null;                   // for the invite modal·
function setCFilter(e, val) {
  _clientFilter = val;
  document.querySelectorAll('#cli-tiles .tile').forEach(t =>
    t.classList.toggle('sel', t.dataset.cfilter === val));
  renderClients();
}·
async function openClients() {
  showScreen('s-clients');
  await loadClients();
}·
async function loadClients() {
  document.getElementById('clients-out').innerHTML = '<div class=\"empty\"><span class=\"spin\">↻</span> Loading…</div>';
  try {
    _clients = await api('/client_admin_v?order=is_active.desc,name.asc');
  } catch (e) {
    toast('Load failed: ' + e.message, 'err');
    _clients = [];
  }
  renderClients();
  refreshClientsBadge();
}·
async function refreshClientsBadge() {
  try {
    const r = await fetch(`${DB}/client_admin_v?is_active=eq.true&select=client_id`, {
      headers: { 'Accept-Profile': 'counters', 'Accept': 'application/json' },
    });
    const arr = r.ok ? await r.json() : [];
    const el = document.getElementById('clients-badge');
    if (el) el.textContent = arr.length > 0 ? `Clients (${arr.length})` : 'Clients';
  } catch (_) {}
}·
// ── Phase 4.6 — Notifications ───────────────────────────────────
let _notify     = [];
let _notifyTab  = 'unseen';·
async function openNotify() {
  showScreen('s-notify');
  await loadNotify();
}·
async function loadNotify() {
  const out = document.getElementById('notify-out');
  out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div>`;
  try {
    _notify = await api('/rpc/admin_list_notifications', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_limit: 100 }),
    });
  } catch (e) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⚠️</div><div>Failed to load: ${esc(e.message)}</div></div>`;
    return;
  }
  renderNotify();
  refreshNotifyBadge();
}·
function setNotifyTab(tab) {
  _notifyTab = tab;
  document.getElementById('nf-tab-unseen').classList.toggle('on', tab === 'unseen');
  document.getElementById('nf-tab-all').classList.toggle('on',    tab === 'all');
  renderNotify();
}·
function _notifyMeta(evt) {
  if (evt === 'recce_marked_official')    return { icon: '🏁', title: 'Recce marked official',  chip: 'evt-official' };
  if (evt === 'client_response')          return { icon: '💬', title: 'Client response',        chip: 'evt-response' };
  if (evt === 'magic_link_login')         return { icon: '🔐', title: 'Magic-link login',       chip: '' };
  if (evt === 'suspicious_login')         return { icon: '⚠️', title: 'Suspicious login',       chip: 'evt-response' };
  if (evt === 'supervisor_weekly_digest') return { icon: '📨', title: 'Weekly digest sent',     chip: 'evt-official' };
  if (evt === 'recce_comment')            return { icon: '💬', title: 'Recce comment',          chip: 'evt-response' };
  return { icon: '🔔', title: evt, chip: '' };
}·
function _fmtRel(ts) {
  if (!ts) return '';
  const d = new Date(ts);
  const diff = (Date.now() - d.getTime()) / 1000;
  if (diff < 60)   return Math.floor(diff) + 's ago';
  if (diff < 3600) return Math.floor(diff/60) + 'm ago';
  if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
  return Math.floor(diff/86400) + 'd ago';
}·
function renderNotify() {
  const out = document.getElementById('notify-out');
  const list = _notify.filter(n => _notifyTab === 'all' ? true : n.status !== 'suppressed');
  if (!list.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">📭</div><div>${_notifyTab === 'unseen' ? 'No unseen notifications.' : 'No notifications yet.'}</div></div>`;
    return;
  }
  out.innerHTML = list.map(n => {
    const m   = _notifyMeta(n.event_type);
    const p   = n.payload || {};
    const seen = n.status === 'suppressed';
    let body  = '';
    let chips = '';
    if (n.event_type === 'recce_marked_official') {
      body  = `<b>${esc(p.picked_by || '?')}</b> picked Recce as official for <b>${esc(p.brand || '?')}</b> at counter <b>${esc(p.counter_id || '?')}</b>.`;
      chips = `<span class=\"nf-chip\">${esc(p.brand || '?')}</span><span class=\"nf-chip\">${esc(p.counter_id || '?')}</span>`;
    } else if (n.event_type === 'client_response') {
      body  = `Client marked Recce <b>${esc(p.client_status || '?')}</b> for <b>${esc(p.brand || '?')}</b> at counter <b>${esc(p.counter_id || '?')}</b>.${p.client_comment ? `<br><span style=\"color:var(--muted)\">\"${esc(p.client_comment)}\"</span>` : ''}`;
      chips = `<span class=\"nf-chip\">${esc(p.brand || '?')}</span><span class=\"nf-chip\">${esc(p.client_status || '?')}</span>`;
    } else if (n.event_type === 'magic_link_login') {
      body  = `Sign-in to Recce Portal from <b>${esc(p.ip || 'unknown IP')}</b>.`;
      chips = `<span class=\"nf-chip\">${esc(p.client_user_id || '')}</span>`;
    } else if (n.event_type === 'suspicious_login') {
      const reasons = Array.isArray(p.reasons) ? p.reasons.join(', ') : '';
      body  = `Risk-flagged login for <b>${esc(p.email || p.client_user_id || '?')}</b> from <b>${esc(p.ip || 'unknown IP')}</b>${reasons ? ` · <span style=\"color:var(--muted)\">${esc(reasons)}</span>` : ''}.`;
      chips = `<span class=\"nf-chip ${p.risk === 'high' ? 'evt-response' : ''}\">${esc(p.risk || 'risk')}</span>${p.client_user_id ? `<span class=\"nf-chip\">${esc(p.client_user_id)}</span>` : ''}`;
    } else if (n.event_type === 'recce_comment') {
      const from = p.from === 'supervisor' ? '360 team' : 'Client user';
      body  = `<b>${esc(from)}</b> on Recce <b>${esc(p.sub_id || '?')}</b>: <span style=\"color:var(--muted)\">\"${esc(p.preview || '')}\"</span>`;
      chips = `<span class=\"nf-chip\">${esc(p.brand || '')}</span>${p.author_name ? `<span class=\"nf-chip\">${esc(p.author_name)}</span>` : ''}`;
    } else if (n.event_type === 'supervisor_weekly_digest') {
      const w = p.window || {};
      const li = p.logins || {};
      const rp = p.response || {};
      body  = `Sent weekly digest to <b>${esc(p.employee_id || '?')}</b> · logins ${esc(String(li.total || 0))} (high ${esc(String(li.high || 0))}), responses ${esc(String((rp.approved || 0) + (rp.approved_with_changes || 0) + (rp.rejected || 0)))}, top users ${esc(String(p.top_count || 0))}.`;
      chips = `<span class=\"nf-chip\">${esc(p.employee_id || '')}</span><span class=\"nf-chip\">${esc(String(w.from || '').slice(0, 10))} → ${esc(String(w.to || '').slice(0, 10))}</span>`;
    } else {
      body  = `<pre style=\"white-space:pre-wrap;font-size:10.5px;color:var(--muted);margin:0\">${esc(JSON.stringify(p))}</pre>`;
    }
    return `
      <div class=\"notify-card${seen ? '' : ' unseen'}\">
        <div class=\"nf-top\">
          <span class=\"nf-icon\">${m.icon}</span>
          <div class=\"nf-title\">${esc(m.title)}</div>
          <div class=\"nf-when\">${esc(_fmtRel(n.created_at))}</div>
        </div>
        <div class=\"nf-chips\">
          <span class=\"nf-chip ${m.chip}\">${esc(n.event_type)}</span>
          ${chips}
          ${seen ? '<span class=\"nf-chip status-suppressed\">seen</span>' : ''}
        </div>
        <div class=\"nf-body\">${body}</div>
        <div class=\"nf-actions\">
          ${seen ? '' : `<button class=\"seen\" onclick=\"markNotifySeen(${n.notify_id})\">Mark seen</button>`}
        </div>
      </div>`;
  }).join('');
}·
async function markNotifySeen(id) {
  try {
    const j = await api('/rpc/admin_mark_notification_seen', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_notify_id: id }),
    });
    if (!j?.ok) throw new Error(j?.error || 'failed');
    const row = _notify.find(n => Number(n.notify_id) === Number(id));
    if (row) row.status = 'suppressed';
    renderNotify();
    refreshNotifyBadge();
  } catch (e) { toast('Could not mark seen: ' + e.message, 'err'); }
}·
async function markAllNotifySeen() {
  const unseen = _notify.filter(n => n.status !== 'suppressed');
  if (!unseen.length) { toast('Nothing to mark', 'ok'); return; }
  if (!confirm(`Mark ${unseen.length} notification(s) as seen?`)) return;
  for (const n of unseen) {
    try {
      await api('/rpc/admin_mark_notification_seen', {
        method: 'POST',
        body:   JSON.stringify({ p_supervisor_id: myId, p_notify_id: n.notify_id }),
      });
      n.status = 'suppressed';
    } catch (_) {}
  }
  renderNotify();
  refreshNotifyBadge();
  toast(`Marked ${unseen.length} seen`, 'ok');
}·
async function refreshNotifyBadge() {
  try {
    const rows = await api('/rpc/admin_list_notifications', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_limit: 100 }),
    });
    const unseen = (rows || []).filter(n => n.status !== 'suppressed').length;
    const el = document.getElementById('notify-badge');
    if (el) el.textContent = unseen > 0 ? `Notify (${unseen})` : 'Notify';
    const btn = document.getElementById('btn-notify');
    if (btn) btn.style.borderColor = unseen > 0 ? 'var(--accent)' : '';
  } catch (_) {}
}·
// ── Phase 4.7 — Audit log ──────────────────────────────────────
let _audit            = [];
let _auditDebounce    = null;·
async function openAudit() {
  showScreen('s-audit');
  await loadAudit();
}·
function loadAuditDebounced() {
  clearTimeout(_auditDebounce);
  _auditDebounce = setTimeout(loadAudit, 350);
}·
async function loadAudit() {
  const out = document.getElementById('audit-out');
  out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div>`;
  const action = (document.getElementById('audit-action').value || '').trim() || null;
  const user   = (document.getElementById('audit-user').value   || '').trim() || null;
  const fromD  = (document.getElementById('audit-from').value   || '').trim();
  const toD    = (document.getElementById('audit-to').value     || '').trim();
  try {
    _audit = await api('/rpc/admin_list_audit', {
      method: 'POST',
      body:   JSON.stringify({
        p_supervisor_id:  myId,
        p_limit:          fromD || toD ? 5000 : 200,
        p_client_user_id: user,
        p_action:         action,
        p_from:           fromD ? new Date(fromD + 'T00:00:00').toISOString() : null,
        p_to:             toD   ? new Date(toD   + 'T23:59:59').toISOString() : null,
      }),
    });
  } catch (e) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⚠️</div><div>Failed: ${esc(e.message)}</div></div>`;
    return;
  }
  renderAudit();
  refreshAuditBadge();
}·
// Phase 4.12 — CSV export of currently-loaded audit rows
function exportAuditCsv() {
  if (!_audit.length) { toast('Nothing to export', 'err'); return; }
  const cols = ['created_at','action','client_user_email','client_user_id','client_id','sub_id','ip','user_agent','detail'];
  const esc = v => `\"${String(v == null ? '' : v).replace(/\"/g, '\"\"')}\"`;
  const lines = [cols.join(',')];
  for (const r of _audit) {
    lines.push(cols.map(c => esc(c === 'detail' ? JSON.stringify(r.detail || {}) : r[c])).join(','));
  }
  const blob = new Blob([lines.join('\\n')], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = `audit_${new Date().toISOString().slice(0, 10)}.csv`;
  document.body.appendChild(a); a.click(); a.remove();
  toast(`Exported ${_audit.length} rows`, 'ok');
}·
async function pruneAuditNow() {
  if (!confirm('Delete audit rows older than 180 days? Cannot be undone.')) return;
  try {
    const j = await api('/rpc/admin_prune_audit', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_days: 180 }),
    });
    if (!j?.ok) throw new Error(j?.error || 'failed');
    toast(`Pruned ${j.deleted_count} row(s)`, 'ok');
    await loadAudit();
  } catch (e) { toast('Prune failed: ' + e.message, 'err'); }
}·
// ── Phase 4.17 — supervisor comment thread ──────────────────
let _commentSubId = null;·
async function openCommentsModal(subId, brand) {
  _commentSubId = subId;
  document.getElementById('comments-sub-label').textContent = `${subId.slice(0, 22)}${subId.length > 22 ? '…' : ''}${brand ? ` (${brand})` : ''}`;
  document.getElementById('comments-out').innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div>`;
  document.getElementById('comment-body').value = '';
  document.getElementById('modal-comments').classList.add('show');
  await loadCommentsModal();
}·
async function loadCommentsModal() {
  if (!_commentSubId) return;
  let rows = [];
  try {
    rows = await api('/rpc/list_comments_for_sub', {
      method: 'POST',
      body:   JSON.stringify({
        p_session_id:    null,
        p_supervisor_id: myId,
        p_sub_id:        _commentSubId,
      }),
    });
  } catch (e) {
    document.getElementById('comments-out').innerHTML = `<div class=\"empty\">⚠️ ${esc(e.message)}</div>`;
    return;
  }
  const out = document.getElementById('comments-out');
  if (!rows.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">💬</div><div>No messages yet. Start the conversation.</div></div>`;
    return;
  }
  out.innerHTML = rows.map(r => `
    <div class=\"cmt-msg ${esc(r.author_type)}\">
      <div class=\"ch\">
        <span>${esc(r.author_type === 'supervisor' ? ('360 · ' + (r.author_name || r.author_id || 'team')) : (r.author_name || r.author_id || 'Client'))}</span>
        <span>${esc(_fmtRel(r.created_at))}</span>
      </div>
      <div class=\"cb\">${esc(r.body)}</div>
    </div>
  `).join('');
}·
async function postSupervisorComment() {
  const body = (document.getElementById('comment-body').value || '').trim();
  if (!body) { toast('Type a message first', 'err'); return; }
  if (!_commentSubId) return;
  const btn = document.getElementById('comment-post');
  btn.disabled = true; btn.textContent = 'Posting…';
  try {
    const j = await api('/rpc/add_comment_as_supervisor', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_sub_id: _commentSubId, p_body: body }),
    });
    if (!j?.ok) throw new Error(j?.error || 'failed');
    document.getElementById('comment-body').value = '';
    toast('Posted ✓', 'ok');
    await loadCommentsModal();
  } catch (e) { toast('Failed: ' + e.message, 'err'); }
  finally {
    btn.disabled = false; btn.textContent = 'Post reply';
  }
}·
// ── Phase 4.14 — audit stats modal ──────────────────────────
async function openAuditStats() {
  const fromD = (document.getElementById('audit-from').value || '').trim();
  const toD   = (document.getElementById('audit-to').value   || '').trim();
  document.getElementById('stats-window').textContent =
    fromD || toD ? `${fromD || '…'} → ${toD || '…'}` : 'last 7 days';
  document.getElementById('stats-out').innerHTML =
    `<div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div>`;
  document.getElementById('modal-stats').classList.add('show');
  let j;
  try {
    j = await api('/rpc/admin_audit_stats', {
      method: 'POST',
      body:   JSON.stringify({
        p_supervisor_id: myId,
        p_from: fromD ? new Date(fromD + 'T00:00:00').toISOString() : null,
        p_to:   toD   ? new Date(toD   + 'T23:59:59').toISOString() : null,
      }),
    });
  } catch (e) {
    document.getElementById('stats-out').innerHTML =
      `<div class=\"empty\">⚠️ ${esc(e.message)}</div>`;
    return;
  }
  if (!j?.ok) {
    document.getElementById('stats-out').innerHTML =
      `<div class=\"empty\">⚠️ ${esc(j?.error || 'failed')}</div>`;
    return;
  }
  renderAuditStats(j);
}·
function _bar(parts, total) {
  if (!total) return '<div style=\"font-size:11px;color:var(--muted);margin-top:6px\">No data in window.</div>';
  return `<div class=\"bar\">${
    parts.map(([cls, n]) => `<div class=\"${cls}\" style=\"width:${(n / total * 100).toFixed(1)}%\"></div>`).join('')
  }</div>`;
}·
function renderAuditStats(j) {
  const lr = j.logins_by_risk || { low: 0, medium: 0, high: 0, total: 0 };
  const rm = j.response_mix    || { approved: 0, rejected: 0, approved_with_changes: 0, pending: 0, total: 0 };
  const top = Array.isArray(j.top_users) ? j.top_users : [];
  const delay = j.avg_response_delay_hours;
  const delayTxt = delay == null ? '—' : `${Number(delay).toFixed(1)}h`;
  const out = document.getElementById('stats-out');
  out.innerHTML = `
    <div class=\"stat-grid\">
      <div class=\"stat-card\">
        <h4>Logins by risk</h4>
        <div class=\"big\">${lr.total || 0}</div>
        <div class=\"sub\">low ${lr.low || 0} · med ${lr.medium || 0} · high ${lr.high || 0}</div>
        ${_bar([['low', lr.low || 0], ['medium', lr.medium || 0], ['high', lr.high || 0]], lr.total || 0)}
        <div class=\"legend\">
          <span style=\"color:#22c55e\">low</span>
          <span style=\"color:#eab308\">medium</span>
          <span style=\"color:#ef4444\">high</span>
        </div>
      </div>
      <div class=\"stat-card\">
        <h4>Avg response delay</h4>
        <div class=\"big\">${esc(delayTxt)}</div>
        <div class=\"sub\">Recce official → client response (window)</div>
      </div>
      <div class=\"stat-card full\">
        <h4>Response mix (all official Recces)</h4>
        <div class=\"sub\">approved ${rm.approved || 0} · w/ changes ${rm.approved_with_changes || 0} · rejected ${rm.rejected || 0} · pending ${rm.pending || 0}</div>
        ${_bar([
          ['approved',              rm.approved || 0],
          ['approved_with_changes', rm.approved_with_changes || 0],
          ['rejected',              rm.rejected || 0],
          ['pending',               rm.pending || 0],
        ], rm.total || 0)}
        <div class=\"legend\">
          <span style=\"color:#22c55e\">approved</span>
          <span style=\"color:#eab308\">w/ changes</span>
          <span style=\"color:#ef4444\">rejected</span>
          <span style=\"color:#6b7280\">pending</span>
        </div>
      </div>
      <div class=\"stat-card full\">
        <h4>Top active users (window)</h4>
        ${top.length === 0
          ? '<div class=\"sub\">No activity in window.</div>'
          : top.slice(0, 5).map(u => `
              <div class=\"top-user\">
                <div class=\"tu-email\">${esc(u.email || u.client_user_id)}</div>
                <div class=\"tu-count\">👁 ${u.n_views || 0} · 💬 ${u.n_responses || 0}</div>
              </div>`).join('')
        }
      </div>
    </div>`;
}·
function _auditMeta(action) {
  if (action === 'login')           return { icon: '🔐', cls: 'login',    label: 'login'    };
  if (action === 'recce_view')      return { icon: '👁',  cls: 'view',     label: 'view'     };
  if (action === 'response_submit') return { icon: '💬', cls: 'response', label: 'response' };
  return { icon: '•', cls: '', label: action };
}·
function renderAudit() {
  const out = document.getElementById('audit-out');
  const riskFilter = (document.getElementById('audit-risk')?.value || '').trim();
  const filtered = _audit.filter(r => {
    if (!riskFilter) return true;
    const lvl = r.action === 'login' ? (r.detail?.risk || 'low') : null;
    if (riskFilter === 'high')   return lvl === 'high';
    if (riskFilter === 'medium') return lvl === 'medium' || lvl === 'high';
    return true;
  });
  if (!filtered.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">📋</div><div>No audit rows match.</div></div>`;
    return;
  }
  out.innerHTML = filtered.map(r => {
    const m = _auditMeta(r.action);
    const d = r.detail || {};
    let body = '';
    let riskChip = '';
    if (r.action === 'login') {
      const risk = d.risk || 'low';
      const reasons = Array.isArray(d.reasons) ? d.reasons.join(', ') : '';
      riskChip = `<span class=\"au-risk ${esc(risk)}\">${esc(risk)}${reasons ? ' · ' + esc(reasons) : ''}</span>`;
      body = `Magic-link login. Invite: <b>${esc(d.invite_id || '?')}</b>.`;
    } else if (r.action === 'recce_view') {
      body = `Opened Recce <b>${esc(r.sub_id || '?')}</b> in viewer.`;
    } else if (r.action === 'response_submit') {
      body = `Marked Recce <b>${esc(r.sub_id || '?')}</b> as <b>${esc(d.new_status || '?')}</b>${d.prev_status && d.prev_status !== d.new_status ? ` <span style=\"color:var(--muted)\">(was ${esc(d.prev_status)})</span>` : ''}${d.has_comment ? ' · 💬 with comment' : ''}${d.bulk_id ? ` · <span style=\"color:var(--muted)\">bulk ${esc(d.bulk_id.slice(0, 10))}…</span>` : ''}.`;
    } else if (r.action === 'session_revoke') {
      const by = d.by === 'supervisor' ? `supervisor ${esc(d.supervisor_id || '?')}` : 'self';
      body = d.scope === 'all_others'
        ? `Revoked ${esc(String(d.revoked_count || 0))} other session(s) (${by}).`
        : `Revoked session ${esc((d.target_session_id || '').slice(0, 12))}… (${by}).`;
    }
    return `
      <div class=\"audit-row\">
        <div class=\"au-top\">
          <span class=\"au-icon\">${m.icon}</span>
          <span class=\"au-action ${m.cls}\">${esc(m.label)}</span>
          ${riskChip}
          <span class=\"au-user\">${esc(r.client_user_email || r.client_user_id)}</span>
          <span class=\"au-when\">${esc(_fmtRel(r.created_at))}</span>
        </div>
        <div class=\"au-body\">${body}</div>
        <div class=\"au-meta\">
          ${r.client_user_name ? `<span>${esc(r.client_user_name)}</span>` : ''}
          ${r.client_id ? `<span>🏷 ${esc(r.client_id)}</span>` : ''}
          ${r.ip ? `<span>📡 ${esc(r.ip)}</span>` : ''}
          ${r.user_agent ? `<span title=\"${esc(r.user_agent)}\">🖥 ${esc(r.user_agent.slice(0, 32))}${r.user_agent.length > 32 ? '…' : ''}</span>` : ''}
        </div>
      </div>`;
  }).join('');
}·
async function refreshAuditBadge() {
  try {
    const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
    const r = await fetch(`${DB}/client_audit_log?created_at=gte.${since}&select=audit_id`, {
      headers: { 'Accept-Profile': 'counters', 'Accept': 'application/json' },
    });
    const arr = r.ok ? await r.json() : [];
    const el = document.getElementById('audit-badge');
    if (el) el.textContent = arr.length > 0 ? `Audit (${arr.length})` : 'Audit';
  } catch (_) {}
}·
function renderClients() {
  const active   = _clients.filter(c =>  c.is_active);
  const inactive = _clients.filter(c => !c.is_active);
  const withUsers = _clients.filter(c => c.active_user_count > 0);
  const setTxt = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
  setTxt('cf-all',      _clients.length);
  setTxt('cf-active',   active.length);
  setTxt('cf-inactive', inactive.length);
  setTxt('cf-users',    withUsers.length);
  document.getElementById('clients-count').textContent =
    `${active.length} active · ${_clients.length} total`;·
  const q = (document.getElementById('cli-srch')?.value || '').toLowerCase().trim();
  let list = _clientFilter === 'active'   ? active
           : _clientFilter === 'inactive' ? inactive
           : _clientFilter === 'users'    ? withUsers
           : _clients;
  if (q) list = list.filter(c =>
    c.name.toLowerCase().includes(q) ||
    (c.brand_codes || []).some(b => b.toLowerCase().includes(q)));·
  const out = document.getElementById('clients-out');
  if (!list.length) {
    out.innerHTML = q || _clientFilter !== 'all'
      ? `<div class=\"empty\"><div class=\"empty-icon\">🔍</div><div>No clients match. Try a different search or filter.</div></div>`
      : `<div class=\"empty\"><div class=\"empty-icon\">🏷</div><div>No clients yet. Tap + Client to add one.</div></div>`;
    return;
  }
  out.innerHTML = list.map(c => {
    const brandsTxt = (c.brand_codes || []).slice(0, 4).join(', ') + ((c.brand_codes || []).length > 4 ? '…' : '');
    return `
      <div class=\"cli-card\" id=\"cli-card-${esc(c.client_id)}\">
        <div class=\"cli-top\" onclick=\"toggleClient('${esc(c.client_id)}')\">
          <div class=\"cli-name\">
            <span class=\"name\">${esc(c.name)}${c.is_active ? '' : ' (inactive)'}</span>
            <div class=\"meta\">
              ${brandsTxt ? `<span class=\"cli-meta-chip\">${esc(brandsTxt)}</span>` : ''}
              <span class=\"cli-meta-chip users\">${c.active_user_count} user${c.active_user_count !== 1 ? 's' : ''}</span>
              ${c.pending_invite_count > 0 ? `<span class=\"cli-meta-chip pending\">${c.pending_invite_count} pending</span>` : ''}
              ${c.revoked_user_count > 0 ? `<span class=\"cli-meta-chip inactive\">${c.revoked_user_count} revoked</span>` : ''}
            </div>
          </div>
        </div>
        <div class=\"cli-body\" id=\"cli-body-${esc(c.client_id)}\"></div>
      </div>`;
  }).join('');
}·
async function toggleClient(clientId) {
  const card = document.getElementById('cli-card-' + clientId);
  if (!card) return;
  const wasOpen = card.classList.contains('expanded');
  document.querySelectorAll('.cli-card.expanded').forEach(c => c.classList.remove('expanded'));
  if (wasOpen) { _expandedClient = null; return; }
  card.classList.add('expanded');
  _expandedClient = clientId;
  await loadClientDetail(clientId);
}·
async function loadClientDetail(clientId) {
  const body = document.getElementById('cli-body-' + clientId);
  if (!body) return;
  body.innerHTML = '<div class=\"empty\" style=\"padding:14px\"><span class=\"spin\">↻</span> Loading users + invites…</div>';
  try {
    const [users, invites] = await Promise.all([
      api('/rpc/admin_list_client_users', {
        method: 'POST',
        body:   JSON.stringify({ p_supervisor_id: myId, p_client_id: clientId }),
      }),
      api('/rpc/admin_list_invites', {
        method: 'POST',
        body:   JSON.stringify({ p_supervisor_id: myId, p_client_id: clientId }),
      }),
    ]);
    _clientUsersMap.set(clientId, users || []);
    _clientInvitesMap.set(clientId, invites || []);
    renderClientDetail(clientId);
  } catch (e) {
    body.innerHTML = `<div class=\"empty\" style=\"padding:14px;color:var(--r)\">Load failed: ${esc(e.message)}</div>`;
  }
}·
function renderClientDetail(clientId) {
  const users   = _clientUsersMap.get(clientId)   || [];
  const invites = _clientInvitesMap.get(clientId) || [];
  const body = document.getElementById('cli-body-' + clientId);
  if (!body) return;
  const usersHtml = users.length
    ? users.map(u => `
        <div class=\"cli-user ${u.is_active ? '' : 'revoked'}\">
          <div class=\"u-main\">
            <div class=\"u-email\">${esc(u.email)}${u.display_name && u.display_name !== u.email ? ' · ' + esc(u.display_name) : ''}</div>
            <div class=\"u-meta\">
              <span class=\"role\">${esc(u.role || 'Viewer')}</span>
              <span class=\"scope\">${esc(u.scope_kind || 'all')}${u.scope_values && u.scope_values.length ? ' · ' + esc(u.scope_values.join(',').slice(0, 30)) : ''}</span>
              ${u.last_login_at ? `<span>last login ${esc(fmtDate(u.last_login_at))}</span>` : '<span>never logged in</span>'}
              ${u.is_active ? '' : '<span style=\"color:#fca5a5\">REVOKED</span>'}
            </div>
          </div>
          <div class=\"actions\">
            <button onclick=\"openUserSessions('${esc(u.client_user_id)}', '${esc(u.email)}')\">🔑 Sessions</button>
            ${u.is_active
              ? `<button class=\"danger\" onclick=\"revokeClientUser('${esc(u.client_user_id)}', '${esc(clientId)}', '${esc(u.email)}')\">Revoke</button>`
              : `<button onclick=\"reactivateClientUser('${esc(u.client_user_id)}', '${esc(clientId)}')\">Re-activate</button>`}
          </div>
        </div>`).join('')
    : `<div class=\"empty\" style=\"padding:14px\">No users yet.</div>`;·
  const invitesHtml = invites.length
    ? invites.map(i => `
        <div class=\"cli-invite\">
          <span>✉ ${esc(i.email)} — ${esc(i.role)} · expires ${esc(fmtDate(i.expires_at))}</span>
          <div class=\"actions\">
            <button onclick=\"revokeInvite('${esc(i.invite_id)}', '${esc(clientId)}', '${esc(i.email)}')\">Revoke</button>
          </div>
        </div>`).join('')
    : '';·
  body.innerHTML = `
    <div class=\"cli-section-title\">Users (${users.length})</div>
    ${usersHtml}
    ${invites.length ? `<div class=\"cli-section-title\">Pending invites (${invites.length})</div>${invitesHtml}` : ''}
    <div class=\"cli-row-actions\">
      <button onclick=\"closeClientDetail()\">Collapse</button>
      <button class=\"invite\" onclick=\"openInvite('${esc(clientId)}')\">✉ Invite user</button>
    </div>`;
}·
function closeClientDetail() {
  document.querySelectorAll('.cli-card.expanded').forEach(c => c.classList.remove('expanded'));
  _expandedClient = null;
}·
// ── New Client ──────────────────────────────────────────────
function openNewClient() {
  if (!isSupervisor()) return;
  ['ncf-name','ncf-brand_codes','ncf-contact_name','ncf-contact_email','ncf-notes']
    .forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
  document.getElementById('modal-newclient').classList.add('show');
}·
async function saveNewClient() {
  const name = (document.getElementById('ncf-name').value || '').trim();
  if (!name) { toast('Name required', 'err'); return; }
  const brand_codes = (document.getElementById('ncf-brand_codes').value || '')
    .split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
  const clientId = 'cli-' + slugify(name) + '-' + Math.floor(Math.random() * 0xffff).toString(36);
  const payload = {
    client_id: clientId,
    name,
    brand_codes,
    contact_name:  trim('ncf-contact_name'),
    contact_email: trim('ncf-contact_email'),
    notes:         trim('ncf-notes'),
    created_by:    myId,
  };
  try {
    await api('/client', {
      method:  'POST',
      body:    JSON.stringify(payload),
      headers: { Prefer: 'return=minimal' },
    });
    closeModal('modal-newclient');
    await loadClients();
    toast('Client created ✓', 'ok');
  } catch (e) { toast('Create failed: ' + e.message, 'err'); }
}·
// ── Invite User ─────────────────────────────────────────────
function openInvite(clientId) {
  if (!isSupervisor()) return;
  _inviteClientId = clientId;
  ['invf-email','invf-display_name','invf-scope_values'].forEach(id => { document.getElementById(id).value = ''; });
  document.getElementById('invf-role').value = 'Viewer';
  document.getElementById('invf-scope_kind').value = 'all';
  const c = _clients.find(x => x.client_id === clientId);
  document.getElementById('invite-ttl').textContent = `✉ Invite user · ${c?.name || clientId}`;
  document.getElementById('modal-invite').classList.add('show');
}·
async function saveInvite() {
  const email = (document.getElementById('invf-email').value || '').trim();
  if (!email || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
    toast('Please enter a valid email', 'err'); return;
  }
  const scope_kind = document.getElementById('invf-scope_kind').value;
  const raw_values = (document.getElementById('invf-scope_values').value || '').trim();
  const scope_values = scope_kind === 'all' ? []
    : raw_values.split(',').map(s => s.trim()).filter(Boolean);
  try {
    const j = await api('/rpc/invite_client_user', {
      method: 'POST',
      body: JSON.stringify({
        p_email:         email,
        p_client_id:     _inviteClientId,
        p_role:          document.getElementById('invf-role').value,
        p_scope_kind:    scope_kind,
        p_scope_values:  scope_values,
        p_display_name:  trim('invf-display_name'),
        p_invited_by:    myId,
      }),
    });
    if (!j?.ok) throw new Error(j?.error || 'unknown');
    closeModal('modal-invite');
    toast(`Invite queued — mail worker delivers within 30s`, 'ok');
    if (_expandedClient === _inviteClientId) await loadClientDetail(_inviteClientId);
    refreshClientsBadge();
  } catch (e) { toast('Invite failed: ' + e.message, 'err'); }
}·
// ── Phase 4.8 — sessions modal (supervisor) ───────────────────
let _sessionsForUser = null;·
async function openUserSessions(cuId, email) {
  _sessionsForUser = cuId;
  document.getElementById('sess-ttl').textContent = `🔑 Sessions — ${email}`;
  document.getElementById('sess-modal-out').innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">⏳</div><div>Loading…</div></div>`;
  document.getElementById('modal-sessions').classList.add('show');
  await loadUserSessions();
}·
async function loadUserSessions() {
  if (!_sessionsForUser) return;
  let rows, invites = [];
  try {
    const [sRows, iRows] = await Promise.all([
      api('/rpc/admin_list_sessions', {
        method: 'POST',
        body:   JSON.stringify({ p_supervisor_id: myId, p_client_user_id: _sessionsForUser, p_limit: 50 }),
      }),
      api('/rpc/admin_list_invites', {
        method: 'POST',
        body:   JSON.stringify({ p_supervisor_id: myId, p_client_id: null, p_client_user_id: _sessionsForUser }),
      }),
    ]);
    rows = sRows; invites = iRows || [];
  } catch (e) {
    document.getElementById('sess-modal-out').innerHTML = `<div class=\"empty\">⚠️ ${esc(e.message)}</div>`;
    return;
  }
  const active = rows.filter(r => r.is_active).length;
  const out = document.getElementById('sess-modal-out');
  const invitesHtml = invites.length
    ? `<div class=\"cli-section-title\" style=\"margin-top:8px\">Pending invites (${invites.length})</div>` +
      invites.map(i => `
        <div class=\"cli-invite\">
          <span>✉ ${esc(i.email)} — issued ${esc(_fmtRel(i.issued_at))} · expires ${esc(new Date(i.expires_at).toLocaleString('en-IN'))} · by ${esc(i.invited_by || '?')}</span>
          <div class=\"actions\">
            <button onclick=\"adminRevokePendingInvite('${esc(i.invite_id)}')\">Revoke</button>
          </div>
        </div>`).join('')
    : '';
  if (!rows.length && !invites.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">🔑</div><div>No sessions or pending invites.</div></div>`;
    return;
  }
  if (!rows.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">🔑</div><div>No sessions yet.</div></div>${invitesHtml}`;
    return;
  }
  out.innerHTML = `
    <div style=\"font-size:11.5px;color:var(--muted);margin-bottom:8px\">${active} active · ${rows.length} total (last 50)</div>
    ` + rows.map(r => {
      const status = r.is_active
        ? '<span class=\"cli-meta-chip users\">ACTIVE</span>'
        : (r.revoked_at
            ? '<span class=\"cli-meta-chip inactive\">REVOKED</span>'
            : '<span class=\"cli-meta-chip pending\">EXPIRED</span>');
      return `
        <div class=\"cli-user${r.is_active ? '' : ' revoked'}\" style=\"flex-direction:column;align-items:stretch\">
          <div style=\"display:flex;gap:6px;align-items:center;margin-bottom:2px\">
            ${status}
            <span style=\"font-size:10.5px;color:var(--muted);margin-left:auto\">${esc(r.session_id.slice(0, 14))}…</span>
          </div>
          <div style=\"font-size:11.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">${esc((r.user_agent || 'Unknown device').slice(0, 70))}</div>
          <div class=\"u-meta\">
            ${r.ip ? `<span>📡 ${esc(r.ip)}</span>` : ''}
            <span>Issued ${esc(_fmtRel(r.issued_at))}</span>
            <span>Last used ${esc(r.last_used_at ? _fmtRel(r.last_used_at) : '—')}</span>
            <span>${r.revoked_at ? `Revoked ${esc(_fmtRel(r.revoked_at))}` : `Expires ${esc(new Date(r.expires_at).toLocaleDateString('en-IN'))}`}</span>
          </div>
          ${r.is_active ? `
            <div class=\"actions\" style=\"justify-content:flex-end;margin-top:6px\">
              <button class=\"danger\" onclick=\"adminRevokeSession('${esc(r.session_id)}')\">Revoke session</button>
            </div>` : ''}
        </div>`;
    }).join('') + invitesHtml;
}·
async function adminRevokePendingInvite(inviteId) {
  if (!confirm('Revoke this pending invite? The link in the email will stop working.')) return;
  try {
    const j = await api('/rpc/admin_revoke_invite', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_invite_id: inviteId }),
    });
    if (!j?.ok) throw new Error(j?.error || 'unknown');
    toast('Invite revoked', 'ok');
    await loadUserSessions();
  } catch (e) { toast('Revoke failed: ' + e.message, 'err'); }
}·
async function adminRevokeSession(sid) {
  if (!confirm('Revoke this session? That device will be signed out.')) return;
  try {
    const j = await api('/rpc/admin_revoke_session', {
      method: 'POST',
      body:   JSON.stringify({ p_supervisor_id: myId, p_session_id: sid }),
    });
    if (!j?.ok) throw new Error(j?.error || 'unknown');
    toast('Session revoked', 'ok');
    await loadUserSessions();
  } catch (e) { toast('Revoke failed: ' + e.message, 'err'); }
}·
async function revokeClientUser(cuId, clientId, email) {
  if (!confirm(`Revoke access for ${email}? Any active sessions will be killed; they will not be able to log in.`)) return;
  try {
    const j = await api('/rpc/admin_revoke_client_user', {
      method: 'POST',
      body: JSON.stringify({ p_supervisor_id: myId, p_client_user_id: cuId }),
    });
    if (!j?.ok) throw new Error(j?.error || 'unknown');
    toast(`Revoked ✓${j.sessions_revoked ? ` (${j.sessions_revoked} session${j.sessions_revoked !== 1 ? 's' : ''} killed)` : ''}`, 'ok');
    await loadClientDetail(clientId);
    refreshClientsBadge();
  } catch (e) { toast('Revoke failed: ' + e.message, 'err'); }
}·
async function reactivateClientUser(cuId, clientId) {
  if (!confirm('Re-activate this user? They will need to request a fresh login link.')) return;
  try {
    await api('/rpc/admin_reactivate_client_user', {
      method: 'POST',
      body: JSON.stringify({ p_supervisor_id: myId, p_client_user_id: cuId }),
    });
    toast('Re-activated', 'ok');
    await loadClientDetail(clientId);
    refreshClientsBadge();
  } catch (e) { toast('Re-activate failed: ' + e.message, 'err'); }
}·
async function revokeInvite(inviteId, clientId, email) {
  if (!confirm(`Revoke the pending invite for ${email}? The magic link in their email will stop working.`)) return;
  try {
    const j = await api('/rpc/admin_revoke_invite', {
      method: 'POST',
      body: JSON.stringify({ p_supervisor_id: myId, p_invite_id: inviteId }),
    });
    if (!j?.ok) throw new Error('not_found_or_already_used');
    toast('Invite revoked', 'ok');
    await loadClientDetail(clientId);
    refreshClientsBadge();
  } catch (e) { toast('Revoke failed: ' + e.message, 'err'); }
}·
// ══════════════════════════════════════════════════════════
// PHASE 3 — MULTI-RECCE REVIEW (supervisor picks \"official\")
// ══════════════════════════════════════════════════════════
let _mrgGroups = [];
let _mrgRecces = new Map();   // group_key (counter_id||group_key) → recce rows·
async function openMultiRecce() {
  showScreen('s-multirecce');
  await loadMultiRecce();
}·
async function loadMultiRecce() {
  document.getElementById('mrg-out').innerHTML = '<div class=\"empty\"><span class=\"spin\">↻</span> Loading…</div>';
  try {
    _mrgGroups = await api('/multi_recce_groups_v?order=last_visit.desc&limit=300');
  } catch (e) {
    toast('Load failed: ' + e.message, 'err');
    _mrgGroups = [];
  }
  renderMultiRecce();
  refreshMultiRecceBadge();
}·
async function refreshMultiRecceBadge() {
  try {
    const r = await fetch(`${DB}/multi_recce_groups_v?select=counter_id`, {
      headers: { 'Accept-Profile': 'counters', 'Accept': 'application/json' },
    });
    const arr = r.ok ? await r.json() : [];
    const el = document.getElementById('multirecce-badge');
    if (el) el.textContent = arr.length > 0 ? `Multi-Recce (${arr.length})` : 'Multi-Recce';
  } catch (_) {}
}·
function renderMultiRecce() {
  document.getElementById('mrg-count').textContent = `${_mrgGroups.length} group${_mrgGroups.length !== 1 ? 's' : ''}`;
  const out = document.getElementById('mrg-out');
  if (!_mrgGroups.length) {
    out.innerHTML = '<div class=\"empty\"><div class=\"empty-icon\">📌</div><div>No multi-Recce groups</div><div style=\"font-size:11px;margin-top:4px\">A group forms when 2+ Recces are submitted against the same counter + job_id.</div></div>';
    return;
  }
  out.innerHTML = _mrgGroups.map(g => {
    const hasOfficial = g.official_count > 0;
    const groupId = g.counter_id + '||' + g.group_key;
    return `
      <div class=\"mrg-row\" id=\"mrg-${cssEsc(groupId)}\">
        <div class=\"mrg-top\" onclick=\"toggleMrg('${esc(groupId)}')\">
          <div style=\"flex:1;min-width:0\">
            <div class=\"mrg-name\">${esc(g.display_name)}</div>
            <div class=\"mrg-meta\">
              ${g.city ? `<span>📍 ${esc(g.city)}</span>` : ''}
              <span>Group: ${esc(g.group_key)}</span>
              <span>${g.recce_count} Recces</span>
              <span>Last ${esc(fmtDate(g.last_visit))}</span>
            </div>
          </div>
          <button onclick=\"event.stopPropagation();openDetail('${esc(g.counter_id)}')\" style=\"font-size:11px;padding:3px 9px;border-radius:12px;border:1px solid var(--accent);background:transparent;color:var(--accent);cursor:pointer;white-space:nowrap;flex-shrink:0\">View Counter →</button>
          <span class=\"mrg-pick ${hasOfficial ? 'has' : 'none'}\">${hasOfficial ? '✓ Picked' : 'Pick now'}</span>
        </div>
        <div class=\"mrg-body\" id=\"mrg-body-${cssEsc(groupId)}\"></div>
      </div>`;
  }).join('');
}·
function cssEsc(s) { return (s || '').replace(/[^a-z0-9_-]/gi, '_').slice(0, 80); }·
async function toggleMrg(groupId) {
  const safe = cssEsc(groupId);
  const row  = document.getElementById('mrg-' + safe);
  if (!row) return;
  const wasOpen = row.classList.contains('expanded');
  document.querySelectorAll('.mrg-row.expanded').forEach(r => r.classList.remove('expanded'));
  if (wasOpen) return;
  row.classList.add('expanded');
  await loadMrgRecces(groupId);
}·
async function loadMrgRecces(groupId) {
  const [counterId, groupKey] = groupId.split('||');
  const safe = cssEsc(groupId);
  const body = document.getElementById('mrg-body-' + safe);
  body.innerHTML = '<div class=\"empty\" style=\"padding:16px\"><span class=\"spin\">↻</span> Loading Recces…</div>';
  try {
    // Filter by counter_id AND matching group key (job_id or free_text_identifier)
    let q = `/submissions?counter_id=eq.${encodeURIComponent(counterId)}&order=visit_date.desc,submitted_at.desc`;
    if (groupKey === '<no-id>') {
      q += '&job_id=is.null&free_text_identifier=is.null';
    } else {
      // group_key matches either job_id or free_text_identifier
      q += `&or=(job_id.eq.${encodeURIComponent(groupKey)},free_text_identifier.eq.${encodeURIComponent(groupKey)})`;
    }
    const recces = await fetch(`${DB}${q}`, {
      headers: { 'Accept-Profile': 'recce', 'Accept': 'application/json' },
    }).then(r => r.ok ? r.json() : []);
    _mrgRecces.set(groupId, recces);
    renderMrgBody(groupId, body, recces);
  } catch (e) {
    body.innerHTML = '<div class=\"empty\" style=\"padding:16px;color:var(--r)\">Load failed: ' + esc(e.message) + '</div>';
  }
}·
// Phase 4.0a: sign a /recce-view-proxy/ token for the current Hub session,
// then open the returned URL. Replaces the previous direct window.open on
// view_url / pdf_url which is now 401-blocked by Traefik.
async function openSignedRecceView(subId, kind = 'html') {
  try {
    const hubAuth = 'Hub ' + btoa(JSON.stringify({
      empId: session.empId, name: session.name, role: session.role || 'user',
      loginAt: session.loginAt || new Date().toISOString(),
    }));
    const r = await fetch('/recce-view-proxy/sign', {
      method:  'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': hubAuth },
      body:    JSON.stringify({ sub_id: subId, ttl_s: 900 }),
    });
    if (!r.ok) throw new Error(r.status + ' ' + (await r.text()).slice(0, 80));
    const j = await r.json();
    const url = kind === 'pdf' ? j.urls.pdf : j.urls.html;
    window.open(url, '_blank', 'noopener');
  } catch (e) {
    toast('View failed: ' + e.message, 'err');
  }
}·
async function pickOfficial(subId, groupId) {
  if (!confirm('Mark this Recce as the official one for the client?\\nAny prior official in this group will be un-marked.')) return;
  try {
    await api('/rpc/mark_official_recce', {
      method: 'POST',
      body:   JSON.stringify({ p_sub_id: subId, p_picked_by: myId }),
    });
    toast('Marked official ✓', 'ok');
    await loadMrgRecces(groupId);
    await loadMultiRecce();
  } catch (e) {
    toast('Pick failed: ' + e.message, 'err');
  }
}·
// ══════════════════════════════════════════════════════════
// PAST RECCES TAB — filterable history browser (ADR-027 chips)
// ══════════════════════════════════════════════════════════·
function renderRecceTab() {
  const tab = document.getElementById('d-recces');
  if (!tab) return;·
  const brands    = [...new Set(detailRecces.map(r => r.brand).filter(Boolean))].sort();
  const campaigns = [...new Set(detailRecces.map(r => r.job_id || r.free_text_identifier).filter(Boolean))].sort();·
  // ID/Name policy (ADR-130 §5): `campaigns` mixes real job_id FKs with free-text
  // identifiers (r.free_text_identifier is already human text, never a raw ID —
  // only r.job_id values need resolving). Render synchronously with whatever is
  // cached now (raw id as last-resort fallback), then batch-resolve any missing
  // job_ids and re-render once — never a per-row fetch.
  const realJobIds = [...new Set(detailRecces.map(r => r.job_id).filter(Boolean))];
  const missingJobNames = realJobIds.filter(id => !_jobNameCache[id]);
  if (missingJobNames.length) _ensureJobNames(missingJobNames).then(changed => { if (changed) renderRecceTab(); });·
  let list = detailRecces;
  if (_recceFilter.brands.size)    list = list.filter(r => _recceFilter.brands.has(r.brand));
  if (_recceFilter.campaigns.size) list = list.filter(r => _recceFilter.campaigns.has(r.job_id || r.free_text_identifier));
  if (_recceFilter.from)           list = list.filter(r => r.visit_date >= _recceFilter.from);
  if (_recceFilter.to)             list = list.filter(r => r.visit_date <= _recceFilter.to);·
  const chipRow = (dim, vals, active, labelFn) =>
    `<div class=\"recce-filter-chips\">${
      vals.map(v => `<span class=\"filter-chip${active.has(v) ? ' active' : ''}\" onclick=\"rcChip('${dim}','${esc(v)}')\">${esc(labelFn ? labelFn(v) : v)}</span>`).join('')
    }${active.size ? `<span class=\"filter-chip\" style=\"color:var(--r);border-color:var(--r)\" onclick=\"rcClear('${dim}')\">✕ Clear</span>` : ''}</div>`;·
  const showFilter = brands.length > 1 || campaigns.length > 1 || detailRecces.length > 5;
  const filterBar = showFilter ? `<div class=\"recce-filter-bar\">
    ${brands.length > 1    ? chipRow('brands', brands, _recceFilter.brands) : ''}
    ${campaigns.length > 1 ? chipRow('campaigns', campaigns, _recceFilter.campaigns, v => _jobNameCache[v] || v) : ''}
    ${detailRecces.length > 5 ? `<div class=\"recce-date-row\">
      <label>From</label><input type=\"date\" value=\"${_recceFilter.from}\" onchange=\"_recceFilter.from=this.value;renderRecceTab()\">
      <label>To</label><input type=\"date\" value=\"${_recceFilter.to}\" onchange=\"_recceFilter.to=this.value;renderRecceTab()\">
    </div>` : ''}
  </div>` : '';·
  const cards = list.length
    ? list.map(r => {
        const photos = (r.front_photos||0)+(r.inside_photos||0)+(r.addl_photos||0);
        return `
        <div class=\"recce-card ${r.is_official_for_client ? 'official' : ''}\">
          <div class=\"rc-top\">
            <span class=\"rc-date\">${esc(fmtDate(r.visit_date))} · ${esc(r.brand || '—')}</span>
            ${r.is_official_for_client ? '<span class=\"rc-official-badge\">✓ Official</span>' : ''}
            ${r.superseded_by_sub_id ? '<span class=\"rc-official-badge\" style=\"background:#1e3a5f;color:#93c5fd\">↳ Superseded</span>' : ''}
          </div>
          <div class=\"rc-meta\">
            <span>By ${esc(r.submitted_by || '—')}</span>
            ${r.job_id ? `<span>Campaign: ${esc(_jobNameCache[r.job_id] || r.job_id)}</span>` : ''}
            <span>${photos} 📷</span>
            ${r.has_branding ? `<span>🏷 ${r.branding_count||0} branding</span>` : ''}
            ${r.has_video ? '<span>🎥 video</span>' : ''}
            ${r.gps_lat ? `<a href=\"https://maps.google.com/?q=${r.gps_lat},${r.gps_lng}\" target=\"_blank\" style=\"color:var(--accent);font-size:11px;text-decoration:none\">📍 Map</a>` : ''}
          </div>
          <div class=\"rc-actions\">
            ${r.view_url ? `<button class=\"rc-view\" onclick=\"openSignedRecceView('${esc(r.sub_id)}','html')\">👁 View</button>` : ''}
            ${r.pdf_url  ? `<button class=\"rc-view\" onclick=\"openSignedRecceView('${esc(r.sub_id)}','pdf')\">📄 PDF</button>` : ''}
          </div>
        </div>`;
      }).join('')
    : `<div class=\"empty\" style=\"padding:20px 0\"><div class=\"empty-icon\">🔍</div><div>No recces match filters</div></div>`;·
  tab.innerHTML = filterBar + (list.length === 0 && !detailRecces.length
    ? `<div class=\"empty\"><div class=\"empty-icon\">📸</div><div>No recces recorded for this counter yet</div></div>`
    : cards);·
  const recTab = document.querySelector('.tabs button[data-tab=\"recces\"]');
  if (recTab) {
    const n = detailRecces.length;
    const f = list.length;
    recTab.textContent = n === 0 ? 'Past Recces'
      : f < n ? `Past Recces (${f}/${n})`
      : `Past Recces (${n})`;
  }
}·
function rcChip(dim, val) {
  if (_recceFilter[dim].has(val)) _recceFilter[dim].delete(val);
  else _recceFilter[dim].add(val);
  renderRecceTab();
}·
function rcClear(dim) { _recceFilter[dim].clear(); renderRecceTab(); }·
// ══════════════════════════════════════════════════════════
// MULTI-RECCE — Conflict Resolver
// ══════════════════════════════════════════════════════════·
function renderMrgBody(groupId, body, recces) {
  if (!recces.length) {
    body.innerHTML = '<div class=\"empty\" style=\"padding:16px\">No recces found</div>';
    return;
  }
  if (recces.length === 2) {
    body.innerHTML = _recceConflictUI(recces[0], recces[1], groupId);
  } else {
    // 3+ recces — list with individual pick buttons
    body.innerHTML = `<div style=\"font-size:11px;color:var(--muted);margin-bottom:8px\">
      ⚠ ${recces.length} recces — pick one as official, or open the counter's Past Recces tab for full history.
    </div>${recces.map(r => _recceSingleCard(r, groupId)).join('')}`;
  }
}·
function _recceSingleCard(r, groupId) {
  const total = (r.front_photos||0)+(r.inside_photos||0)+(r.addl_photos||0);
  return `
    <div class=\"recce-card ${r.is_official_for_client ? 'official' : ''}\">
      <div class=\"rc-top\">
        <span class=\"rc-date\">${esc(fmtDate(r.visit_date))} · ${esc(r.brand||'—')}</span>
        <div style=\"display:flex;gap:5px;align-items:center\">
          ${r.is_official_for_client ? '<span class=\"rc-official-badge\">✓ Official</span>' : ''}
          ${r.superseded_by_sub_id ? '<span class=\"rc-official-badge\" style=\"background:#1e3a5f;color:#93c5fd\">↳ Superseded</span>' : ''}
        </div>
      </div>
      <div class=\"rc-meta\">
        <span>By ${esc(r.submitted_by||'—')}</span>
        ${r.submitted_at ? `<span>Submitted ${esc(fmtDate(r.submitted_at.slice(0,10)))}</span>` : ''}
        <span>${total} 📷</span>
        ${r.has_branding ? `<span>🏷 ${r.branding_count||0}</span>` : ''}
        ${r.has_video ? '<span>🎥</span>' : ''}
        ${r.official_picked_by ? `<span style=\"color:#86efac\">Picked by ${esc(r.official_picked_by)}</span>` : ''}
      </div>
      <div class=\"rc-actions\">
        ${r.view_url ? `<button class=\"rc-view\" onclick=\"openSignedRecceView('${esc(r.sub_id)}','html')\">👁 View</button>` : ''}
        ${r.pdf_url  ? `<button class=\"rc-view\" onclick=\"openSignedRecceView('${esc(r.sub_id)}','pdf')\">📄 PDF</button>` : ''}
        ${r.is_official_for_client ? `<button class=\"rc-view\" onclick=\"openCommentsModal('${esc(r.sub_id)}','${esc(r.brand||'')}')\">💬</button>` : ''}
        <button class=\"rc-pick\" ${r.is_official_for_client ? 'disabled' : `onclick=\"pickOfficial('${esc(r.sub_id)}','${esc(groupId)}')\"`}>
          ${r.is_official_for_client ? '✓ Official' : '★ Pick'}
        </button>
      </div>
    </div>`;
}·
function _recceConflictUI(rA, rB, groupId) {
  const sg = cssEsc(groupId);
  const officialA = rA.is_official_for_client, officialB = rB.is_official_for_client;
  return `
    <div class=\"mrg-ab-tabs\">
      <button class=\"mrg-ab-tab active\" id=\"tab-a-${sg}\" onclick=\"mrgShowAB('${sg}','a')\">
        Recce A · ${esc(fmtDate(rA.visit_date))}${officialA ? ' ✓' : ''}
      </button>
      <button class=\"mrg-ab-tab\" id=\"tab-b-${sg}\" onclick=\"mrgShowAB('${sg}','b')\">
        Recce B · ${esc(fmtDate(rB.visit_date))}${officialB ? ' ✓' : ''}
      </button>
    </div>
    <div id=\"card-a-${sg}\">${_recceSingleCard(rA, groupId)}</div>
    <div id=\"card-b-${sg}\" style=\"display:none\">${_recceSingleCard(rB, groupId)}</div>
    <button onclick=\"mrgToggleDiff('${sg}')\" style=\"width:100%;margin:6px 0;padding:6px;background:var(--card);border:1px solid var(--border);border-radius:var(--rads);color:var(--muted);font-size:11.5px;cursor:pointer;font-family:inherit\">
      ⇆ Compare A vs B (field diff)
    </button>
    <div id=\"diff-${sg}\" style=\"display:none\">${_recceDiffTable(rA, rB)}</div>
    <div class=\"mrg-resolve-bar\">
      <button onclick=\"pickOfficial('${esc(rA.sub_id)}','${esc(groupId)}')\" ${officialA ? 'disabled' : ''} style=\"background:var(--card);color:var(--text)\">
        ${officialA ? '✓ A is Official' : '★ A is Official'}
      </button>
      <button onclick=\"pickOfficial('${esc(rB.sub_id)}','${esc(groupId)}')\" ${officialB ? 'disabled' : ''} style=\"background:var(--card);color:var(--text)\">
        ${officialB ? '✓ B is Official' : '★ B is Official'}
      </button>
      <button onclick=\"mrgToggleOverride('${sg}')\" style=\"background:rgba(251,191,36,.12);color:#fde68a;border-color:rgba(251,191,36,.3)\">
        ⊕ Override
      </button>
    </div>
    <div class=\"mrg-override-panel\" id=\"ovr-${sg}\">
      <div style=\"font-size:11.5px;color:var(--muted);margin-bottom:8px\">Choose base record and add a supervisor note. The other will be marked superseded.</div>
      <div style=\"display:flex;gap:14px;margin-bottom:8px;font-size:12.5px\">
        <label><input type=\"radio\" name=\"ovr-base-${sg}\" value=\"a\" checked> Recce A as base</label>
        <label><input type=\"radio\" name=\"ovr-base-${sg}\" value=\"b\"> Recce B as base</label>
      </div>
      <textarea id=\"ovr-note-${sg}\" placeholder=\"Supervisor note: reason for decision, what differs, which photos to use…\" rows=\"3\"
        style=\"width:100%;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:12px;padding:8px;resize:vertical;box-sizing:border-box;font-family:inherit\"></textarea>
      <button onclick=\"applyOverride('${esc(rA.sub_id)}','${esc(rB.sub_id)}','${sg}','${esc(groupId)}')\"
        style=\"margin-top:8px;width:100%;padding:10px;background:var(--accent);color:#fff;border:none;border-radius:var(--rads);font-size:13px;font-weight:700;cursor:pointer;font-family:inherit\">
        Apply Supervisor Override
      </button>
    </div>`;
}·
function _recceDiffTable(rA, rB) {
  const FIELDS = [
    { key: 'store_name',     label: 'Store name' },
    { key: 'brand',          label: 'Brand' },
    { key: 'visit_date',     label: 'Visit date', fmt: v => fmtDate(v) },
    { key: 'submitted_by',   label: 'Agent' },
    { key: 'address',        label: 'Address' },
    { key: 'city',           label: 'City' },
    { key: 'mobile',         label: 'Mobile' },
    { key: 'has_branding',   label: 'Has branding', fmt: v => v ? 'Yes' : 'No' },
    { key: 'branding_count', label: 'Branding count', fmt: v => String(v||0) },
    { key: 'has_video',      label: 'Has video', fmt: v => v ? 'Yes' : 'No' },
    { key: 'front_photos',   label: 'Front photos', fmt: v => String(v||0) },
    { key: 'inside_photos',  label: 'Inside photos', fmt: v => String(v||0) },
    { key: 'addl_photos',    label: 'Addl photos', fmt: v => String(v||0) },
    { key: 'gps_lat',        label: 'GPS', fmt: (v, r) => r.gps_lat ? `${Number(r.gps_lat).toFixed(5)}, ${Number(r.gps_lng).toFixed(5)}` : '—' },
    { key: 'status',         label: 'Status' },
    { key: 'client_name',    label: 'Client name' },
  ];
  const fmt = (f, r) => f.fmt ? f.fmt(r[f.key], r) : (r[f.key] != null ? String(r[f.key]) : '—');
  const rows = FIELDS.map(f => {
    const vA = fmt(f, rA), vB = fmt(f, rB);
    return `<tr class=\"${vA !== vB ? 'diff-row' : ''}\">
      <td>${f.label}</td><td>${esc(vA)}</td><td>${esc(vB)}</td>
    </tr>`;
  }).join('');
  return `<table class=\"diff-table\">
    <thead><tr><th>Field</th><th>Recce A</th><th>Recce B</th></tr></thead>
    <tbody>${rows}</tbody>
  </table>`;
}·
function mrgShowAB(sg, which) {
  document.getElementById('card-a-' + sg).style.display = which === 'a' ? '' : 'none';
  document.getElementById('card-b-' + sg).style.display = which === 'b' ? '' : 'none';
  document.getElementById('tab-a-'  + sg).classList.toggle('active', which === 'a');
  document.getElementById('tab-b-'  + sg).classList.toggle('active', which === 'b');
}·
function mrgToggleDiff(sg) {
  const el = document.getElementById('diff-' + sg);
  if (el) el.style.display = el.style.display === 'none' ? '' : 'none';
}·
function mrgToggleOverride(sg) {
  document.getElementById('ovr-' + sg)?.classList.toggle('open');
}·
async function applyOverride(subIdA, subIdB, sg, groupId) {
  const base = document.querySelector(`input[name=\"ovr-base-${sg}\"]:checked`)?.value || 'a';
  const note = document.getElementById('ovr-note-' + sg)?.value.trim();
  if (!note) { toast('Add a supervisor note before applying', 'err'); return; }
  const winnerId = base === 'a' ? subIdA : subIdB;
  const loserId  = base === 'a' ? subIdB : subIdA;
  if (!confirm(`Mark Recce ${base.toUpperCase()} as official? The other will be marked superseded.`)) return;
  try {
    await api('/rpc/supervisor_pick_official', {
      method: 'POST',
      body:   JSON.stringify({ p_winner_sub_id: winnerId, p_loser_sub_id: loserId, p_picked_by: myId, p_note: note }),
    });
    toast('Supervisor override applied ✓', 'ok');
    await loadMrgRecces(groupId);
    await loadMultiRecce();
  } catch (e) {
    toast('Override failed: ' + e.message, 'err');
  }
}·
// ══════════════════════════════════════════════════════════
// PHASE 2 — UPLOAD QUEUE (Strict Gate approval)
// ══════════════════════════════════════════════════════════
let _uploads = [];
let _pendingUploadRows = [];   // parsed CSV rows awaiting submission·
// ── Template Download ──────────────────────────────────────
function exportCounterCSV() {
  const list = filterCounters();
  if (!list.length) { toast('No counters to export', 'err'); return; }
  const cols = [
    'counter_id','display_name','legal_name','proprietor_name',
    'address_line1','address_line2','city','state','pincode',
    'gstin','pan','fssai','drug_lic','counter_type','channel',
    'customer_group','territory','distributor_name','notes','brand_codes',
    'recce_count','last_recce_date','status','approval_status',
    'primary_contact_name','primary_contact_phone',
  ];
  const csvEscVal = v => {
    if (v == null) return '';
    const s = Array.isArray(v) ? v.join(',') : String(v);
    return s.includes(',') || s.includes('\"') || s.includes('\\n')
      ? '\"' + s.replace(/\"/g, '\"\"') + '\"'
      : s;
  };
  const rows = [cols.join(',')];
  list.forEach(c => rows.push(cols.map(k => csvEscVal(c[k])).join(',')));
  const blob = new Blob([rows.join('\\n')], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = `counters_export_${new Date().toISOString().slice(0, 10)}.csv`;
  a.click();
  URL.revokeObjectURL(a.href);
  toast(`Exported ${list.length} counters`, 'ok');
}·
function downloadUploadTemplate() {
  // Collect distinct brand codes from loaded counters as a reference
  const knownBrands = [...new Set([...(window.MASTER_BRAND_CODES || []),
                                   ...allCounters.flatMap(c => c.brand_codes || [])])].sort().join(', ') || 'HP, HP_INK, SAMSUNG';
  const lines = [
    '# 360LM Counter Upload Template v2 | Fields marked * are MANDATORY | Multiple brand_codes: separate with comma e.g. HP,SAMSUNG',
    `# Valid brand_codes in system: ${knownBrands}`,
    `# counter_type values: sub_dealer / dealer / distributor / modern_trade / standalone / other`,
    `# channel values: general_trade / modern_trade / institution / e_commerce / other`,
    'display_name*,legal_name,proprietor_name,address_line1,address_line2,city*,state*,pincode*,gstin,pan,fssai,drug_lic,counter_type,channel,customer_group,territory,distributor_name,notes,brand_codes,primary_contact_name,primary_contact_phone',
    'Example Store Name,,Proprietor Name,123 Main Street,,Mumbai,Maharashtra,400001,27AABCS1234A1ZS,,,,,dealer,general_trade,West Zone,City Zone,HP Distributor Ltd,First visit,HP,Ramesh Kumar,9876543210',
  ];
  const blob = new Blob([lines.join('\\n')], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = `counter_upload_template_${new Date().toISOString().slice(0, 10)}.csv`;
  a.click();
  URL.revokeObjectURL(a.href);
}·
// ── CSV Parse ─────────────────────────────────────────────
function parseCSVLine(line) {
  const result = [];
  let cur = '', inQuote = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (ch === '\"') {
      if (inQuote && line[i + 1] === '\"') { cur += '\"'; i++; }
      else inQuote = !inQuote;
    } else if (ch === ',' && !inQuote) {
      result.push(cur.trim()); cur = '';
    } else {
      cur += ch;
    }
  }
  result.push(cur.trim());
  return result;
}·
function parseCSV(text) {
  const lines = text.split(/\\r?\\n/);
  const dataLines = lines.filter(l => l.trim() && !l.startsWith('#'));
  if (dataLines.length < 2) return [];
  const headers = parseCSVLine(dataLines[0]).map(h => h.replace(/\\*/g, '').toLowerCase().trim());
  return dataLines.slice(1).map(l => {
    const vals = parseCSVLine(l);
    const obj = {};
    headers.forEach((h, i) => { if (h) obj[h] = (vals[i] || '').trim(); });
    return obj;
  }).filter(r => Object.values(r).some(v => v));
}·
// ── Handle file selection ─────────────────────────────────
function handleUploadFileSelect(input) {
  const file = input.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = e => {
    const rows = parseCSV(e.target.result);
    if (!rows.length) { toast('No data rows found in CSV', 'err'); return; }
    _pendingUploadRows = rows;
    // Pre-fill list name from filename
    const nameEl = document.getElementById('csvup-name');
    if (!nameEl.value) nameEl.value = file.name.replace(/\\.[^.]+$/, '').replace(/[_-]/g, ' ');
    // Show preview
    const preview = document.getElementById('csvup-preview');
    const sample = rows.slice(0, 3).map(r => r.display_name || r.name || JSON.stringify(r).slice(0, 60)).join(', ');
    preview.innerHTML = `<b>${rows.length} rows</b> parsed from <b>${esc(file.name)}</b><br>Preview: ${esc(sample)}${rows.length > 3 ? '…' : ''}`;
    document.getElementById('modal-csvup').classList.add('show');
  };
  reader.onerror = () => toast('Failed to read file', 'err');
  reader.readAsText(file);
  input.value = ''; // reset so same file can be re-selected
}·
// ── AI Import ─────────────────────────────────────────────
const AI_CONVERT_URL    = '/counter-ai/';
const AI_CONVERT_SECRET = '6PItF6boihpxT1z7PDJClAnzR6QbfqPsQttIjfV7zPU';·
let _aiConvertedRows = [];·
function openAiImport() {
  if (!isSupervisor()) return;
  _aiConvertedRows = [];
  document.getElementById('ai-raw-input').value = '';
  document.getElementById('ai-import-status').textContent = '';
  document.getElementById('ai-detect-hint').textContent = '';
  document.getElementById('ai-import-preview').style.display = 'none';
  document.getElementById('btn-ai-use-rows').style.display = 'none';
  document.getElementById('btn-ai-convert').disabled = false;
  document.getElementById('btn-ai-convert').textContent = 'Convert with AI';
  document.getElementById('modal-ai-import').classList.add('show');
}·
function aiDetectStores(val) {
  const hint = document.getElementById('ai-detect-hint');
  if (!val.trim()) { hint.textContent = ''; return; }
  const blocks = val.trim().split(/\\n\\s*\\n/).filter(b => b.trim());
  const n = blocks.length;
  const batches = Math.ceil(n / 15);
  hint.textContent = `~${n} store${n !== 1 ? 's' : ''} detected` +
    (batches > 1 ? ` · ${batches} batches` : '');
}·
async function runAiConvert(allowApi = false) {
  const raw = document.getElementById('ai-raw-input').value.trim();
  if (!raw) { toast('Paste a store list first', 'err'); return; }·
  const btn      = document.getElementById('btn-ai-convert');
  const statusEl = document.getElementById('ai-import-status');
  const previewEl = document.getElementById('ai-import-preview');
  const useBtn   = document.getElementById('btn-ai-use-rows');·
  btn.disabled = true;
  btn.textContent = 'Converting…';
  statusEl.style.color = 'var(--muted)';
  const estBatches = Math.ceil(raw.trim().split(/\\n\\s*\\n/).filter(b => b.trim()).length / 15);
  statusEl.textContent = allowApi
    ? '⏳ Retrying via Anthropic API…'
    : estBatches > 1
      ? `⏳ Converting ${estBatches} batches… may take ${estBatches * 15}–${estBatches * 25}s`
      : '⏳ Sending to AI… this may take 10–30 seconds';
  previewEl.style.display = 'none';
  useBtn.style.display = 'none';·
  try {
    const resp = await fetch(AI_CONVERT_URL, {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': 'Bearer ' + AI_CONVERT_SECRET,
      },
      body: JSON.stringify({ raw, allow_api: allowApi }),
    });·
    const data = await resp.json();·
    // Auto-routes failed — ask supervisor whether to use the API key
    if (resp.status === 503 && data.need_api_permission) {
      btn.disabled = false;
      btn.textContent = 'Re-convert';
      statusEl.style.color = 'var(--or)';
      statusEl.innerHTML =
        '⚠ Auto AI routes (OAuth &amp; OpenRouter) unavailable. ' +
        '<button class=\"btn btn-s\" style=\"font-size:11px;padding:3px 10px;margin-left:6px\" ' +
        'onclick=\"confirmApiUsage()\">Allow Anthropic API</button>';
      return;
    }·
    if (!resp.ok || data.error) {
      throw new Error(data.error || data.message || `HTTP ${resp.status}`);
    }·
    _aiConvertedRows = data.rows || [];
    if (!_aiConvertedRows.length) {
      statusEl.style.color = 'var(--or)';
      statusEl.textContent = '⚠ AI returned no rows — try rephrasing or check the input.';
      btn.disabled = false;
      btn.textContent = 'Retry';
      return;
    }·
    // Show preview table
    statusEl.style.color = '#4ade80';
    const batchInfo = data.batches > 1 ? ` (${data.batches} batches)` : '';
    statusEl.textContent = `✓ ${_aiConvertedRows.length} store${_aiConvertedRows.length !== 1 ? 's' : ''} converted via ${data.mode || 'AI'}${batchInfo}.`;
    document.getElementById('ai-preview-count').textContent =
      `${_aiConvertedRows.length} row${_aiConvertedRows.length !== 1 ? 's' : ''}`;
    document.getElementById('ai-preview-table').innerHTML = _aiConvertedRows.map((r, i) =>
      `<div style=\"padding:4px 0;${i ? 'border-top:1px solid var(--border)' : ''}\">` +
        `<b>${esc(r.display_name || '(no name)')}</b>` +
        (r.city    ? ` · ${esc(r.city)}`    : '') +
        (r.state   ? `, ${esc(r.state)}`   : '') +
        (r.pincode ? ` ${esc(r.pincode)}`  : '') +
        (r.primary_contact_phone ? ` · 📞 ${esc(r.primary_contact_phone)}` : '') +
        (r.counter_type ? ` <span style=\"color:var(--muted)\">[${esc(r.counter_type)}]</span>` : '') +
      '</div>'
    ).join('');
    previewEl.style.display = '';
    useBtn.style.display = '';
    btn.textContent = 'Re-convert';
    btn.disabled = false;
  } catch (err) {
    statusEl.style.color = 'var(--err)';
    statusEl.textContent = `✗ ${err.message}`;
    btn.disabled = false;
    btn.textContent = 'Retry';
  }
}·
function confirmApiUsage() {
  const ok = confirm(
    'Auto AI routes (OAuth + OpenRouter) are currently unavailable.\\n\\n' +
    'Allow use of Anthropic API key as fallback?\\n' +
    '(This uses paid API credits from the configured API key.)'
  );
  if (!ok) {
    const statusEl = document.getElementById('ai-import-status');
    statusEl.style.color = 'var(--err)';
    statusEl.textContent = '✗ API key usage declined. Try again later when OAuth or OpenRouter is available.';
    return;
  }
  runAiConvert(true);
}·
// ── Campaign Enrichment — 4-phase guided flow (ADR-062) ──────────────────────·
// Fuzzy name matching (inline Levenshtein, zero dependency)
function _normName(s) {
  return (s||'').toLowerCase().replace(/[^a-z0-9 ]/g,' ').replace(/\\s+/g,' ').trim();
}
function _lev(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({length:m+1}, (_,i) => Array.from({length:n+1}, (_,j) => i||j));
  for (let i=1;i<=m;i++) for (let j=1;j<=n;j++)
    dp[i][j] = a[i-1]===b[j-1] ? dp[i-1][j-1] : 1+Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]);
  return dp[m][n];
}
function _fuzzyScore(a, b) {
  const na=_normName(a), nb=_normName(b);
  const ml=Math.max(na.length,nb.length);
  return ml ? 1-_lev(na,nb)/ml : 0;
}·
let _enrichRows = [];  // [{aiRow, matchType, matchedId, matchScore, geo, decision}]
let _enrichIdx  = 0;·
function _enrichShow(phase) {
  ['enrich-p12','enrich-p3','enrich-p4'].forEach(id => {
    const el = document.getElementById(id);
    el.style.display = id===phase ? (phase==='enrich-p3'?'flex':'flex') : 'none';
  });
}·
async function startEnrichment() {
  if (!_aiConvertedRows.length) return;
  document.getElementById('ai-import-ttl').textContent = '🤖 Match & Enrich';
  _enrichShow('enrich-p3');
  _enrichRows = [];
  _enrichIdx  = 0;·
  // ── Stage A: 3-tier name matching ────────────────────────
  document.getElementById('enrich-p3-status').textContent = `Matching ${_aiConvertedRows.length} stores to existing counters…`;
  for (const aiRow of _aiConvertedRows) {
    const name   = (aiRow.display_name||'').trim();
    const gstin  = (aiRow.gstin||'').trim().toUpperCase();
    const pin    = (aiRow.pincode||'').trim();
    const city   = (aiRow.city||'').trim().toLowerCase();
    let matchType='new', matchedId=null, matchScore=0;·
    // Tier 1: GSTIN exact
    if (gstin) {
      const m = allCounters.find(c => (c.gstin||'').toUpperCase()===gstin);
      if (m) { matchType='exact'; matchedId=m.counter_id; matchScore=1; }
    }
    // Tier 2: Name + pincode exact (case-insensitive)
    if (!matchedId && name && pin) {
      const m = allCounters.find(c => (c.pincode||'')=== pin && _normName(c.display_name)===_normName(name));
      if (m) { matchType='exact'; matchedId=m.counter_id; matchScore=1; }
    }
    // Tier 3: Fuzzy name (city-scoped, threshold 0.80)
    if (!matchedId && name) {
      let best=0, bestId=null;
      for (const c of allCounters) {
        if (city && c.city && c.city.toLowerCase()!==city) continue;
        const s = _fuzzyScore(name, c.display_name||'');
        if (s>best) { best=s; bestId=c.counter_id; }
      }
      if (best>=0.80) { matchType='fuzzy'; matchedId=bestId; matchScore=best; }
    }
    _enrichRows.push({ aiRow, matchType, matchedId, matchScore, geo:null, decision:null });
  }·
  // ── Stage B: geo enrichment via /geo ─────────────────────
  const geoTargets = _enrichRows.filter(r => r.aiRow.city);
  document.getElementById('enrich-p3-status').textContent = `Enriching geo fields for ${geoTargets.length} stores via AI…`;
  try {
    const resp = await fetch(AI_CONVERT_URL + 'geo', {
      method:'POST',
      headers:{'Content-Type':'application/json','Authorization':'Bearer '+AI_CONVERT_SECRET},
      body: JSON.stringify({ counters: geoTargets.map((r,i) => ({
        counter_id: String(i),
        display_name: r.aiRow.display_name||'',
        city: r.aiRow.city||'',
        state: r.aiRow.state||'',
      }))})
    });
    if (resp.ok) {
      const data = await resp.json();
      (data.suggestions||[]).forEach(sug => {
        const idx = parseInt(sug.counter_id,10);
        if (!isNaN(idx) && geoTargets[idx]) geoTargets[idx].geo = sug;
      });
    }
  } catch(e) { console.warn('[enrich] geo call failed:', e); }·
  // ── Transition to Phase 4 ─────────────────────────────────
  _enrichShow('enrich-p4');
  renderEnrichCard(_enrichIdx);
}·
function renderEnrichPips() {
  document.getElementById('enrich-pips').innerHTML =
    _enrichRows.map((r,i) => `<div class=\"epip${i<_enrichIdx?' done':i===_enrichIdx?' active':''}\"></div>`).join('');
  const done = _enrichRows.filter(r=>r.decision).length;
  document.getElementById('enrich-pip-label').textContent =
    `${done} of ${_enrichRows.length} reviewed${done===_enrichRows.length?' — ready to apply':''}`;
}·
function renderEnrichCard(idx) {
  if (idx >= _enrichRows.length) {
    // All reviewed
    document.getElementById('btn-enrich-accept').style.display = 'none';
    document.getElementById('btn-enrich-skip').style.display   = 'none';
    document.getElementById('btn-enrich-apply').style.display  = '';
    const accepted = _enrichRows.filter(r=>r.decision==='accept').length;
    document.getElementById('enrich-apply-status').textContent =
      `${accepted} accepted · ${_enrichRows.filter(r=>r.decision==='skip').length} skipped`;
    renderEnrichPips();
    document.getElementById('enrich-card').innerHTML =
      `<div style=\"text-align:center;padding:20px;color:var(--g);font-size:13px\">✓ All ${_enrichRows.length} stores reviewed — click Apply to write changes</div>`;
    return;
  }
  renderEnrichPips();
  const row  = _enrichRows[idx];
  const ai   = row.aiRow;
  const c    = row.matchedId ? allCounters.find(x=>x.counter_id===row.matchedId) : null;
  const geo  = row.geo;
  const pct  = Math.round(row.matchScore*100);·
  const badgeCls = row.matchType==='exact' ? 'exact' : row.matchType==='fuzzy' ? 'fuzzy' : 'newc';
  const badgeTxt = row.matchType==='exact' ? '✓ Exact Match'
                 : row.matchType==='fuzzy' ? `≈ Fuzzy Match ${pct}%`
                 : '+ New Counter';·
  function diffRow(label, oldVal, newVal) {
    if (!newVal || newVal===oldVal) return '';
    return `<div class=\"enrich-diff-row\">
      <span class=\"ef-label\">${label}</span>
      ${oldVal ? `<span class=\"ef-old\">${esc(oldVal)}</span><span style=\"color:var(--muted)\">→</span>` : ''}
      <span class=\"ef-new\">${esc(newVal)}</span>
    </div>`;
  }·
  let diffs = '';
  if (c) {
    diffs += diffRow('State Code',  c.state_code||'',  geo?.state_code||'');
    diffs += diffRow('City Tier',   c.city_tier||'',   geo?.city_tier||'');
    diffs += diffRow('Geo Cluster', c.geo_cluster||'', geo?.geo_cluster||'');
    diffs += diffRow('Channel',     c.channel||'',     ai.channel||'');
  } else {
    // New counter — show what will be created
    diffs += diffRow('City',       '', ai.city||'');
    diffs += diffRow('State',      '', ai.state||'');
    diffs += diffRow('State Code', '', geo?.state_code||'');
    diffs += diffRow('City Tier',  '', geo?.city_tier||'');
    diffs += diffRow('Cluster',    '', geo?.geo_cluster||'');
    diffs += diffRow('Type',       '', ai.counter_type||'');
    diffs += diffRow('Channel',    '', ai.channel||'');
  }
  if (!diffs) diffs = `<div style=\"color:var(--muted);font-size:11px;padding:6px 0\">No enrichable fields found — accepting will make no DB changes</div>`;·
  document.getElementById('enrich-card').innerHTML = `
    <div style=\"display:flex;align-items:center;gap:8px;margin-bottom:10px\">
      <div style=\"font-weight:700;font-size:13px;flex:1\">${esc(ai.display_name||'Unnamed')}</div>
      <span class=\"mbadge ${badgeCls}\">${badgeTxt}</span>
    </div>
    ${c ? `<div style=\"font-size:10px;color:var(--muted);margin-bottom:8px\">→ ${esc(c.display_name)} · ${esc(c.city||'')}${c.state?', '+esc(c.state):''}</div>` : ''}
    <div style=\"font-size:11px;color:var(--muted);margin-bottom:6px;font-weight:700;text-transform:uppercase;letter-spacing:.4px\">${c?'Suggested enrichments':'Will create'}</div>
    ${diffs}
  `;·
  document.getElementById('btn-enrich-accept').style.display = '';
  document.getElementById('btn-enrich-skip').style.display   = '';
  document.getElementById('btn-enrich-apply').style.display  = 'none';
  document.getElementById('enrich-apply-status').textContent = '';
}·
function enrichDecide(decision) {
  if (_enrichIdx >= _enrichRows.length) return;
  _enrichRows[_enrichIdx].decision = decision;
  _enrichIdx++;
  renderEnrichCard(_enrichIdx);
}·
async function applyEnrichment() {
  const btn   = document.getElementById('btn-enrich-apply');
  const stEl  = document.getElementById('enrich-apply-status');
  btn.disabled = true;
  btn.textContent = 'Applying…';
  let ok=0, fail=0;
  const newRows = [];·
  for (const row of _enrichRows.filter(r=>r.decision==='accept')) {
    if (row.matchType==='new') {
      newRows.push(row.aiRow);
      continue;
    }
    // Enrich existing matched counter
    const patch = {};
    const c   = allCounters.find(x=>x.counter_id===row.matchedId);
    const geo = row.geo;
    if (geo?.state_code  && geo.state_code  !== (c?.state_code||''))  patch.state_code  = geo.state_code;
    if (geo?.city_tier   && geo.city_tier   !== (c?.city_tier||''))   patch.city_tier   = geo.city_tier;
    if (geo?.geo_cluster && geo.geo_cluster !== (c?.geo_cluster||'')) patch.geo_cluster = geo.geo_cluster;
    if (row.aiRow.channel && !c?.channel) patch.channel = row.aiRow.channel;
    if (!Object.keys(patch).length) { ok++; continue; }
    try {
      await api(`/counter?counter_id=eq.${encodeURIComponent(row.matchedId)}`,
        {method:'PATCH', headers:{Prefer:'return=minimal'}, body:JSON.stringify(patch)});
      if (c) Object.assign(c, patch);
      ok++;
    } catch { fail++; }
  }·
  // New counters → push to upload queue
  if (newRows.length) {
    _aiConvertedRows = newRows;
    useAiRows();   // stages into _pendingUploadRows and opens upload modal
  } else {
    closeModal('modal-ai-import');
  }·
  const msg = `Enrichment done — ${ok} counter${ok!==1?'s':''} updated${fail?`, ${fail} failed`:''}${newRows.length?`, ${newRows.length} new → upload queue`:''}`;
  toast(msg, ok||newRows.length ? 'ok' : 'err');
  if (ok) renderList();
}·
function useAiRows() {
  if (!_aiConvertedRows.length) return;
  // Normalise keys to match what the upload parser expects
  _pendingUploadRows = _aiConvertedRows.map(r => ({
    display_name:           r.display_name           || '',
    legal_name:             r.legal_name             || '',
    proprietor_name:        r.proprietor_name        || '',
    address_line1:          r.address_line1          || '',
    address_line2:          r.address_line2          || '',
    city:                   r.city                   || '',
    state:                  r.state                  || '',
    pincode:                r.pincode                || '',
    gstin:                  r.gstin                  || '',
    pan:                    r.pan                    || '',
    fssai:                  r.fssai                  || '',
    drug_lic:               r.drug_lic               || '',
    counter_type:           r.counter_type           || '',
    channel:                r.channel                || '',
    customer_group:         r.customer_group         || '',
    territory:              r.territory              || '',
    distributor_name:       r.distributor_name       || '',
    notes:                  r.notes                  || '',
    brand_codes:            Array.isArray(r.brand_codes) ? r.brand_codes.join(',') : (r.brand_codes || ''),
    primary_contact_name:   r.primary_contact_name   || '',
    primary_contact_phone:  r.primary_contact_phone  || '',
  }));·
  closeModal('modal-ai-import');·
  const nameEl = document.getElementById('csvup-name');
  if (!nameEl.value) nameEl.value = `AI Import ${new Date().toLocaleDateString('en-IN')}`;·
  const preview = document.getElementById('csvup-preview');
  const sample  = _pendingUploadRows.slice(0, 3).map(r => r.display_name || 'Unnamed').join(', ');
  preview.innerHTML = `<b>${_pendingUploadRows.length} rows</b> from AI conversion<br>Preview: ${esc(sample)}${_pendingUploadRows.length > 3 ? '…' : ''}`;
  document.getElementById('modal-csvup').classList.add('show');
}·
// ── Submit upload to DB ───────────────────────────────────
async function submitUploadRows() {
  const listName = document.getElementById('csvup-name').value.trim();
  if (!listName) { toast('Enter a list name', 'err'); return; }
  if (!_pendingUploadRows.length) { toast('No rows to submit', 'err'); return; }
  const btn = document.getElementById('csvup-submit');
  btn.disabled = true; btn.textContent = 'Submitting…';
  try {
    const uid = 'upl_' + Date.now().toString(36);
    await api('/upload', {
      method: 'POST',
      body: JSON.stringify({
        upload_id:    uid,
        uploaded_by:  myId,
        list_name:    listName,
        job_id:       document.getElementById('csvup-job').value.trim() || null,
        source_name:  'CSV upload',
        raw_rows:     _pendingUploadRows,
        row_count:    _pendingUploadRows.length,
      }),
      headers: { Prefer: 'return=minimal' },
    });
    const submittedCount = _pendingUploadRows.length;
    closeModal('modal-csvup');
    _pendingUploadRows = [];
    document.getElementById('csvup-name').value = '';
    document.getElementById('csvup-job').value  = '';
    toast(`Submitted ${submittedCount} rows — awaiting supervisor review`, 'ok');
    await openUploads(); // refresh the queue
  } catch (e) {
    toast('Submit failed: ' + e.message, 'err');
  } finally {
    btn.disabled = false; btn.textContent = 'Submit for Review';
  }
}·
// ── Helper: parse brand_codes column ─────────────────────
function parseBrandCodes(val) {
  if (Array.isArray(val)) return val.map(b => b.toString().toUpperCase().trim()).filter(Boolean);
  if (!val) return [];
  return val.toString().split(',').map(b => b.toUpperCase().trim()).filter(Boolean);
}·
async function openUploads() {
  const sup = isSupervisor();
  // Adapt header title by role
  const h1 = document.querySelector('#s-uploads .hdr h1');
  if (h1) h1.textContent = sup ? '📥 Counter Uploads' : '📤 My Uploads';
  // Supervisors review — they don't upload; hide the upload button for them
  const btnUp = document.querySelector('#s-uploads .hdr-btn.p');
  if (btnUp) btnUp.style.display = sup ? 'none' : '';
  showScreen('s-uploads');
  await loadUploads();
}·
async function loadUploads() {
  document.getElementById('uploads-out').innerHTML = '<div class=\"empty\"><span class=\"spin\">↻</span> Loading…</div>';
  try {
    // Supervisors see all pending; field reps see only their own (any status, last 30 days)
    const filter = isSupervisor()
      ? '/upload?approval_status=eq.pending&order=uploaded_at.desc&limit=200'
      : `/upload?uploaded_by=eq.${encodeURIComponent(myId)}&order=uploaded_at.desc&limit=50`;
    _uploads = await api(filter);
  } catch (e) {
    toast('Load failed: ' + e.message, 'err');
    _uploads = [];
  }
  // ID/Name policy (ADR-130 §5): resolve every upload's job_id to a name once
  // (batched) before rendering, instead of showing the raw id.
  await _ensureJobNames(_uploads.map(u => u.job_id));
  renderUploads();
  updateUploadBadge();
}·
async function refreshUploadBadge() {
  try {
    const r = await fetch(`${DB}/upload?approval_status=eq.pending&select=upload_id`, {
      headers: { 'Accept-Profile': 'counters', 'Accept': 'application/json' },
    });
    const arr = r.ok ? await r.json() : [];
    setUploadBadge(arr.length);
  } catch (_) {}
}·
function setUploadBadge(n) {
  const el = document.getElementById('upload-badge');
  if (!el) return;
  el.textContent = n > 0 ? `Uploads (${n})` : 'Uploads';
}·
function updateUploadBadge() { setUploadBadge((_uploads || []).length); }·
async function refreshFieldRepUploadBadge() {
  if (!myId) return;
  const btn = document.getElementById('btn-uploads');
  if (!btn) return;
  try {
    const r = await fetch(`${DB}/upload?uploaded_by=eq.${encodeURIComponent(myId)}&approval_status=eq.rejected&select=upload_id`, {
      headers: { 'Accept-Profile': 'counters', 'Accept': 'application/json' },
    });
    const arr = r.ok ? await r.json() : [];
    const el = document.getElementById('upload-badge');
    if (!el) return;
    if (arr.length > 0) {
      el.textContent = `Uploads (${arr.length} ✗)`;
      el.style.color = 'var(--rd, #dc2626)';
      el.style.fontWeight = '600';
    }
  } catch (_) {}
}·
function renderUploads() {
  const sup = isSupervisor();
  const emptyMsg = sup
    ? 'No pending uploads'
    : 'No uploads yet — tap ⬆ Upload CSV above to submit a counter list';
  // Update sec-label to match user role
  const secLabel = document.querySelector('#s-uploads .sec-label span:first-child');
  if (secLabel) secLabel.textContent = sup ? 'Awaiting supervisor review' : 'My Uploads';
  document.getElementById('uploads-count').textContent =
    sup ? `${_uploads.length} pending` : `${_uploads.length} submitted`;
  const out = document.getElementById('uploads-out');
  if (!_uploads.length) {
    out.innerHTML = `<div class=\"empty\"><div class=\"empty-icon\">📥</div><div>${emptyMsg}</div></div>`;
    return;
  }
  out.innerHTML = _uploads.map(u => {
    const statusChip = u.approval_status === 'approved'
      ? `<span class=\"status-chip active\">APPROVED</span>`
      : u.approval_status === 'rejected'
      ? `<span class=\"status-chip inactive\">REJECTED</span>`
      : `<span class=\"status-chip pending_review\">PENDING</span>`;
    const actionBtns = sup && u.approval_status === 'pending' ? `
      <div class=\"btn-row\" style=\"margin-top:10px\">
        <button class=\"btn btn-r\" onclick=\"rejectUpload('${esc(u.upload_id)}')\">Reject</button>
        <button class=\"btn btn-p\" onclick=\"approveUpload('${esc(u.upload_id)}')\">Approve + Create counters</button>
      </div>` : u.approval_status === 'rejected' && u.approval_note
      ? `<div style=\"font-size:12px;color:var(--rd);margin-top:6px\">Reason: ${esc(u.approval_note)}</div>`
      : '';
    return `
    <div class=\"upload-row\" id=\"upl-${u.upload_id}\">
      <div class=\"ur-top\" onclick=\"toggleUpload('${esc(u.upload_id)}')\">
        <div style=\"flex:1;min-width:0\">
          <div class=\"ur-name\">${esc(u.list_name)}</div>
          <div class=\"ur-meta\">
            <span>By ${esc(u.uploaded_by)}</span>
            <span>${esc(fmtDate(u.uploaded_at))}</span>
            <span>${u.row_count} rows</span>
            ${u.job_id ? `<span>Job: ${esc(_jobNameCache[u.job_id] || u.job_id)}</span>` : ''}
            ${u.source_name ? `<span>${esc(u.source_name)}</span>` : ''}
          </div>
        </div>
        ${statusChip}
      </div>
      <div class=\"ur-body\">
        <div id=\"upl-hints-${u.upload_id}\"></div>
        <div class=\"upload-rows-table\" id=\"upl-rows-${u.upload_id}\"></div>
        ${actionBtns}
      </div>
    </div>`;
  }).join('');
}·
function toggleUpload(id) {
  const row = document.getElementById('upl-' + id);
  if (!row) return;
  const wasOpen = row.classList.contains('expanded');
  // Close others
  document.querySelectorAll('.upload-row.expanded').forEach(r => r.classList.remove('expanded'));
  if (!wasOpen) {
    row.classList.add('expanded');
    fillUploadBody(id);
  }
}·
function fillUploadBody(id) {
  const u = _uploads.find(x => x.upload_id === id);
  if (!u) return;
  const rows = Array.isArray(u.raw_rows) ? u.raw_rows : [];
  const hints = computeBindHints(rows);
  const matched   = hints.filter(h => h.matched).length;
  const unmatched = hints.length - matched;·
  const totalConflicts = hints.reduce((s, h) => s + (h.conflicts || []).length, 0);·
  const hintsEl = document.getElementById('upl-hints-' + id);
  hintsEl.innerHTML = `
    <div class=\"upload-match-hint\">✓ ${matched} row${matched !== 1 ? 's' : ''} match existing counters — blank fields will be filled</div>
    <div class=\"upload-match-hint partial\">⌖ ${unmatched} row${unmatched !== 1 ? 's' : ''} will create NEW counters on approval</div>
    ${totalConflicts > 0 ? `<div class=\"upload-match-hint\" style=\"background:rgba(249,115,22,.12);border-left-color:#f97316;color:#fed7aa\">⚡ ${totalConflicts} field conflict${totalConflicts !== 1 ? 's' : ''} — check boxes below to accept uploaded values (unchecked = keep existing)</div>` : ''}
    <div class=\"upload-match-hint partial\" style=\"margin-top:6px\">All approved rows will auto-allocate to <b>${esc(u.uploaded_by)}</b>.</div>`;·
  // Row-by-row table (better than raw JSON)
  const tableEl = document.getElementById('upl-rows-' + id);
  const preview = hints.slice(0, 30).map((h, i) => {
    const existing = h.matched ? allCounters.find(c => c.counter_id === h.matchedId) : null;
    const badge = h.matched
      ? `<span style=\"background:#92400e;color:#fde68a;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:700\">MATCH ${h.gstin ? 'GSTIN' : 'Name+Pin'}</span>`
      : `<span style=\"background:#064e3b;color:#6ee7b7;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:700\">NEW</span>`;
    const name = h.name || h.raw.display_name || '—';
    const city  = h.raw.city || '';
    const brands = parseBrandCodes(h.raw.brand_codes || h.raw.brand || '').join(', ') || '—';
    const matchInfo = existing
      ? `<div style=\"color:#fde68a;font-size:10px;margin-top:2px\">→ ${esc(existing.display_name)} (${esc(existing.city || '')})</div>`
      : '';
    const conflictHtml = h.conflicts && h.conflicts.length ? `
      <div class=\"upload-conflicts\">
        <div class=\"uc-hdr\">⚡ ${h.conflicts.length} field conflict${h.conflicts.length > 1 ? 's' : ''} — check to accept uploaded value:</div>
        ${h.conflicts.map(cf => `
          <label class=\"uc-row\">
            <input type=\"checkbox\" class=\"override-check\"
              id=\"ov_${esc(id)}_${i}_${cf.field}\"
              data-upload=\"${esc(id)}\" data-counter=\"${esc(h.matchedId)}\"
              data-field=\"${cf.field}\" data-val=\"${esc(cf.uploaded)}\">
            <div class=\"uc-content\">
              <span class=\"uc-label\">${esc(cf.label)}</span>
              <div class=\"uc-vals\">
                <span class=\"uc-old\">Current: ${esc(cf.existing)}</span>
                <span class=\"uc-new\">→ ${esc(cf.uploaded)}</span>
              </div>
            </div>
          </label>`).join('')}
      </div>` : '';
    return `<div style=\"padding:6px 0;border-bottom:1px solid rgba(255,255,255,.05)\">
      ${badge} <b>${esc(name)}</b> · ${esc(city)} · Brands: ${esc(brands)}
      ${matchInfo}
      ${conflictHtml}
    </div>`;
  }).join('');
  tableEl.innerHTML = preview + (hints.length > 30 ? `<div style=\"color:var(--muted);padding:4px 0\">…and ${hints.length - 30} more rows</div>` : '');
}·
function computeBindHints(rows) {
  const CONFLICT_FIELDS = [
    ['display_name','Name'], ['address_line1','Address 1'], ['address_line2','Address 2'],
    ['city','City'], ['state','State'], ['pincode','PIN'], ['gstin','GSTIN'],
    ['pan','PAN'], ['counter_type','Type'], ['channel','Channel'],
    ['customer_group','Group'], ['territory','Territory'],
    ['distributor_name','Distributor'], ['notes','Notes'],
  ];
  return rows.map(r => {
    const gstin = (r.gstin || r.GSTIN || r.GSTN || '').toString().trim().toUpperCase();
    const name  = (r.display_name || r.name || r.store_name || r['Store Name'] || '').toString().trim();
    const pin   = (r.pincode || r.PIN || r['PIN CODE'] || '').toString().trim();
    let matched = false, matchedId = null;
    if (gstin) {
      const m = allCounters.find(c => c.gstin && c.gstin.toUpperCase() === gstin);
      if (m) { matched = true; matchedId = m.counter_id; }
    }
    if (!matched && name && pin) {
      const m = allCounters.find(c => c.pincode === pin && (c.display_name || '').toLowerCase() === name.toLowerCase());
      if (m) { matched = true; matchedId = m.counter_id; }
    }
    const conflicts = [];
    if (matched && matchedId) {
      const existing = allCounters.find(c => c.counter_id === matchedId);
      if (existing) {
        for (const [field, label] of CONFLICT_FIELDS) {
          const eVal = (existing[field] || '').toString().trim();
          const nVal = (r[field] || '').toString().trim();
          if (eVal && nVal && eVal.toLowerCase() !== nVal.toLowerCase()) {
            conflicts.push({ field, label, existing: eVal, uploaded: nVal });
          }
        }
      }
    }
    return { matched, matchedId, gstin, name, pin, raw: r, conflicts };
  });
}·
async function approveUpload(id) {
  const u = _uploads.find(x => x.upload_id === id);
  if (!u) return;
  // Collect override checkbox selections from rendered diff UI
  const overrideMap = new Map(); // counter_id → { field: value }
  document.querySelectorAll(`.override-check[data-upload=\"${CSS.escape(id)}\"]:checked`).forEach(cb => {
    const cid = cb.dataset.counter;
    if (!overrideMap.has(cid)) overrideMap.set(cid, {});
    overrideMap.get(cid)[cb.dataset.field] = cb.dataset.val;
  });
  const overrideCount = [...overrideMap.values()].reduce((s, m) => s + Object.keys(m).length, 0);
  const overrideNote = overrideCount > 0 ? `\\n${overrideCount} field override${overrideCount > 1 ? 's' : ''} will be applied to matched counters.` : '';
  if (!confirm(`Approve ${u.row_count} row(s)?\\nNew counters will be created for unmatched rows and allocated to ${u.uploaded_by}.${overrideNote}`)) return;
  const hints = computeBindHints(Array.isArray(u.raw_rows) ? u.raw_rows : []);
  const newRows  = [];
  const newIds   = [];
  const matchIds = [];
  const matchedPatches = [];  // PATCH payloads for existing matched counters
  const contactRows = [];     // primary contacts to create from CSV data
  hints.forEach(h => {
    if (h.matched) {
      matchIds.push(h.matchedId);
      // Patch blank fields on the existing counter (fill-blanks strategy)
      const existing = allCounters.find(c => c.counter_id === h.matchedId) || {};
      const patch = {};
      const str = (v, max) => v ? v.toString().trim().slice(0, max) || null : null;
      if (!existing.legal_name       && h.raw.legal_name)       patch.legal_name       = str(h.raw.legal_name, 240);
      if (!existing.proprietor_name  && h.raw.proprietor_name)  patch.proprietor_name  = str(h.raw.proprietor_name, 240);
      if (!existing.address_line1    && (h.raw.address_line1 || h.raw.address))
                                                                 patch.address_line1    = str(h.raw.address_line1 || h.raw.address, 240);
      if (!existing.address_line2    && h.raw.address_line2)    patch.address_line2    = str(h.raw.address_line2, 240);
      if (!existing.city             && h.raw.city)             patch.city             = str(h.raw.city, 80);
      if (!existing.state            && h.raw.state)            patch.state            = str(h.raw.state, 60);
      if (!existing.pincode          && h.pin)                  patch.pincode          = h.pin;
      if (!existing.gstin            && h.gstin)                patch.gstin            = h.gstin;
      if (!existing.pan              && h.raw.pan)              patch.pan              = str(h.raw.pan, 20);
      if (!existing.counter_type     && h.raw.counter_type)     patch.counter_type     = str(h.raw.counter_type, 40);
      if (!existing.channel          && h.raw.channel)          patch.channel          = str(h.raw.channel, 40);
      if (!existing.customer_group   && h.raw.customer_group)   patch.customer_group   = str(h.raw.customer_group, 80);
      if (!existing.territory        && h.raw.territory)        patch.territory        = str(h.raw.territory, 80);
      if (!existing.distributor_name && h.raw.distributor_name) patch.distributor_name = str(h.raw.distributor_name, 120);
      // Merge new brand_codes (additive only)
      const newBrands = parseBrandCodes(h.raw.brand_codes || h.raw.brand || '');
      if (newBrands.length) {
        const merged = [...new Set([...(existing.brand_codes || []), ...newBrands])];
        if (merged.length !== (existing.brand_codes || []).length) patch.brand_codes = merged;
      }
      // Apply supervisor-approved overrides (overwrite even existing values when checkbox checked)
      const overrides = overrideMap.get(h.matchedId) || {};
      Object.assign(patch, overrides);
      if (Object.keys(patch).length) matchedPatches.push({ counter_id: h.matchedId, patch });
      // Add primary contact if CSV has one and existing counter has none
      const cName = str(h.raw.primary_contact_name || h.raw.contact_name, 120);
      const cPhone = str(h.raw.primary_contact_phone || h.raw.contact_phone || h.raw.mobile, 30);
      const existing2 = allCounters.find(c => c.counter_id === h.matchedId) || {};
      if (cName && !existing2.primary_contact_name) {
        contactRows.push({ counter_id: h.matchedId, role: 'primary', name: cName, phone: cPhone || null, is_primary: true });
      }
      return;
    }
    const cid = newId(h.name || 'counter');
    newIds.push(cid);
    const str = (v, max) => v ? v.toString().trim().slice(0, max) || null : null;
    newRows.push({
      counter_id:        cid,
      display_name:      h.raw.display_name ? str(h.raw.display_name, 240) : (h.name || 'Unnamed'),
      legal_name:        str(h.raw.legal_name, 240),
      proprietor_name:   str(h.raw.proprietor_name, 240),
      address_line1:     str(h.raw.address_line1 || h.raw.address || '—', 240),
      address_line2:     str(h.raw.address_line2, 240),
      city:              str(h.raw.city, 80),
      state:             str(h.raw.state, 60),
      pincode:           h.pin || null,
      gstin:             h.gstin || null,
      pan:               str(h.raw.pan, 20),
      fssai:             str(h.raw.fssai, 30),
      drug_lic:          str(h.raw.drug_lic, 60),
      counter_type:      str(h.raw.counter_type, 40),
      channel:           str(h.raw.channel, 40),
      customer_group:    str(h.raw.customer_group, 80),
      territory:         str(h.raw.territory, 80),
      distributor_name:  str(h.raw.distributor_name, 120),
      notes:             str(h.raw.notes, 500),
      brand_codes:       parseBrandCodes(h.raw.brand_codes || h.raw.brand || ''),
      created_by:        myId,
      created_from:      'upload',
      approval_status:   'approved',
      approved_by:       myId,
      approved_at:       new Date().toISOString(),
    });
    // Primary contact from CSV if provided
    const cName = str(h.raw.primary_contact_name || h.raw.contact_name, 120);
    const cPhone = str(h.raw.primary_contact_phone || h.raw.contact_phone || h.raw.mobile, 30);
    if (cName) contactRows.push({ counter_id: cid, role: 'primary', name: cName, phone: cPhone || null, is_primary: true });
  });
  try {
    if (newRows.length) {
      await api('/counter', { method: 'POST', body: JSON.stringify(newRows), headers: { Prefer: 'return=minimal' } });
    }
    // Create primary contacts for new counters (and fill-blanks for matched counters without a contact)
    if (contactRows.length) {
      await api('/contact', {
        method: 'POST', body: JSON.stringify(contactRows), headers: { Prefer: 'return=minimal' },
      }).catch(() => {});
    }
    // Patch blank fields on matched counters (fill-blanks + brand merge)
    for (const mp of matchedPatches) {
      await api(`/counter?counter_id=eq.${encodeURIComponent(mp.counter_id)}`, {
        method: 'PATCH', body: JSON.stringify(mp.patch), headers: { Prefer: 'return=minimal' },
      }).catch(() => {});
    }
    // Write brand associations to counter_brand table (if migration v19 has been run)
    const brandAssocRows = [
      ...newRows.flatMap(r => (r.brand_codes || []).map(bc => ({
        counter_id: r.counter_id, brand_code: bc, source: 'upload', added_by: myId,
      }))),
      ...matchedPatches.flatMap(mp => (mp.patch.brand_codes || []).map(bc => ({
        counter_id: mp.counter_id, brand_code: bc, source: 'upload', added_by: myId,
      }))),
    ];
    if (brandAssocRows.length) {
      await api('/counter_brand', {
        method: 'POST', body: JSON.stringify(brandAssocRows),
        headers: { Prefer: 'return=minimal,resolution=ignore-duplicates' },
      }).catch(() => {}); // silently ignore if table doesn't exist yet
    }
    // Auto-allocate ALL touched counters to the uploader
    const allCids = [...matchIds, ...newIds];
    if (allCids.length) {
      const rows = allCids.map(cid => ({
        employee_id: u.uploaded_by, counter_id: cid, job_id: u.job_id || null,
        allocated_by: myId, source: 'upload_auto',
      }));
      // Use ON CONFLICT via duplicate-tolerant approach — try insert; ignore duplicate errors silently
      try {
        await api('/allocation', { method: 'POST', body: JSON.stringify(rows), headers: { Prefer: 'return=minimal,resolution=ignore-duplicates' } });
      } catch (_) { /* most duplicates surface via index conflict — silenced */ }
    }
    // Unblock submissions waiting on this upload
    await api(`/submissions?pending_upload_id=eq.${id}`, {
      method: 'PATCH', body: JSON.stringify({ pending_supervisor_approval: false, pending_upload_id: null }),
      headers: { 'Content-Profile': 'recce', 'Accept-Profile': 'recce', Prefer: 'return=minimal' },
    }).catch(() => {});
    // Mark upload approved
    await api(`/upload?upload_id=eq.${encodeURIComponent(id)}`, {
      method: 'PATCH',
      body: JSON.stringify({
        approval_status: 'approved', approved_by: myId, approved_at: new Date().toISOString(),
        promoted_counter_ids: newIds, matched_counter_ids: matchIds,
      }),
      headers: { Prefer: 'return=minimal' },
    });
    toast(`Approved — ${newIds.length} new, ${matchIds.length} matched`, 'ok');
    await loadUploads();
    await loadList();
  } catch (e) {
    toast('Approve failed: ' + e.message, 'err');
  }
}·
async function rejectUpload(id) {
  const note = prompt('Optional reason for rejection (blank to skip):', '') || null;
  try {
    await api(`/upload?upload_id=eq.${encodeURIComponent(id)}`, {
      method: 'PATCH',
      body: JSON.stringify({
        approval_status: 'rejected', approved_by: myId, approved_at: new Date().toISOString(), approval_note: note,
      }),
      headers: { Prefer: 'return=minimal' },
    });
    toast('Rejected', 'ok');
    await loadUploads();
  } catch (e) {
    toast('Reject failed: ' + e.message, 'err');
  }
}·
// ── Update banner + SW ────────────────────────────────────
function checkUpdate() {
  const seen = localStorage.getItem('counters-ver-seen');
  localStorage.setItem('counters-ver-seen', CACHE_VER);
  if (seen && seen !== CACHE_VER) document.getElementById('upd-banner').classList.add('show');
}·
function registerSW() {
  if (!('serviceWorker' in navigator)) return;
  const sw = `
const C='${CACHE_VER}';
self.addEventListener('install',e=>{self.skipWaiting();});
self.addEventListener('activate',e=>{
  e.waitUntil(caches.keys().then(ks=>Promise.all(ks.filter(k=>k!==C).map(k=>caches.delete(k)))));
  self.clients.claim();
});
self.addEventListener('fetch',e=>{
  if(e.request.method!=='GET')return;
  const url=new URL(e.request.url);
  if(url.pathname.startsWith('/db'))return;
  if(url.pathname.startsWith('/counters/')){
    e.respondWith(
      caches.open(C).then(c=>c.match(e.request).then(r=>r||fetch(e.request).then(f=>{c.put(e.request,f.clone());return f;})))
    );
  }
});`;
  const blob = new Blob([sw], { type: 'application/javascript' });
  navigator.serviceWorker.register(URL.createObjectURL(blob), { scope: '/counters/' }).catch(() => {});
}·
// ── Boot ──────────────────────────────────────────────────
async function boot() {
  if (!bootSession()) return;
  checkUpdate();
  registerSW();
  showScreen('s-list');
  await loadList();
  if (isSupervisor()) { refreshUploadBadge(); refreshMultiRecceBadge(); refreshClientsBadge(); refreshNotifyBadge(); refreshAuditBadge(); }
  else { refreshFieldRepUploadBadge(); }
  // Deep-link: /counters/?counter_id=XXX opens that counter directly
  const _dlParams = new URLSearchParams(location.search);
  const _dlId = _dlParams.get('counter_id');
  if (_dlId) { history.replaceState({}, '', location.pathname); openDetail(_dlId); }
  // Deep-link: /counters/?new=1&name=...&city=...&address=...&mobile=... opens New Counter pre-filled
  else if (_dlParams.get('new') === '1' && isSupervisor()) {
    history.replaceState({}, '', location.pathname);
    openNew({
      name:    _dlParams.get('name')    || '',
      address: _dlParams.get('address') || '',
      city:    _dlParams.get('city')    || '',
      mobile:  _dlParams.get('mobile')  || '',
      notes:   _dlParams.get('notes')   || '',
    });
  }
}·
// ── Onboarding tour ──────────────────────────────────────────
const TOUR_STEPS = [
  {
    emoji: '🗺️',
    title: 'Supervisor Guide',
    body: 'This tour walks through the full client-allotment journey: managing counters, setting up brand clients, inviting client users, and the client portal.\\n\\nTap Next to continue or Skip tour to close.'
  },
  {
    emoji: '📋',
    title: 'The Counter List',
    body: 'The main screen shows all your counters. Use the filter tiles (All / Active / Pending / Closed) and the search bar to find any outlet quickly. Tap any row to open its detail.',
    screenFn: () => showScreen('s-list')
  },
  {
    emoji: '🏪',
    title: 'Counter Detail',
    body: 'Each counter profile shows address, territory, GPS pin, brand affiliations, contacts, and all past Recce visits. Supervisors can allocate agents and edit counter info here.',
    screenFn: () => showScreen('s-list')
  },
  {
    emoji: '➕',
    title: 'Adding a New Counter',
    body: 'Tap \"+ New\" in the header to register a new outlet. Fill in the display name, address, territory, GPS co-ordinates, and brand codes. Every Recce visit is linked back to a counter.',
    screenFn: () => showScreen('s-list')
  },
  {
    emoji: '🏷️',
    title: 'Brand Clients',
    body: 'Tap \"🏷 Clients\" in the header to open the Clients section — visible to supervisors only. Each client represents a brand company (like HP, Lenovo, or KFC) whose products are placed in your counters.',
    screenFn: () => showScreen('s-clients')
  },
  {
    emoji: '🏢',
    title: 'What Is a Client?',
    body: 'A client has a name, one or more brand codes (e.g. HP), and an optional contact email.\\n\\nBrand codes must match exactly what agents enter in Recce submissions — this is how the portal filters which Recces each client can see.',
    screenFn: () => showScreen('s-clients')
  },
  {
    emoji: '🆕',
    title: 'Creating a New Client',
    body: 'Tap \"+ Client\" to register a brand client. Set the brand code carefully — capitalisation must match the brand used in Recce submissions (e.g. \"HP\" not \"hp\"). Multiple brand codes are allowed.',
    screenFn: () => showScreen('s-clients')
  },
  {
    emoji: '✉️',
    title: 'Inviting a Client User',
    body: 'Expand any client row and tap \"✉ Invite user\". Enter the person\\'s email, choose a role (SM, ASM, etc.), and set their scope.\\n\\nA magic link is sent to their email and stays valid for 7 days.',
    screenFn: () => showScreen('s-clients')
  },
  {
    emoji: '🔭',
    title: 'Controlling What Clients See',
    body: 'Scope decides which Recces a client user can access:\\n• All — every Recce for their brand\\n• By territory — specific zones only\\n• By job_id — a specific campaign\\n• By Recce ID — handpick individual reports\\n\\nUse \"All\" when counters have no territory assigned.',
    screenFn: () => showScreen('s-clients')
  },
  {
    emoji: '🌐',
    title: 'The Client Portal',
    body: 'When the client opens their magic link they land on the Recce Client Portal (/recce-client/). They can approve, request changes, or reject each Recce.\\n\\nResponses are saved instantly and visible to supervisors in the DB.'
  },
  {
    emoji: '🤖',
    title: 'AI Import — Bulk Add Counters',
    body: 'The \"🤖 AI Import\" button (Uploads screen, supervisors only) converts any raw store list — WhatsApp forwards, Excel paste, typed notes — into the counter upload format automatically.\\n\\nNo CSV template needed: just paste and convert.',
    screenFn: () => showScreen('s-uploads')
  },
  {
    emoji: '📋',
    title: 'Paste Any Format',
    body: 'AI Import accepts virtually any layout:\\n• WhatsApp-forwarded shop lists\\n• Excel copy-paste (columns or rows)\\n• Typed lists with mixed formats\\n• GSTIN / phone / pincode scattered anywhere\\n\\nPaste the raw text into the box and tap \"Convert with AI\".',
    screenFn: () => { showScreen('s-uploads'); openAiImport(); }
  },
  {
    emoji: '⚡',
    title: 'AI Extracts Every Field',
    body: 'The AI picks out:\\n• Store name, owner, address\\n• City, state (abbreviations expanded)\\n• Pincode, GSTIN, PAN\\n• Phone (stripped to 10 digits)\\n• Counter type inferred from context\\n\\nConverts 3–50 stores in 15–30 seconds via OAuth.',
    screenFn: () => { showScreen('s-uploads'); openAiImport(); }
  },
  {
    emoji: '✅',
    title: 'Review Before Upload',
    body: 'After conversion, a preview shows every extracted row — name, city, state, phone, counter type.\\n\\nCheck the preview, then tap \"Use these rows →\" to feed them into the existing upload queue. A supervisor approves the batch before counters are created.',
    screenFn: () => { showScreen('s-uploads'); openAiImport(); }
  },
  {
    emoji: '🔑',
    title: 'AI Route & Fallback',
    body: 'AI Import tries routes in order:\\n1️⃣ Claude OAuth (free, via Claude Code on the server)\\n2️⃣ OpenRouter (free-tier models as backup)\\n3️⃣ Anthropic API key — only if you explicitly approve when prompted\\n\\nFor large lists (50+ stores) it splits into batches automatically.'
  },
  {
    emoji: '⚡',
    title: 'Bulk Edit Counters',
    body: 'The \"⚡ Bulk\" button (supervisor only) applies a change to all counters matching your current filter in one shot.\\n\\nUse it to:\\n• Assign a brand code to hundreds of counters at once\\n• Set counter type or territory across a filtered set\\n• No existing values are overwritten — brands are merged, other fields fill blanks only.',
    screenFn: () => { showScreen('s-list'); openBulkEdit(); }
  }
];·
initTour(TOUR_STEPS, { storageKey: 'tour-counters-v1', autoShow: false });·
boot();·
// ── AI Geo-fill ────────────────────────────────────────────────────────────
const STATE_CODE_MAP = {
  'andhra pradesh':'AP','arunachal pradesh':'AR','assam':'AS','bihar':'BR',
  'chandigarh':'CH','chhattisgarh':'CG','delhi':'DL','goa':'GA','gujarat':'GJ',
  'haryana':'HR','himachal':'HP','himachal pradesh':'HP','j&k':'JK',
  'jammu & kashmir':'JK','jammu and kashmir':'JK','jharkhand':'JH',
  'karnataka':'KA','kerala':'KL','ladakh':'LA','madhya pradesh':'MP',
  'maharashtra':'MH','manipur':'MN','meghalaya':'ML','mizoram':'MZ',
  'nagaland':'NL','odisha':'OD','puducherry':'PY','punjab':'PB',
  'rajasthan':'RJ','sikkim':'SK','tamil nadu':'TN','telangana':'TS',
  'tripura':'TR','uttar pradesh':'UP','uttarakhand':'UK','west bengal':'WB',
};·
const CITY_TIER_MAP = {
  // Tier 1 – national metros
  'delhi':'tier1','mumbai':'tier1','bangalore':'tier1','bengaluru':'tier1',
  'chennai':'tier1','kolkata':'tier1','hyderabad':'tier1','pune':'tier1','ahmedabad':'tier1',
  // Tier 2 – state capitals + major cities
  'chandigarh':'tier2','ludhiana':'tier2','amritsar':'tier2','jalandhar':'tier2',
  'patiala':'tier2','faridabad':'tier2','gurugram':'tier2','gurgaon':'tier2',
  'rohtak':'tier2','hisar':'tier2','ambala':'tier2','karnal':'tier2','panipat':'tier2',
  'shimla':'tier2','jammu':'tier2','srinagar':'tier2','dehradun':'tier2',
  'jaipur':'tier2','lucknow':'tier2','bhopal':'tier2','indore':'tier2',
  'nagpur':'tier2','coimbatore':'tier2','visakhapatnam':'tier2','kochi':'tier2',
  'surat':'tier2','vadodara':'tier2','rajkot':'tier2','patna':'tier2',
  // Tier 3 – district HQs / medium towns
  'bathinda':'tier3','moga':'tier3','hoshiarpur':'tier3','gurdaspur':'tier3',
  'sangrur':'tier3','ferozepur':'tier3','muktsar':'tier3','nawanshahr':'tier3',
  'rupnagar':'tier3','ropar':'tier3','barnala':'tier3','mansa':'tier3',
  'mohali':'tier3','sas nagar':'tier3','panchkula':'tier3','sonipat':'tier3',
  'bhiwani':'tier3','jind':'tier3','kaithal':'tier3','kurukshetra':'tier3',
  'sirsa':'tier3','rewari':'tier3','palwal':'tier3','nuh':'tier3',
  'solan':'tier3','mandi':'tier3','dharamshala':'tier3','kangra':'tier3',
  'kullu':'tier3','bilaspur':'tier3','hamirpur':'tier3','una':'tier3',
  'chamba':'tier3','palampur':'tier3','nurpur':'tier3','baddi':'tier3',
  'baramullah':'tier3','baramulla':'tier3','anantnag':'tier3','pulwama':'tier3',
  'kathua':'tier3','udhampur':'tier3','rajouri':'tier3',
};·
let _geoSuggestions = []; // {counter_id, display_name, city, state, cur_sc, sug_sc, cur_tier, sug_tier, cur_cluster, sug_cluster, source, checked}·
function gfCounterPick() {
  const sel = document.getElementById('gf-counter-sel');
  const c   = allCounters.find(x => x.counter_id === sel.value);
  if (c) {
    document.getElementById('gf-city').value  = c.city  || '';
    document.getElementById('gf-state').value = c.state || '';
  }
}·
async function gfAskAI() {
  const sel     = document.getElementById('gf-counter-sel');
  const city    = document.getElementById('gf-city').value.trim();
  const state   = document.getElementById('gf-state').value.trim();
  const statusEl= document.getElementById('gf-ask-status');
  const btn     = document.getElementById('btn-gf-ask');
  const cid     = sel.value;
  const cName   = sel.options[sel.selectedIndex]?.text || cid;·
  if (!cid)  { statusEl.textContent = '⚠ Select a counter first'; return; }
  if (!city) { statusEl.textContent = '⚠ Enter a city name'; return; }·
  btn.disabled = true;
  btn.textContent = '⟳ Asking…';
  statusEl.style.color = 'var(--muted)';
  statusEl.textContent = 'Sending to AI…';·
  try {
    const resp = await fetch(AI_CONVERT_URL + 'geo', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + AI_CONVERT_SECRET },
      body: JSON.stringify({ counters: [{ counter_id: cid, display_name: cName, city, state }] }),
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const data = await resp.json();
    const sug  = (data.suggestions || [])[0];
    if (!sug) throw new Error('No suggestion returned');·
    const c = allCounters.find(x => x.counter_id === cid);
    const needsSC   = sug.state_code  && sug.state_code  !== (c?.state_code  || '').toUpperCase();
    const needsTier = sug.city_tier   && sug.city_tier   !== (c?.city_tier   || '');
    const needsClu  = sug.geo_cluster && sug.geo_cluster !== (c?.geo_cluster  || '');
    if (!needsSC && !needsTier && !needsClu) {
      statusEl.style.color = 'var(--g)';
      statusEl.textContent = '✓ AI agrees — no changes needed for this counter';
    } else {
      // Remove any existing suggestion for this counter, then add fresh AI one
      _geoSuggestions = _geoSuggestions.filter(s => s.counter_id !== cid);
      _geoSuggestions.push({
        counter_id: cid, display_name: cName, city, state,
        cur_sc: c?.state_code || '', sug_sc: needsSC ? sug.state_code : null,
        cur_tier: c?.city_tier || '', sug_tier: needsTier ? sug.city_tier : null,
        cur_cluster: c?.geo_cluster || '', sug_cluster: needsClu ? sug.geo_cluster : null,
        source: 'ai', checked: true,
      });
      renderGeoFillTable();
      document.getElementById('btn-geofill-apply').disabled = false;
      statusEl.style.color = 'var(--g)';
      statusEl.textContent = `✓ Added suggestion for ${cName}`;
    }
  } catch (e) {
    statusEl.style.color = 'var(--err)';
    statusEl.textContent = '⚠ AI call failed: ' + e.message;
  } finally {
    btn.disabled = false;
    btn.textContent = 'Ask AI';
  }
}·
async function openGeoFill() {
  _geoSuggestions = [];
  const el    = document.getElementById('geofill-table');
  const stEl  = document.getElementById('geofill-status');
  const apBtn = document.getElementById('btn-geofill-apply');
  el.innerHTML = '<div style=\"color:var(--muted);font-size:12px;padding:12px\">Scanning counters…</div>';
  stEl.textContent = '';
  apBtn.disabled = true;
  document.getElementById('gf-ask-status').textContent = '';
  document.getElementById('btn-gf-ask').disabled = false;
  document.getElementById('btn-gf-ask').textContent = 'Ask AI';·
  // Populate counter dropdown
  const sel = document.getElementById('gf-counter-sel');
  sel.innerHTML = '<option value=\"\">— pick a counter —</option>' +
    allCounters
      .filter(c => c.city)
      .sort((a, b) => a.display_name.localeCompare(b.display_name))
      .map(c => `<option value=\"${esc(c.counter_id)}\">${esc(c.display_name)} · ${esc(c.city||'')}${c.state?', '+esc(c.state):''}</option>`)
      .join('');
  document.getElementById('gf-city').value  = '';
  document.getElementById('gf-state').value = '';·
  document.getElementById('modal-geofill').classList.add('show');·
  // ── Pass 1: static lookup (instant) ──────────────────────
  for (const c of allCounters) {
    const stateKey  = (c.state || '').toLowerCase().trim();
    const cityKey   = (c.city  || '').toLowerCase().trim();
    const sugSC     = STATE_CODE_MAP[stateKey]  || null;
    const sugTier   = CITY_TIER_MAP[cityKey]    || null;
    const needsSC   = sugSC   && sugSC   !== (c.state_code || '').toUpperCase();
    const needsTier = sugTier && sugTier !== c.city_tier;
    if (!needsSC && !needsTier) continue;
    _geoSuggestions.push({
      counter_id: c.counter_id, display_name: c.display_name, city: c.city, state: c.state,
      cur_sc: c.state_code || '', sug_sc: needsSC ? sugSC : null,
      cur_tier: c.city_tier || '', sug_tier: needsTier ? sugTier : null,
      cur_cluster: c.geo_cluster || '', sug_cluster: null,
      source: 'static', checked: true,
    });
  }·
  // ── Pass 2: AI for counters missing geo_cluster ───────────
  const aiTargets = allCounters.filter(c => c.city && !c.geo_cluster);
  if (aiTargets.length) {
    el.innerHTML = `<div style=\"color:var(--muted);font-size:12px;padding:12px\">
      ✨ Pass 1 done (${_geoSuggestions.length} static). 🤖 Asking AI for geo_cluster on ${aiTargets.length} counters…
      <span style=\"display:inline-block;margin-left:6px;animation:spin 1s linear infinite\">⟳</span></div>`;
    try {
      const resp = await fetch(AI_CONVERT_URL + 'geo', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + AI_CONVERT_SECRET },
        body: JSON.stringify({ counters: aiTargets.map(c => ({ counter_id: c.counter_id, display_name: c.display_name, city: c.city || '', state: c.state || '' })) }),
      });
      if (resp.ok) {
        const data = await resp.json();
        for (const sug of (data.suggestions || [])) {
          if (!sug.geo_cluster && !sug.state_code && !sug.city_tier) continue;
          const c = allCounters.find(x => x.counter_id === sug.counter_id);
          if (!c) continue;
          const existing = _geoSuggestions.find(s => s.counter_id === sug.counter_id);
          if (existing) {
            if (sug.geo_cluster && sug.geo_cluster !== (c.geo_cluster || '')) existing.sug_cluster = sug.geo_cluster;
          } else {
            const needsSC  = sug.state_code  && sug.state_code  !== (c.state_code || '').toUpperCase();
            const needsTier= sug.city_tier   && sug.city_tier   !== c.city_tier;
            const needsClu = sug.geo_cluster && sug.geo_cluster !== (c.geo_cluster || '');
            if (!needsSC && !needsTier && !needsClu) continue;
            _geoSuggestions.push({
              counter_id: c.counter_id, display_name: c.display_name, city: c.city, state: c.state,
              cur_sc: c.state_code || '', sug_sc: needsSC ? sug.state_code : null,
              cur_tier: c.city_tier || '', sug_tier: needsTier ? sug.city_tier : null,
              cur_cluster: c.geo_cluster || '', sug_cluster: needsClu ? sug.geo_cluster : null,
              source: 'ai', checked: true,
            });
          }
        }
      }
    } catch (e) { console.warn('[geo-fill] AI pass failed:', e); }
  }·
  if (!_geoSuggestions.length) {
    el.innerHTML = '<div style=\"color:var(--g);font-size:12px;padding:12px\">✓ All counters already have up-to-date geo fields.</div>';
    return;
  }
  renderGeoFillTable();
  apBtn.disabled = false;
}·
function renderGeoFillTable() {
  const el = document.getElementById('geofill-table');
  const checked = _geoSuggestions.filter(s => s.checked).length;
  document.getElementById('geofill-status').textContent = `${checked} of ${_geoSuggestions.length} changes selected`;
  const diffCell = (cur, sug) => sug
    ? `<span style=\"color:var(--muted);text-decoration:line-through\">${esc(cur)||'—'}</span> → <b style=\"color:var(--g)\">${esc(sug)}</b>`
    : `<span style=\"color:var(--muted)\">${esc(cur)||'—'}</span>`;
  el.innerHTML = `<table style=\"width:100%;border-collapse:collapse;font-size:11px\">
    <thead><tr style=\"background:var(--card2)\">
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">✓</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">Counter</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">City / State</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">SC</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">Tier</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">Cluster</th>
      <th style=\"padding:5px 4px;text-align:left;color:var(--muted)\">Src</th>
    </tr></thead><tbody>` +
    _geoSuggestions.map((s, i) => `
      <tr style=\"border-top:1px solid var(--border)\">
        <td style=\"padding:4px\"><input type=\"checkbox\" ${s.checked ? 'checked' : ''} onchange=\"geoToggle(${i},this.checked)\"></td>
        <td style=\"padding:4px;font-weight:600;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">${esc(s.display_name)}</td>
        <td style=\"padding:4px;color:var(--muted);white-space:nowrap\">${esc(s.city||'')}${s.state?', '+esc(s.state):''}</td>
        <td style=\"padding:4px\">${diffCell(s.cur_sc, s.sug_sc)}</td>
        <td style=\"padding:4px\">${diffCell(s.cur_tier, s.sug_tier)}</td>
        <td style=\"padding:4px\">${diffCell(s.cur_cluster, s.sug_cluster)}</td>
        <td style=\"padding:4px\"><span style=\"font-size:10px;padding:1px 5px;border-radius:10px;background:${s.source==='ai'?'var(--accent)':'var(--card2)'};color:${s.source==='ai'?'#fff':'var(--muted)'}\">${s.source==='ai'?'🤖':'✨'}</span></td>
      </tr>`).join('') +
    '</tbody></table>';
}·
function geoToggle(i, v) {
  _geoSuggestions[i].checked = v;
  const checked = _geoSuggestions.filter(s => s.checked).length;
  document.getElementById('geofill-status').textContent = `${checked} of ${_geoSuggestions.length} changes selected`;
  document.getElementById('btn-geofill-apply').disabled = checked === 0;
}·
async function applyGeoFill() {
  const targets = _geoSuggestions.filter(s => s.checked);
  if (!targets.length) return;
  const btn = document.getElementById('btn-geofill-apply');
  btn.disabled = true;
  btn.textContent = 'Applying…';
  let ok = 0, fail = 0;
  for (const s of targets) {
    const patch = {};
    if (s.sug_sc)      patch.state_code  = s.sug_sc;
    if (s.sug_tier)    patch.city_tier   = s.sug_tier;
    if (s.sug_cluster) patch.geo_cluster = s.sug_cluster;
    if (!Object.keys(patch).length) continue;
    try {
      await api(`/counter?counter_id=eq.${encodeURIComponent(s.counter_id)}`, {
        method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify(patch),
      });
      ok++;
      const c = allCounters.find(x => x.counter_id === s.counter_id);
      if (c) { if (patch.state_code) c.state_code = patch.state_code; if (patch.city_tier) c.city_tier = patch.city_tier; if (patch.geo_cluster) c.geo_cluster = patch.geo_cluster; }
    } catch { fail++; }
  }
  closeModal('modal-geofill');
  toast(`Geo-fill done — ${ok} updated${fail ? `, ${fail} failed` : ''}`, ok ? 'ok' : 'err');
  if (ok) renderList();
}
</script>·
<!-- ── MODAL: AI GEO-FILL ── -->
<div class=\"modal\" id=\"modal-geofill\">
  <div class=\"modal-box\" style=\"max-height:90vh;display:flex;flex-direction:column\">
    <div class=\"modal-hdr\">
      <div class=\"modal-ttl\">✨ AI Geo-fill</div>
      <button class=\"modal-close\" onclick=\"closeModal('modal-geofill')\">×</button>
    </div>·
    <!-- Manual lookup panel (human in loop) -->
    <div style=\"background:var(--card2);border:1px solid var(--border);border-radius:8px;padding:10px;margin-bottom:10px\">
      <div style=\"font-size:11px;font-weight:700;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:.4px\">🤖 Ask AI for any counter</div>
      <div style=\"display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end\">
        <div style=\"flex:2;min-width:120px\">
          <div style=\"font-size:10px;color:var(--muted);margin-bottom:2px\">Counter</div>
          <select id=\"gf-counter-sel\" onchange=\"gfCounterPick()\" style=\"width:100%;font-size:11px;padding:4px 6px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:inherit\"></select>
        </div>
        <div style=\"flex:1;min-width:80px\">
          <div style=\"font-size:10px;color:var(--muted);margin-bottom:2px\">City</div>
          <input id=\"gf-city\" placeholder=\"city\" style=\"width:100%;font-size:11px;padding:4px 6px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:inherit;box-sizing:border-box\">
        </div>
        <div style=\"flex:1;min-width:80px\">
          <div style=\"font-size:10px;color:var(--muted);margin-bottom:2px\">State</div>
          <input id=\"gf-state\" placeholder=\"state\" style=\"width:100%;font-size:11px;padding:4px 6px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-family:inherit;box-sizing:border-box\">
        </div>
        <button class=\"btn btn-p\" id=\"btn-gf-ask\" onclick=\"gfAskAI()\" style=\"font-size:11px;padding:5px 14px;white-space:nowrap\">Ask AI</button>
      </div>
      <div id=\"gf-ask-status\" style=\"font-size:11px;min-height:16px;margin-top:6px;color:var(--muted)\"></div>
    </div>·
    <!-- Auto-scan results table -->
    <div style=\"font-size:11px;color:var(--muted);margin-bottom:4px\">Review suggestions — uncheck any you disagree with</div>
    <div id=\"geofill-table\" style=\"overflow-y:auto;flex:1\"></div>
    <div id=\"geofill-status\" style=\"font-size:12px;min-height:18px;margin:8px 0;color:var(--muted)\"></div>
    <div class=\"btn-row\" style=\"margin-top:4px\">
      <button class=\"btn btn-s\" onclick=\"closeModal('modal-geofill')\">Cancel</button>
      <button class=\"btn btn-p\" id=\"btn-geofill-apply\" onclick=\"applyGeoFill()\" disabled>Apply selected changes</button>
    </div>
  </div>
</div>·
<script>
/* ── ADR-143: Client and Brand come from the shared `master` schema ──────────────
   Self-contained and additive: if the master cannot be reached the page keeps
   whatever it already showed, so this is never worse than before it existed. ── */
(function () {
  const MRD = { 'Accept-Profile': 'master' };
  const _esc = n => String(n == null ? '' : n).replace(/[&<>\"]/g,
    c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));
  async function loadMaster() {
    try {
      const [rb, rc] = await Promise.all([
        fetch('/db/brands?select=brand_code,name&is_active=eq.true&order=sort_order', { headers: MRD }),
        fetch('/db/clients?select=name&is_active=eq.true&order=name', { headers: MRD }),
      ]);
      if (!rb.ok || !rc.ok) return null;
      return { brands: await rb.json(), clients: await rc.json() };
    } catch (_) { return null; }
  }
  async function apply() {
    const m = await loadMaster();
    if (!m) return;
    window.MASTER_BRANDS = m.brands;
    window.MASTER_CLIENTS = m.clients;
        window.MASTER_BRAND_CODES = m.brands.map(b => b.brand_code).filter(Boolean);
  }
  if (document.readyState === 'loading') addEventListener('DOMContentLoaded', apply);
  else apply();
})();
</script>
</body>
</html>
"
```

# Test source

```ts
  129 |     expect(html).toContain('id="btn-allocate"');
  130 |     expect(html).toContain('id="btn-uploads"');
  131 |     expect(html).toContain('id="alloc-tiles"');
  132 |   });
  133 | 
  134 |   test('Phase 3 — Multi-Recce review screen + pick official present', () => {
  135 |     expect(html).toContain('id="s-multirecce"');
  136 |     expect(html).toContain('id="btn-multirecce"');
  137 |     expect(html).toContain('id="multirecce-badge"');
  138 |     expect(html).toContain('pickOfficial(');
  139 |     expect(html).toContain('loadMultiRecce()');
  140 |     expect(html).toContain('mark_official_recce');
  141 |     expect(html).toContain('toggleMrg(');
  142 |   });
  143 | 
  144 |   test('Phase 4.5 — clients admin screen + modals + RPCs referenced', () => {
  145 |     expect(html).toContain('id="s-clients"');
  146 |     expect(html).toContain('id="btn-clients"');
  147 |     expect(html).toContain('id="modal-newclient"');
  148 |     expect(html).toContain('id="modal-invite"');
  149 |     expect(html).toContain('openClients()');
  150 |     expect(html).toContain('saveNewClient()');
  151 |     expect(html).toContain('saveInvite()');
  152 |     expect(html).toContain('admin_list_client_users');
  153 |     expect(html).toContain('admin_list_invites');
  154 |     expect(html).toContain('admin_revoke_client_user');
  155 |     expect(html).toContain('admin_revoke_invite');
  156 |     expect(html).toContain('client_admin_v');
  157 |   });
  158 | 
  159 |   test('Phase 4.6 — notifications screen + RPCs referenced', () => {
  160 |     expect(html).toContain('id="s-notify"');
  161 |     expect(html).toContain('id="btn-notify"');
  162 |     expect(html).toContain('id="notify-badge"');
  163 |     expect(html).toContain('openNotify()');
  164 |     expect(html).toContain('renderNotify');
  165 |     expect(html).toContain('admin_list_notifications');
  166 |     expect(html).toContain('admin_mark_notification_seen');
  167 |     expect(html).toContain('🔔 Notifications');
  168 |   });
  169 | 
  170 |   test('Phase 4.7 — audit screen + RPCs referenced', () => {
  171 |     expect(html).toContain('id="s-audit"');
  172 |     expect(html).toContain('id="btn-audit"');
  173 |     expect(html).toContain('id="audit-badge"');
  174 |     expect(html).toContain('openAudit()');
  175 |     expect(html).toContain('renderAudit');
  176 |     expect(html).toContain('admin_list_audit');
  177 |     expect(html).toContain('client_audit_log');
  178 |     expect(html).toContain('📋 Audit log');
  179 |   });
  180 | 
  181 |   test('Phase 4.8 — sessions modal + RPCs referenced', () => {
  182 |     expect(html).toContain('id="modal-sessions"');
  183 |     expect(html).toContain('openUserSessions(');
  184 |     expect(html).toContain('admin_list_sessions');
  185 |     expect(html).toContain('admin_revoke_session');
  186 |     expect(html).toContain('🔑 Sessions');
  187 |   });
  188 | 
  189 |   test('Phase 4.11 — risk chip styles + audit risk filter + suspicious_login meta', () => {
  190 |     expect(html).toContain('au-risk');
  191 |     expect(html).toContain('au-risk.low');
  192 |     expect(html).toContain('au-risk.medium');
  193 |     expect(html).toContain('au-risk.high');
  194 |     expect(html).toContain('id="audit-risk"');
  195 |     expect(html).toContain("'suspicious_login'");
  196 |     expect(html).toContain('Suspicious login');
  197 |   });
  198 | 
  199 |   test('Phase 4.12 — audit export + prune UI wired', () => {
  200 |     expect(html).toContain('id="audit-from"');
  201 |     expect(html).toContain('id="audit-to"');
  202 |     expect(html).toContain('exportAuditCsv()');
  203 |     expect(html).toContain('pruneAuditNow()');
  204 |     expect(html).toContain('admin_prune_audit');
  205 |     expect(html).toContain('Retention: rows older than 180 days');
  206 |   });
  207 | 
  208 |   test('Phase 4.14 — audit stats modal + RPC referenced', () => {
  209 |     expect(html).toContain('id="modal-stats"');
  210 |     expect(html).toContain('openAuditStats()');
  211 |     expect(html).toContain('admin_audit_stats');
  212 |     expect(html).toContain('📊 Stats');
  213 |     expect(html).toContain('logins_by_risk');
  214 |     expect(html).toContain('response_mix');
  215 |   });
  216 | 
  217 |   test('Phase 4.15 — supervisor_weekly_digest event meta wired', () => {
  218 |     expect(html).toContain("'supervisor_weekly_digest'");
  219 |     expect(html).toContain('Weekly digest sent');
  220 |   });
  221 | 
  222 |   test('Phase 4.17 — comments modal + RPCs referenced', () => {
  223 |     expect(html).toContain('id="modal-comments"');
  224 |     expect(html).toContain('openCommentsModal(');
  225 |     expect(html).toContain('postSupervisorComment');
  226 |     expect(html).toContain('list_comments_for_sub');
  227 |     expect(html).toContain('add_comment_as_supervisor');
  228 |     expect(html).toContain('Recce comment');
> 229 |     expect(html).toContain('💬 Comments');
      |                  ^ Error: expect(received).toContain(expected) // indexOf
  230 |   });
  231 | 
  232 |   test('Phase 4.18 — Sessions modal references pending-invite admin RPC', () => {
  233 |     expect(html).toContain('adminRevokePendingInvite');
  234 |     expect(html).toContain('admin_revoke_invite');
  235 |     expect(html).toContain('admin_list_invites');
  236 |     expect(html).toContain('p_client_user_id: _sessionsForUser');
  237 |     expect(html).toContain('Pending invites');
  238 |   });
  239 | 
  240 |   test('Phase 4.0a — signed-URL viewer wired (no direct window.open on view_url)', () => {
  241 |     expect(html).toContain('openSignedRecceView(');
  242 |     expect(html).toContain('/recce-view-proxy/sign');
  243 |     expect(html).not.toMatch(/window\.open\(['"][^'"]*\$\{[^}]*view_url[^}]*\}['"]/);
  244 |   });
  245 | });
  246 | 
  247 | // ──────────────────────────────────────────────────────────
  248 | // AUTH
  249 | // ──────────────────────────────────────────────────────────
  250 | test.describe('Counters — Auth', () => {
  251 |   test('no session → redirects to /hub/?next=/counters/', async ({ page }) => {
  252 |     await page.goto(BASE);
  253 |     await page.waitForURL(/\/hub\//, { timeout: 4000 }).catch(() => {});
  254 |     const url = page.url();
  255 |     const onHubWithNext = url.includes('/hub/') && url.includes('next=') && url.includes('counters');
  256 |     const stillOnCounters = !await page.evaluate(() => !!localStorage.getItem('lm360-session'));
  257 |     expect(onHubWithNext || stillOnCounters).toBeTruthy();
  258 |   });
  259 | 
  260 |   test('supervisor (harish) sees + New button', async ({ page }) => {
  261 |     await injectSession(page, 'harish', 'Harish Kumar Lal');
  262 |     await page.goto(BASE);
  263 |     await page.waitForTimeout(600);
  264 |     await expect(page.locator('#btn-new')).toBeVisible();
  265 |     await expect(page.locator('#user-pill')).toContainText('SUPERVISOR');
  266 |   });
  267 | 
  268 |   test('non-supervisor (mukesh) hides + New + Edit', async ({ page }) => {
  269 |     await injectSession(page, 'mukesh', 'Mukesh');
  270 |     await page.goto(BASE);
  271 |     await page.waitForTimeout(600);
  272 |     await expect(page.locator('#btn-new')).toBeHidden();
  273 |     await expect(page.locator('#user-pill')).not.toContainText('SUPERVISOR');
  274 |   });
  275 | 
  276 |   test('rakesh is a supervisor', async ({ page }) => {
  277 |     await injectSession(page, 'rakesh', 'Rakesh');
  278 |     await page.goto(BASE);
  279 |     await page.waitForTimeout(600);
  280 |     await expect(page.locator('#btn-new')).toBeVisible();
  281 |     await expect(page.locator('#user-pill')).toContainText('SUPERVISOR');
  282 |   });
  283 | });
  284 | 
  285 | // ──────────────────────────────────────────────────────────
  286 | // HUB REGISTRATION
  287 | // ──────────────────────────────────────────────────────────
  288 | test.describe('Counters — Hub registration', () => {
  289 |   test('PWA row in hub.pwa_registry with grants for harish, pramod, rakesh', async () => {
  290 |     const ctx = await request.newContext();
  291 |     const reg = await ctx.get(`${DB}/pwa_registry?id=eq.counters&select=id,label,access_group,is_live`, {
  292 |       headers: { 'Accept-Profile': 'hub' },
  293 |     });
  294 |     expect(reg.status()).toBe(200);
  295 |     const rows = await reg.json();
  296 |     expect(rows).toHaveLength(1);
  297 |     expect(rows[0].access_group).toBe('counters');
  298 |     expect(rows[0].is_live).toBe(true);
  299 | 
  300 |     const acc = await ctx.get(`${DB}/employee_pwa_access?access_group=eq.counters&select=employee_id`, {
  301 |       headers: { 'Accept-Profile': 'hub' },
  302 |     });
  303 |     const empIds = (await acc.json()).map(r => r.employee_id).sort();
  304 |     expect(empIds).toEqual(expect.arrayContaining(['harish', 'pramod', 'rakesh']));
  305 |     await ctx.dispose();
  306 |   });
  307 | });
  308 | 
  309 | // ──────────────────────────────────────────────────────────
  310 | // END-TO-END — create, edit, contact, delete
  311 | // ──────────────────────────────────────────────────────────
  312 | test.describe('Counters — E2E supervisor flow', () => {
  313 |   test('create counter → detail → add contact → search → cleanup', async ({ page }) => {
  314 |     await injectSession(page);
  315 |     await page.goto(BASE);
  316 |     await page.waitForTimeout(500);
  317 | 
  318 |     // Create
  319 |     await page.click('#btn-new');
  320 |     await page.waitForTimeout(150);
  321 |     const stamp = Date.now().toString(36).slice(-6);
  322 |     const name  = `Playwright Counter ${stamp}`;
  323 |     await page.fill('#f-display_name',  name);
  324 |     await page.fill('#f-address_line1', '99 Test Road');
  325 |     await page.fill('#f-city',          'Mohali');
  326 |     await page.fill('#f-pincode',       '160055');
  327 |     await page.fill('#brand-inp', 'HP');     await page.press('#brand-inp', 'Enter');
  328 |     await page.fill('#brand-inp', 'LENOVO'); await page.press('#brand-inp', 'Enter');
  329 |     await page.click('#btn-save');
```