// ───────────────────────────────────────────────────────────
// Lead Hour — hour × weekday economics by lead creation hour (ET).
// Data: hourly-data.json, exported daily by C:\CarBrain\scripts\hourly_export.py (run from dw_publish.ps1).
// Same projection as Campaign Economics (OA/APC by maturity curve, profit per sold unit), split by the
// hour the lead was created (Vehicles.DateAdded) and with spend by hour (Google, Microsoft Ads, Meta).
// The view is plain DOM inside one React container (ported from the Claude artifact "Lead Hour Economics").
// ───────────────────────────────────────────────────────────
const LH_CSS = ".lh{--bg:#FFFFFF;--surface:#F3F8FF;--fg:#002147;--muted:#4A6A8C;--line:#D6E4F3;--accent:#00BBEA;--chip:#DEEEFF;--chip-on:#002147;--chip-on-fg:#FFFFFF;--good:0,120,190;--bad:214,96,40;--seq:0,33,71;--cell-fg:#002147;--cell-fg-strong:#FFFFFF;--empty:#F5F7FA;--display:'Paytone One',Arial,sans-serif;--body:'Nunito Sans',Arial,sans-serif;color:var(--fg);font-family:var(--body);font-size:14px;line-height:1.5}\n.lh .lh-wrap{display:flex;flex-direction:column;gap:20px}\n.lh h1{font-family:var(--display);font-weight:400;font-size:26px;line-height:1.1;margin:0}\n.lh .sub{color:var(--muted);margin:0;max-width:80ch}\n.lh .eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;font-weight:900;color:var(--accent);margin:0}\n.lh .bar{background:var(--surface);border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:12px}\n.lh .row{display:flex;flex-wrap:wrap;gap:12px 18px;align-items:flex-end}\n.lh .f{display:flex;flex-direction:column;gap:4px;min-width:0}\n.lh .f>span{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:900;color:var(--muted)}\n.lh select,.lh input[type=date],.lh input[type=number]{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:6px 8px;max-width:100%}\n.lh select{min-width:150px}\n.lh #lh-campaign{min-width:240px;max-width:min(420px,100%)}\n.lh input[type=number]{width:72px}\n.lh select:focus-visible,.lh input:focus-visible,.lh button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}\n.lh .chips{display:flex;flex-wrap:wrap;gap:6px}\n.lh .chip{font:inherit;font-size:12.5px;font-weight:700;border:1px solid var(--line);background:var(--chip);color:var(--fg);border-radius:999px;padding:4px 10px;cursor:pointer}\n.lh .chip[aria-pressed=\"true\"]{background:var(--chip-on);color:var(--chip-on-fg);border-color:var(--chip-on)}\n.lh .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}\n.lh .seg button{font:inherit;font-size:12.5px;font-weight:700;border:0;background:var(--bg);color:var(--fg);padding:6px 10px;cursor:pointer}\n.lh .seg button[aria-pressed=\"true\"]{background:var(--chip-on);color:var(--chip-on-fg)}\n.lh .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}\n.lh .kpi{display:flex;flex-direction:column;gap:2px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;min-width:0}\n.lh .kpi span{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:900;color:var(--muted)}\n.lh .kpi b{font-size:18px;font-variant-numeric:tabular-nums}\n.lh .scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;min-width:0;border:1px solid var(--line);border-radius:10px}\n.lh table.grid{border-collapse:separate;border-spacing:2px;font-variant-numeric:tabular-nums;font-size:12.5px;width:100%;min-width:720px}\n.lh .grid th{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);font-weight:900;padding:6px 4px;text-align:center;background:var(--bg)}\n.lh .grid td{text-align:center;padding:5px 4px;border-radius:4px;color:var(--cell-fg);white-space:nowrap;cursor:default;border:0}\n.lh .grid td.h{font-weight:900;color:var(--muted);text-align:right;padding-right:8px;background:transparent}\n.lh .grid td.tot,.lh .grid th.tot{font-weight:900}\n.lh .grid tr.totrow td{border-top:2px solid var(--line)}\n.lh .grid td.thin{color:var(--muted);opacity:.55}\n.lh .grid td.sel{outline:2px solid var(--accent)}\n.lh .legend{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;font-size:12px;color:var(--muted)}\n.lh .sw{display:inline-block;width:14px;height:14px;border-radius:3px;vertical-align:-2px;margin-right:4px}\n.lh .detail{border:1px solid var(--line);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}\n.lh .detail table{border-collapse:collapse;font-size:12.5px;font-variant-numeric:tabular-nums}\n.lh .detail td,.lh .detail th{padding:4px 10px;text-align:right;border-bottom:1px solid var(--line)}\n.lh .detail td:first-child,.lh .detail th:first-child{text-align:left}\n.lh .note{font-size:12.5px;color:var(--muted);max-width:90ch}\n.lh ul.note{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:4px}\n.lh #lh-status{font-size:13px;color:var(--muted);margin:0}\n.lh .ms{position:relative}\n.lh .ms summary{list-style:none;cursor:pointer;border:1px solid var(--line);border-radius:8px;padding:6px 28px 6px 8px;background:var(--bg);min-width:180px;max-width:min(360px,80vw);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:relative}\n.lh .ms summary::-webkit-details-marker{display:none}\n.lh .ms summary::after{content:\"▾\";position:absolute;right:9px;top:6px;color:var(--muted)}\n.lh .ms-list{position:absolute;z-index:5;top:calc(100% + 4px);left:0;background:var(--bg);border:1px solid var(--line);border-radius:8px;box-shadow:0 6px 20px rgba(0,33,71,.15);padding:6px;max-height:320px;overflow:auto;min-width:260px;max-width:min(460px,90vw);display:flex;flex-direction:column}\n.lh .ms-list label{display:flex;gap:8px;align-items:flex-start;padding:4px 6px;border-radius:6px;cursor:pointer;font-size:13px}\n.lh .ms-list label:hover{background:var(--surface)}\n.lh .ms-list label.all{font-weight:900;border-bottom:1px solid var(--line);border-radius:0;margin-bottom:4px}\n.lh .ms-list small{color:var(--muted)}\n.lh .vol{display:flex;flex-direction:column;gap:12px}\n.lh .vol h2{font-family:var(--display);font-weight:400;font-size:22px;margin:0}\n.lh .grid td small{display:block;font-size:10.5px;opacity:.8}\n";
const LH_HTML = "<div class=\"lh-wrap\">\n<header style=\"display:flex;flex-direction:column;gap:8px\">\n<div class=\"eyebrow\">CarBrain · by lead creation hour (ET)</div>\n<h1>Lead Hour Economics</h1>\n<p class=\"sub\">Same logic as Campaign Economics (OA/APC projected by maturity, profit per sold unit), split by the hour of day and weekday the lead was created. Spend is also by hour.</p>\n<p id=\"lh-status\">Loading data…</p>\n</header>\n\n<div class=\"bar\">\n<div class=\"row\">\n<label class=\"f\"><span>From</span><input type=\"date\" id=\"lh-from\"></label>\n<label class=\"f\"><span>To</span><input type=\"date\" id=\"lh-to\"></label>\n<div class=\"f\"><span>Quick range</span><div class=\"seg\" id=\"lh-presets\">\n<button type=\"button\" data-p=\"30\">30 days</button><button type=\"button\" data-p=\"90\">90 days</button><button type=\"button\" data-p=\"mature\">Mature (30+ days)</button><button type=\"button\" data-p=\"all\">All</button></div></div>\n</div>\n<div class=\"f\"><span>Channel</span><div class=\"chips\" id=\"lh-channels\"></div></div>\n<div class=\"f\"><span>Lead type</span><div class=\"chips\" id=\"lh-segs\"></div></div>\n<div class=\"row\">\n<div class=\"f\"><span>Theme</span><details class=\"ms\" id=\"lh-theme\"><summary></summary><div class=\"ms-list\"></div></details></div>\n<div class=\"f\"><span>Campaign</span><details class=\"ms\" id=\"lh-campaign\"><summary></summary><div class=\"ms-list\"></div></details></div>\n<label class=\"f\"><span>Metric</span><select id=\"lh-metric\"></select></label>\n<div class=\"f\"><span>Values</span><div class=\"seg\" id=\"lh-mode\"><button type=\"button\" data-m=\"total\" aria-pressed=\"true\">Total</button><button type=\"button\" data-m=\"avg\">Average per day</button></div></div>\n<label class=\"f\"><span>Profit per unit</span><select id=\"lh-pmode\"><option value=\"campaign\">By campaign</option><option value=\"national\">National</option></select></label>\n<label class=\"f\"><span>Min. leads per cell</span><input type=\"number\" id=\"lh-minLeads\" min=\"0\" step=\"5\" value=\"15\"></label>\n</div>\n</div>\n\n<div class=\"kpis\" id=\"lh-kpis\"></div>\n\n<div class=\"legend\" id=\"lh-legend\"></div>\n<div class=\"scroll\"><table class=\"grid\" id=\"lh-grid\"></table></div>\n<div class=\"detail\" id=\"lh-detail\"><b>Cell detail</b><p class=\"note\">Click a cell to see all its metrics and the segment mix.</p></div>\n\n<section class=\"vol\">\n<h2>Lead volume and share by day</h2>\n<p class=\"sub\">Same filters. Each cell shows the number of leads and the share of that day's leads created in that hour. The bottom row shows each day's share of the week; the right column shows each hour's share of all leads.</p>\n<div class=\"f\"><span>Show</span><div class=\"seg\" id=\"lh-volMode\"><button type=\"button\" data-v=\"both\" aria-pressed=\"true\">Leads + share</button><button type=\"button\" data-v=\"leads\">Leads</button><button type=\"button\" data-v=\"share\">Share of day</button></div></div>\n<div class=\"scroll\"><table class=\"grid\" id=\"lh-share\"></table></div>\n</section>\n\n<ul class=\"note\">\n<li><b>PR proj.</b> = projected APC ÷ leads. Projection per cohort (date × campaign × segment × hour): final OA = OA seen + leads × channel OA rate × (1 − share already visible at that age); final APC = APC seen + final OA × OA→APC × (1 − share visible). Rates come from cohorts 35–125 days old, shrunk toward the segment average when volume is low.</li>\n<li><b>Profit</b> = projected APC × profit per sold unit of the segment (last 180 days). \"By campaign\" uses each campaign's own average, shrunk toward national with 20 units. <b>Profit / $1</b> = projected profit ÷ spend.</li>\n<li><b>Seen</b> = APC seen ÷ projected APC. When it is low, the cell relies more on the projection; to decide schedules, use the \"Mature (30+ days)\" range.</li>\n<li>Hour = ET hour the lead was created (Vehicles.DateAdded). Hourly spend in ET: Google Ads, Microsoft Ads and Meta. Channels without spend (SEO, Direct, Affiliate…) show leads and conversion, but no CPL or Profit / $1.</li>\n<li>Color: blue = better than the selection total, orange = worse (for leads and spend, darker = more volume). Cells with fewer leads than the minimum are grey.</li>\n<li><b>Lead type</b> filters leads, OA, APC and profit. Spend cannot be split by lead type, so with a lead-type filter CPL and Cost / APC are full spend ÷ leads (or APC) of the selected types, and Profit / $1 and Profit − spend are hidden.</li>\n</ul>\n<p class=\"note\" id=\"lh-foot\"></p>\n</div>";

function lhInit(root, DATA) {

const SEGS=['SP','Parts','Priority','Premium','No offer'];
const PAID=['PPC - CB','PPC - Pmax','Paid Social'];
const REF_MIN=35,REF_MAX=125,SH_LEADS=400,SH_OA=40,SH_UNITS=20;
const DN=['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const $=id=>root.querySelector('#lh-'+id);
const day=s=>Date.UTC(+s.slice(0,4),+s.slice(5,7)-1,+s.slice(8,10))/864e5;
const iso=d=>new Date(d*864e5).toISOString().slice(0,10);
const money=(n,d=0)=>n==null||!isFinite(n)?'—':`${n<0?'−':''}$${Math.abs(n).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d})}`;
const num=(n,d=0)=>n==null||!isFinite(n)?'—':n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});
const pct=(n,d=1)=>n==null||!isFinite(n)?'—':`${(n*100).toFixed(d)}%`;
const css=n=>getComputedStyle(root).getPropertyValue(n).trim();

// metric: value(cell), format, direction (+1 higher is better, -1 lower is better, 0 = volume)
const METRICS=[
 {k:'pr',l:'PR proj. (purchase rate)',v:c=>c.leads?c.apcProj/c.leads:null,f:x=>pct(x,2),dir:1},
 {k:'leads',l:'Leads',v:c=>c.leads,f:x=>num(x,x<100&&x%1?1:0),dir:0,count:1},
 {k:'spend',l:'Spend',v:c=>c.spend,f:x=>money(x),dir:0,count:1},
 {k:'cpl',l:'CPL',v:c=>c.leads&&c.spend?c.spend/c.leads:null,f:x=>money(x,2),dir:-1},
 {k:'pp',l:'P+P share',v:c=>c.leads?c.pp/c.leads:null,f:x=>pct(x),dir:1},
 {k:'oa',l:'OA rate (seen)',v:c=>c.leads?c.oa/c.leads:null,f:x=>pct(x),dir:1},
 {k:'oap',l:'OA rate proj.',v:c=>c.leads?c.oaProj/c.leads:null,f:x=>pct(x),dir:1},
 {k:'apcp',l:'APC proj.',v:c=>c.apcProj,f:x=>num(x,1),dir:0,count:1},
 {k:'cpa',l:'Cost / APC',v:c=>c.apcProj&&c.spend?c.spend/c.apcProj:null,f:x=>money(x),dir:-1},
 {k:'vpl',l:'Profit / lead',v:c=>c.leads?c.profit/c.leads:null,f:x=>money(x,2),dir:1},
 {k:'ppd',l:'Profit / $1',v:c=>c.spend&&allSegs()?c.profit/c.spend:null,f:x=>x==null?'—':'$'+x.toFixed(2),dir:1},
 {k:'net',l:'Profit − spend',v:c=>c.spend&&allSegs()?c.profit-c.spend:null,f:x=>money(x),dir:1,count:1,signed:1},
 {k:'seen',l:'Seen (maturity)',v:c=>c.apcProj?c.apc/c.apcProj:null,f:x=>pct(x,0),dir:0},
];
const SEG_UI=[0,1,2,3,4]; // SP, Parts, Priority, Premium, No offer
let D,todayDay,rate,state={from:null,to:null,channels:new Set(PAID),themes:new Set(),camps:new Set(),segs:new Set(SEG_UI),metric:'pr',mode:'total',pmode:'campaign',minLeads:15,sel:null,vol:'both'};
const allSegs=()=>state.segs.size===SEG_UI.length;
const esc=t=>String(t).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;');

function rates(){
 const acc={};const add=(ch,s,l,o,a)=>{const k=ch+'|'+s;const r=acc[k]||(acc[k]={leads:0,oa:0,apc:0});r.leads+=l;r.oa+=o;r.apc+=a};
 for(const [d,k,s,h,l,o,a] of D.cohorts){const age=todayDay-1-day(d);if(age<REF_MIN||age>REF_MAX)continue;add(D.keys[k].ch,s,l,o,a);add('*',s,l,o,a)}
 const cache={};
 return (ch,s)=>{const k=ch+'|'+s;if(cache[k])return cache[k];const g=acc['*|'+s]||{leads:0,oa:0,apc:0};const gO=g.leads?g.oa/g.leads:0,gC=g.oa?g.apc/g.oa:0;const c=acc[k]||{leads:0,oa:0,apc:0};
  return cache[k]={oaRate:(c.oa+SH_LEADS*gO)/(c.leads+SH_LEADS),oa2apc:(c.apc+SH_OA*gC)/(c.oa+SH_OA)}};
}
const curve=(seg,kind,age)=>{const a=(D.curves[seg]||{})[kind];if(!a||age>=a.length)return 1;return a[Math.max(0,age)]??1};
const unitCache=new Map();
function unitProfit(s,ki){
 if(s===4)return 0;
 const key=state.pmode+'|'+s+'|'+ki;if(unitCache.has(key))return unitCache.get(key);
 const seg=SEGS[s],nat=(D.profit[seg]||{}).profit||0;let v=nat;
 if(state.pmode==='campaign'){const cid=D.keys[ki].cid;const p=cid&&D.campaignProfit[cid]?D.campaignProfit[cid][s]:null;const n=p?p[0]:0;v=(n*(p?p[1]:0)+SH_UNITS*nat)/(n+SH_UNITS)}
 unitCache.set(key,v);return v;
}
const dowOf=s=>(new Date(day(s)*864e5).getUTCDay()+6)%7;
const empty=()=>({leads:0,pp:0,oa:0,apc:0,oaProj:0,apcProj:0,profit:0,spend:0,segs:[0,0,0,0,0]});
function keyOk(ki){const k=D.keys[ki];if(!state.channels.has(k.ch))return false;if(state.themes.size&&!state.themes.has(k.theme))return false;if(state.camps.size&&!state.camps.has(k.cid))return false;return true}

function aggregate(){
 const cells=[...Array(7)].map(()=>[...Array(24)].map(empty));
 const byH=[...Array(24)].map(empty),byD=[...Array(7)].map(empty),tot=empty();
 const okK=D.keys.map((_,i)=>keyOk(i));const f=state.from,t=state.to;const dowCache={};
 const addTo=(c,x)=>{for(const k of ['leads','pp','oa','apc','oaProj','apcProj','profit','spend'])c[k]+=x[k]||0;if(x.segs)x.segs.forEach((v,i)=>c.segs[i]+=v)};
 for(const [d,ki,s,h,l,o,a] of D.cohorts){
  if(d<f||d>t||!okK[ki]||!state.segs.has(s))continue;
  const dw=dowCache[d]??(dowCache[d]=dowOf(d));const age=Math.max(0,todayDay-day(d)-1);const r=rate(D.keys[ki].ch,s);
  const oaP=o+l*r.oaRate*(1-curve(SEGS[s],'oa',age));const apcP=s===4?a:a+oaP*r.oa2apc*(1-curve(SEGS[s],'apc',age));
  const x={leads:l,pp:(s===2||s===3)?l:0,oa:o,apc:a,oaProj:oaP,apcProj:apcP,profit:apcP*unitProfit(s,ki),segs:[0,0,0,0,0]};x.segs[s]=l;
  addTo(cells[dw][h],x);addTo(byH[h],x);addTo(byD[dw],x);addTo(tot,x);
 }
 for(const [d,ki,h,cost] of D.spend){if(d<f||d>t||!okK[ki])continue;const dw=dowCache[d]??(dowCache[d]=dowOf(d));const x={spend:cost};addTo(cells[dw][h],x);addTo(byH[h],x);addTo(byD[dw],x);addTo(tot,x)}
 // number of each weekday in range (for averages)
 const nd=[0,0,0,0,0,0,0];for(let z=day(f);z<=day(t);z++)nd[(new Date(z*864e5).getUTCDay()+6)%7]++;
 return {cells,byH,byD,tot,nd,days:day(t)-day(f)+1};
}

function color(v,ref,m,maxVol){
 if(v==null||!isFinite(v))return null;
 if(m.dir===0){if(!maxVol)return null;const t=Math.min(1,Math.abs(v)/maxVol);return `rgba(${css('--seq')},${(0.08+0.82*t).toFixed(3)})`}
 if(ref==null||!isFinite(ref)||ref===0)return null;
 let rel=(v-ref)/Math.abs(ref)*m.dir; // >0 better
 const t=Math.min(1,Math.abs(rel)/0.5);
 return `rgba(${css(rel>=0?'--good':'--bad')},${(0.06+0.74*t).toFixed(3)})`;
}

function render(){
 if(!D)return;
 const A=aggregate();const m=METRICS.find(x=>x.k===state.metric);
 const avg=state.mode==='avg'&&m.count;
 const val=(c,dw)=>{let v=m.v(c);if(v!=null&&avg){const n=dw==null?A.days:A.nd[dw];v=n?v/n:null}return v};
 const valH=c=>{let v=m.v(c);if(v!=null&&avg)v=A.days?v/A.days:null;return v};
 const ref=m.v(A.tot);
 // volume scale
 let maxVol=0;if(m.dir===0){for(let dw=0;dw<7;dw++)for(let h=0;h<24;h++){const v=val(A.cells[dw][h],dw);if(v!=null&&isFinite(v))maxVol=Math.max(maxVol,Math.abs(v))}}
 const thin=c=>c.leads<state.minLeads&&!(m.k==='spend');
 const fg=bg=>{if(!bg)return '';const a=+bg.split(',').pop().replace(')','');return a>0.55?`color:${css('--cell-fg-strong')}`:''};
 const cell=(c,dw,h,cls,v)=>{const isThin=thin(c);const bg=isThin?css('--empty'):color(v,ref,m,maxVol);
  const tip=`${dw!=null?DN[dw]:'All days'} ${h!=null?String(h).padStart(2,'0')+':00':'all hours'} · ${num(c.leads)} leads · ${money(c.spend)} · PR ${pct(c.leads?c.apcProj/c.leads:null,2)} · Profit/$1 ${c.spend&&allSegs()?(c.profit/c.spend).toFixed(2):'—'}`;
  const sel=state.sel&&state.sel[0]===dw&&state.sel[1]===h?' sel':'';
  return `<td class="${cls||''}${isThin?' thin':''}${sel}" data-d="${dw??''}" data-h="${h??''}" title="${tip}" style="${bg?'background:'+bg+';':''}${isThin?'':fg(bg)}">${m.f(v)}</td>`};
 let s='<thead><tr><th></th>'+DN.map(d=>`<th>${d}</th>`).join('')+'<th class="tot">Hour total</th></tr></thead><tbody>';
 for(let h=0;h<24;h++){s+=`<tr><td class="h">${String(h).padStart(2,'0')}:00</td>`;for(let dw=0;dw<7;dw++)s+=cell(A.cells[dw][h],dw,h,'',val(A.cells[dw][h],dw));
  s+=cell(A.byH[h],null,h,'tot',valH(A.byH[h]))+'</tr>'}
 s+=`<tr class="totrow"><td class="h">Day total</td>`;for(let dw=0;dw<7;dw++)s+=cell(A.byD[dw],dw,null,'tot',val(A.byD[dw],dw));
 s+=cell(A.tot,null,null,'tot',avg?m.v(A.tot)/A.days:m.v(A.tot))+'</tr></tbody>';
 $('grid').innerHTML=s;
 // legend
 $('legend').innerHTML=m.dir===0?`<span><i class="sw" style="background:rgba(${css('--seq')},.15)"></i>less</span><span><i class="sw" style="background:rgba(${css('--seq')},.85)"></i>more</span><span>${avg?'Average per day of that weekday':'Period total'}</span>`:
  `<span><i class="sw" style="background:rgba(${css('--good')},.7)"></i>better than total (${m.f(ref)})</span><span><i class="sw" style="background:rgba(${css('--bad')},.7)"></i>worse</span><span>Full color = ±50% vs total</span><span><i class="sw" style="background:${css('--empty')}"></i>fewer than ${state.minLeads} leads</span>`;
 // KPIs
 const T=A.tot;
 $('kpis').innerHTML=[['Leads',num(T.leads)],['Spend',money(T.spend)],['CPL',money(T.leads&&T.spend?T.spend/T.leads:null,2)],['PR proj.',pct(T.leads?T.apcProj/T.leads:null,2)],['APC proj.',num(T.apcProj,0)],['Cost / APC',money(T.apcProj&&T.spend?T.spend/T.apcProj:null)],['Profit / $1',T.spend&&allSegs()?'$'+(T.profit/T.spend).toFixed(2):'—'],['Seen',pct(T.apcProj?T.apc/T.apcProj:null,0)]]
  .map(([k,v])=>`<div class="kpi"><span>${k}</span><b>${v}</b></div>`).join('');
 renderDetail(A);
 renderVolume(A);
}

function renderVolume(A){
 const v=state.vol;const hh=h=>String(h).padStart(2,'0')+':00';
 let max=0;for(let dw=0;dw<7;dw++){const t=A.byD[dw].leads;for(let h=0;h<24;h++)if(t)max=Math.max(max,A.cells[dw][h].leads/t)}
 const show=(n,sh)=>v==='leads'?num(n):v==='share'?pct(sh):`${num(n)}<small>${pct(sh)}</small>`;
 const bg=sh=>{if(sh==null||!max)return '';const t=Math.min(1,sh/max);const a=(0.06+0.8*t).toFixed(3);return `background:rgba(${css('--seq')},${a});${a>0.55?'color:'+css('--cell-fg-strong'):''}`};
 let s='<thead><tr><th></th>'+DN.map(d=>`<th>${d}</th>`).join('')+'<th class="tot">Hour total</th></tr></thead><tbody>';
 for(let h=0;h<24;h++){s+=`<tr><td class="h">${hh(h)}</td>`;
  for(let dw=0;dw<7;dw++){const n=A.cells[dw][h].leads,t=A.byD[dw].leads,sh=t?n/t:null;s+=`<td style="${bg(sh)}" title="${DN[dw]} ${hh(h)} · ${num(n)} leads · ${pct(sh)} of ${DN[dw]} leads">${show(n,sh)}</td>`}
  const n=A.byH[h].leads,sh=A.tot.leads?n/A.tot.leads:null;s+=`<td class="tot" title="${hh(h)} · ${pct(sh)} of all leads">${show(n,sh)}</td></tr>`}
 s+='<tr class="totrow"><td class="h">Day total</td>';
 for(let dw=0;dw<7;dw++){const n=A.byD[dw].leads,sh=A.tot.leads?n/A.tot.leads:null;s+=`<td class="tot" title="${DN[dw]} · ${pct(sh)} of the week's leads · ${num(A.nd[dw]?n/A.nd[dw]:null,1)} leads per ${DN[dw]}">${show(n,sh)}</td>`}
 s+=`<td class="tot">${show(A.tot.leads,A.tot.leads?1:null)}</td></tr>`;
 s+='<tr><td class="h">Per day</td>'+DN.map((d,dw)=>`<td class="tot">${num(A.nd[dw]?A.byD[dw].leads/A.nd[dw]:null,1)}</td>`).join('')+`<td class="tot">${num(A.days?A.tot.leads/A.days:null,1)}</td></tr></tbody>`;
 $('share').innerHTML=s;
}

function renderDetail(A){
 if(!state.sel){$('detail').innerHTML='<b>Cell detail</b><p class="note">Click a cell to see all its metrics and the segment mix.</p>';return}
 const [dw,h]=state.sel;const c=dw!=null&&h!=null?A.cells[dw][h]:dw!=null?A.byD[dw]:h!=null?A.byH[h]:A.tot;
 const name=`${dw!=null?DN[dw]:'All days'} · ${h!=null?String(h).padStart(2,'0')+':00–'+String(h).padStart(2,'0')+':59':'all hours'}`;
 const rows=METRICS.map(m=>`<tr><td>${m.l}</td><td>${m.f(m.v(c))}</td><td>${m.f(m.v(A.tot))}</td></tr>`).join('');
 const mix=[3,2,1,0,4].map(i=>`<tr><td>${SEGS[i]}</td><td>${num(c.segs[i])}</td><td>${pct(c.leads?c.segs[i]/c.leads:null)}</td></tr>`).join('');
 $('detail').innerHTML=`<b>${name}</b><div style="display:flex;flex-wrap:wrap;gap:20px"><div class="scroll" style="border:0"><table><thead><tr><th>Metric</th><th>Cell</th><th>Selection total</th></tr></thead><tbody>${rows}</tbody></table></div><div class="scroll" style="border:0"><table><thead><tr><th>Segment</th><th>Leads</th><th>Mix</th></tr></thead><tbody>${mix}</tbody></table></div></div>`;
}

function fillFilters(){
 // themes & campaigns limited to selected channels (and theme)
 const leadsBy={};for(const r of D.cohorts){if(r[0]<state.from||r[0]>state.to)continue;leadsBy[r[1]]=(leadsBy[r[1]]||0)+r[4]}
 for(const r of D.spend){if(r[0]<state.from||r[0]>state.to)continue;leadsBy[r[1]]=(leadsBy[r[1]]||0)+0.0001}
 const themes={},camps={};
 D.keys.forEach((k,i)=>{if(!state.channels.has(k.ch)||!leadsBy[i])return;themes[k.theme]=(themes[k.theme]||0)+leadsBy[i];
  if(k.cid&&(!state.themes.size||state.themes.has(k.theme)))camps[k.cid]={name:k.name||('Campaign '+k.cid),n:((camps[k.cid]||{}).n||0)+leadsBy[i],pf:k.platform}});
 [...state.themes].forEach(t=>{if(!(t in themes))state.themes.delete(t)});
 [...state.camps].forEach(c=>{if(!camps[c])state.camps.delete(c)});
 const tList=Object.keys(themes).sort((a,b)=>themes[b]-themes[a]).map(t=>({v:t,l:t||'(no theme)'}));
 const cList=Object.entries(camps).sort((a,b)=>b[1].n-a[1].n).map(([cid,c])=>({v:cid,l:c.name,sub:c.pf}));
 msFill('theme',tList,state.themes,'All themes','themes');
 msFill('campaign',cList,state.camps,'All campaigns','campaigns');
}
// multi-select dropdown: empty set = all
function msFill(id,items,set,allLabel,noun){
 const el=$(id);const open=el.open;
 const names=items.filter(x=>set.has(x.v)).map(x=>x.l);
 el.querySelector('summary').textContent=!set.size?allLabel:names.length===1?names[0]:`${names.length} ${noun}`;
 el.querySelector('summary').title=names.join(', ');
 el.querySelector('.ms-list').innerHTML=`<label class="all"><input type="checkbox" data-all="1"${set.size?'':' checked'}> ${allLabel}</label>`+
  items.map(x=>`<label><input type="checkbox" value="${esc(x.v)}"${set.has(x.v)?' checked':''}> <span>${esc(x.l)}${x.sub?` <small>· ${esc(x.sub)}</small>`:''}</span></label>`).join('');
 el.open=open;
}
function fillSegs(){
 const all=allSegs();
 $('segs').innerHTML=SEG_UI.map(i=>`<button type="button" class="chip" data-s="${i}" aria-pressed="${!all&&state.segs.has(i)}">${SEGS[i]}</button>`).join('')+
  `<button type="button" class="chip" data-s="pp" aria-pressed="${!all&&state.segs.size===2&&state.segs.has(2)&&state.segs.has(3)}">P+P</button><button type="button" class="chip" data-s="all" aria-pressed="${all}">All</button>`;
}
function fillChannels(){
 const n={};D.cohorts.forEach(r=>{const ch=D.keys[r[1]].ch;n[ch]=(n[ch]||0)+r[4]});
 const chs=Object.keys(n).sort((a,b)=>(PAID.includes(b)-PAID.includes(a))||n[b]-n[a]);
 $('channels').innerHTML=chs.map(c=>`<button type="button" class="chip" data-ch="${c}" aria-pressed="${state.channels.has(c)}">${c}</button>`).join('')+'<button type="button" class="chip" data-ch="__paid">Paid only</button><button type="button" class="chip" data-ch="__all">All</button>';
}
function setPreset(p){
 const last=D.end;const L=day(last);
 if(p==='30'){state.from=iso(L-29);state.to=last}
 else if(p==='90'){state.from=iso(L-89);state.to=last}
 else if(p==='mature'){state.to=iso(L-30);state.from=iso(Math.max(day(D.start),L-30-119))}
 else {state.from=D.start;state.to=last}
 $('from').value=state.from;$('to').value=state.to;
 root.querySelectorAll('#lh-presets button').forEach(b=>b.setAttribute('aria-pressed',b.dataset.p===p));
}
function wire(){
 $('metric').innerHTML=METRICS.map(m=>`<option value="${m.k}">${m.l}</option>`).join('');
 $('presets').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;setPreset(b.dataset.p);fillFilters();render()});
 ['from','to'].forEach(id=>$(id).addEventListener('change',()=>{const v=$(id).value;if(!v)return;state[id]=v;if(state.from>state.to){const t=state.from;state.from=state.to;state.to=t;$('from').value=state.from;$('to').value=state.to}
  root.querySelectorAll('#lh-presets button').forEach(b=>b.setAttribute('aria-pressed','false'));fillFilters();render()}));
 $('channels').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const ch=b.dataset.ch;
  if(ch==='__paid')state.channels=new Set(PAID);else if(ch==='__all')state.channels=new Set(D.keys.map(k=>k.ch));
  else{state.channels.has(ch)?state.channels.delete(ch):state.channels.add(ch);if(!state.channels.size)state.channels.add(ch)}
  fillChannels();fillFilters();render()});
 const msChange=(id,set)=>$(id).addEventListener('change',e=>{const cb=e.target;if(cb.type!=='checkbox')return;
  if(cb.dataset.all)set.clear();else{cb.checked?set.add(cb.value):set.delete(cb.value)}
  fillFilters();render()});
 msChange('theme',state.themes);msChange('campaign',state.camps);
 $('theme').addEventListener('toggle',()=>{if($('theme').open)$('campaign').open=false});
 $('campaign').addEventListener('toggle',()=>{if($('campaign').open)$('theme').open=false});
 document.addEventListener('click',e=>{['theme','campaign'].forEach(id=>{const el=$(id);if(el&&el.open&&!el.contains(e.target))el.open=false})});
 $('segs').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const k=b.dataset.s;
  if(k==='all')state.segs=new Set(SEG_UI);else if(k==='pp')state.segs=new Set([2,3]);
  else{const i=+k;if(allSegs())state.segs=new Set([i]);else{state.segs.has(i)?state.segs.delete(i):state.segs.add(i);if(!state.segs.size)state.segs=new Set(SEG_UI)}}
  fillSegs();render()});
 $('volMode').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;state.vol=b.dataset.v;$('volMode').querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',x===b));render()});
 $('metric').addEventListener('change',()=>{state.metric=$('metric').value;render()});
 $('pmode').addEventListener('change',()=>{state.pmode=$('pmode').value;render()});
 $('minLeads').addEventListener('change',()=>{state.minLeads=Math.max(0,+$('minLeads').value||0);render()});
 $('mode').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;state.mode=b.dataset.m;root.querySelectorAll('#lh-mode button').forEach(x=>x.setAttribute('aria-pressed',x===b));render()});
 $('grid').addEventListener('click',e=>{const td=e.target.closest('td');if(!td||td.classList.contains('h'))return;const d=td.dataset.d===''?null:+td.dataset.d,h=td.dataset.h===''?null:+td.dataset.h;state.sel=[d,h];render()});

}

(j=>{
 D=j;todayDay=day(D.asOf)+1;rate=rates();
 $('from').min=$('to').min=D.start;$('from').max=$('to').max=D.end;
 setPreset('90');fillChannels();fillSegs();wire();fillFilters();render();
 $('status').textContent=`Leads ${D.start} to ${D.end} · maturity and profit as of ${D.asOf} · generated ${D.generated.replace('T',' ')}`;
 $('foot').textContent='Source: BigQuery cag-data-warehouse (car_fact_leads_vehicles_new, Vehicles.DateAdded, VehicleStatusHistory, VehicleOffers) and the Google Ads, Microsoft Ads and Meta APIs. Rebuilt daily by hourly_export.py (dw_publish.ps1).';
})(DATA);
}

function LeadHourTab() {
  const ref = React.useRef(null);
  React.useEffect(() => {
    let alive = true;
    fetch('hourly-data.json', { cache: 'no-cache' })
      .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(d => { if (alive && ref.current) lhInit(ref.current, d); })
      .catch(e => { const s = ref.current && ref.current.querySelector('#lh-status'); if (s) s.textContent = "Couldn't load hourly-data.json (" + e.message + ')'; });
    return () => { alive = false; };
  }, []);
  return <div ref={ref} className="lh" dangerouslySetInnerHTML={{ __html: '<style>' + LH_CSS + '</style>' + LH_HTML }} />;
}
