/* OM Group ERP — Intelligence chart primitives (pure SVG, animated)
   window.IC.*  — every chart: enters on scroll, animates on data change,
   hover-inspects, and reports clicks upward for drill-down.               */
const { useState: icSt, useEffect: icEf, useMemo: icMemo, useRef: icRef, useCallback: icCb, useLayoutEffect: icLay } = React;

const IC_PAL = ['#F97316','#2563EB','#16A34A','#7C3AED','#DB2777','#0891B2','#CA8A04','#DC2626','#0D9488','#4F46E5'];
const IC_MODE_COLOR = { sales:'#F97316', purchase:'#2563EB' };
const icShort = v => window.IntelEngine.util.shortCur(v);
const icCur = v => window.fmtCur ? window.fmtCur(v) : '₹'+Math.round(v||0);
const icTon = v => window.formatQuantity(Number(v||0))+' T';
const icInt = v => new Intl.NumberFormat('en-IN').format(Math.round(v||0));
const icPct = v => Number(v||0).toFixed(1)+'%';
function icFmt(v, kind){
  if(kind==='cur') return icCur(v);
  if(kind==='short') return icShort(v);
  if(kind==='ton') return icTon(v);
  if(kind==='int') return icInt(v);
  if(kind==='pct') return icPct(v);
  if(kind==='rate') return '₹'+Number(v||0).toFixed(0);
  return icShort(v);
}
const icEase = t => t>=1?1:1-Math.pow(2,-10*t);
const icEaseIO = t => t<0.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)/2;

/* ── intelligent x-axis tick reduction ──────────────────────────────────
   Shared by every chart with a date/label axis (ICArea, ICBars, and the
   Diesel Recovery trend). Picks a stable set of label indices spaced by
   `step`, then decides the trailing label on its own merits instead of
   always forcing it in: a last label too close to the previous one
   REPLACES it rather than crowding beside it. The set depends only on
   point count and pixel width — never on hover/selection — so interacting
   with the chart can never shift, add or collide axis labels.           */
function icPickTicks(n, step){
  const set = new Set();
  for(let i=0;i<n;i+=step) set.add(i);
  const last = n-1;
  if(last>=0 && !set.has(last)){
    const shown = Array.from(set);
    const prev = shown.length ? Math.max.apply(null,shown) : -1;
    if(prev>=0 && last-prev < step*0.6) set.delete(prev);
    set.add(last);
  }
  return set;
}

/* ── enter-on-view + animate-on-change ─────────────────────────────────── */
function useIcAnim(dep, dur){
  const [t,setT]=icSt(0);
  const host=icRef(null); const seen=icRef(false); const raf=icRef(0);
  const run=icCb(()=>{
    cancelAnimationFrame(raf.current);
    const D=dur||900, t0=performance.now();
    const step=()=>{ const p=Math.min(1,(performance.now()-t0)/D); setT(p); if(p<1) raf.current=requestAnimationFrame(step); };
    raf.current=requestAnimationFrame(step);
  },[dur]);
  icEf(()=>{
    const el=host.current; if(!el) return;
    if(seen.current){ setT(0); run(); return; }
    if(!('IntersectionObserver' in window)){ seen.current=true; run(); return; }
    const io=new IntersectionObserver(es=>{ es.forEach(e=>{ if(e.isIntersecting && !seen.current){ seen.current=true; run(); io.disconnect(); } }); },{ threshold:0.12 });
    io.observe(el);
    return ()=>io.disconnect();
  },[dep]);
  icEf(()=>()=>cancelAnimationFrame(raf.current),[]);
  return [icEase(t), host, t];
}

/* ── measured width ─────────────────────────────────────────────────────── */
function useIcWidth(fallback){
  const ref=icRef(null); const [w,setW]=icSt(fallback||640);
  icLay(()=>{
    const el=ref.current; if(!el) return;
    const set=()=>{ const b=el.getBoundingClientRect(); if(b.width>0) setW(b.width); };
    set();
    if('ResizeObserver' in window){ const ro=new ResizeObserver(set); ro.observe(el); return ()=>ro.disconnect(); }
    window.addEventListener('resize',set); return ()=>window.removeEventListener('resize',set);
  },[]);
  return [w, ref];
}

/* ── animated number ────────────────────────────────────────────────────── */
function ICNum({ value, kind, dur, className, style }){
  const [disp,setDisp]=icSt(0); const prev=icRef(0); const raf=icRef(0);
  icEf(()=>{
    const from=prev.current, to=Number(value)||0, D=dur||760, t0=performance.now();
    cancelAnimationFrame(raf.current);
    const step=()=>{ const p=Math.min(1,(performance.now()-t0)/D); const e=icEase(p);
      setDisp(from+(to-from)*e); if(p<1) raf.current=requestAnimationFrame(step); else prev.current=to; };
    raf.current=requestAnimationFrame(step);
    return ()=>cancelAnimationFrame(raf.current);
  },[value,dur]);
  return <span className={className} style={Object.assign({fontVariantNumeric:'tabular-nums',fontFeatureSettings:"'tnum'"},style||{})}>{icFmt(disp,kind)}</span>;
}

/* ── floating tooltip ───────────────────────────────────────────────────────
   Delegates to the singleton window.ICTooltip (see intel-tooltip.js): one
   body-portaled DOM node, RAF-smoothed, zero React re-render per mousemove. */
const icTip = window.ICTooltip || { show(){}, hide(){} };

/* ── empty state ────────────────────────────────────────────────────────── */
function ICEmpty({ msg, h }){
  return (
    <div className="ic-empty" style={{height:h||160}}>
      <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4"><path d="M3 18l5-6 4 4 4-7 5 5"/><path d="M3 21h18"/></svg>
      <span>{msg||'No data in this period'}</span>
    </div>
  );
}

/* ══ AREA / LINE ═══════════════════════════════════════════════════════════
   series: [{ key, label, color, dashed, values:[n], }]  labels: [str]      */
function ICArea({ series, labels, height, yKind, onPointClick, hideAxis, subLabels }){
  const H=height||260;
  const [w,wrap]=useIcWidth(700);
  const [t,animRef]=useIcAnim(JSON.stringify([labels, series.map(s=>s.values)]), 1000);
  const [hi,setHi]=icSt(null);
  const padL=hideAxis?6:54, padR=10, padT=14, padB=hideAxis?6:26;
  const iw=Math.max(40,w-padL-padR), ih=Math.max(40,H-padT-padB);
  const n=labels.length;
  const all=series.reduce((a,s)=>a.concat(s.values),[]).map(v=>Number(v)||0);
  const rawMax=Math.max.apply(null,all.length?all:[0]);
  const max=rawMax>0?rawMax*1.14:1;
  const X=i=> n<=1 ? padL+iw/2 : padL + (i/(n-1))*iw;
  const Y=v=> padT + ih - (Math.max(0,Number(v)||0)/max)*ih;
  const ticks=icMemo(()=>{ const c=4, out=[]; for(let i=0;i<=c;i++) out.push(max*i/c); return out; },[max]);
  const xStep=Math.ceil(n/Math.max(3,Math.floor(w/74)));
  const xTicks=icMemo(()=>icPickTicks(n,xStep),[n,xStep]);
  function path(vals, close){
    if(!vals.length) return '';
    let d='';
    for(let i=0;i<vals.length;i++){
      const x=X(i), y=padT+ih-((padT+ih-Y(vals[i]))*t);
      if(i===0) d+='M'+x.toFixed(1)+' '+y.toFixed(1);
      else { const px=X(i-1), py=padT+ih-((padT+ih-Y(vals[i-1]))*t); const cx=(px+x)/2;
        d+=' C'+cx.toFixed(1)+' '+py.toFixed(1)+' '+cx.toFixed(1)+' '+y.toFixed(1)+' '+x.toFixed(1)+' '+y.toFixed(1); }
    }
    if(close) d+=' L'+X(vals.length-1).toFixed(1)+' '+(padT+ih)+' L'+X(0).toFixed(1)+' '+(padT+ih)+' Z';
    return d;
  }
  function onMove(e){
    const box=e.currentTarget.getBoundingClientRect();
    const rel=(e.clientX-box.left)/box.width*w;
    let idx=n<=1?0:Math.round((rel-padL)/iw*(n-1));
    idx=Math.max(0,Math.min(n-1,idx));
    if(idx!==hi) setHi(idx);
    icTip.show(e,{ title: labels[idx], foot: subLabels&&subLabels[idx] ? subLabels[idx] : (onPointClick?'Click to drill down':''),
      rows: series.map(s=>({ dot:s.color, k:s.label, v:icFmt(s.values[idx],yKind||'short'), color:s.color })) },
      { avoidRect: box, axisBand: hideAxis?0:padB });
  }
  if(!n) return <ICEmpty h={H}/>;
  return (
    <div ref={wrap} className="ic-chart" style={{position:'relative'}}>
      <div ref={animRef}>
      <svg width="100%" height={H} viewBox={'0 0 '+w+' '+H} style={{display:'block',overflow:'visible'}}
        onMouseMove={onMove} onMouseLeave={()=>{setHi(null);icTip.hide();}}
        onClick={()=>{ if(onPointClick&&hi!=null) onPointClick(hi); }}>
        <defs>
          {series.map((s,i)=>(
            <linearGradient key={i} id={'icg'+i+'-'+(s.key||i)} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor={s.color} stopOpacity={s.dashed?0.10:0.26}/>
              <stop offset="100%" stopColor={s.color} stopOpacity="0"/>
            </linearGradient>
          ))}
        </defs>
        {!hideAxis && ticks.map((v,i)=>(
          <g key={i}>
            <line x1={padL} x2={padL+iw} y1={Y(v)} y2={Y(v)} stroke={i===0?'#E5E3E0':'#F1EFEC'} strokeWidth="1"/>
            <text x={padL-8} y={Y(v)+3.5} textAnchor="end" fontSize="9.5" fill="#A8A4A0" fontWeight="500">{icShort(v)}</text>
          </g>
        ))}
        {series.map((s,i)=>(
          <g key={'s'+i}>
            {!s.dashed && <path d={path(s.values,true)} fill={'url(#icg'+i+'-'+(s.key||i)+')'} style={{opacity:t}}/>}
            <path d={path(s.values,false)} fill="none" stroke={s.color} strokeWidth={s.dashed?1.8:2.4}
              strokeDasharray={s.dashed?'5 4':null} strokeLinecap="round" strokeLinejoin="round"
              style={{filter:s.dashed?null:'drop-shadow(0 4px 10px '+s.color+'33)'}}/>
          </g>
        ))}
        {hi!=null && (
          <g>
            <line x1={X(hi)} x2={X(hi)} y1={padT} y2={padT+ih} stroke="#1A1917" strokeWidth="1" strokeDasharray="3 3" opacity=".28"/>
            {series.map((s,i)=>(
              <circle key={i} cx={X(hi)} cy={padT+ih-((padT+ih-Y(s.values[hi]))*t)} r="4.5" fill="#fff" stroke={s.color} strokeWidth="2.4"/>
            ))}
          </g>
        )}
        {!hideAxis && labels.map((l,i)=>{
          if(!xTicks.has(i)) return null;
          return <text key={i} x={X(i)} y={H-8} textAnchor="middle" fontSize="9.5" fill={hi===i?'#1A1917':'#A8A4A0'} fontWeight={hi===i?700:500}>{l}</text>;
        })}
      </svg>
      </div>
    </div>
  );
}

/* ══ BARS (grouped / stacked) ══════════════════════════════════════════════ */
function ICBars({ rows, keys, height, yKind, onClick, stacked, subLabels }){
  const H=height||250;
  const [w,wrap]=useIcWidth(700);
  const [t,animRef]=useIcAnim(JSON.stringify([rows.map(r=>r.label), rows.map(r=>keys.map(k=>r[k.key]))]),900);
  const [hi,setHi]=icSt(null);
  const padL=54, padR=10, padT=14, padB=26;
  const iw=Math.max(40,w-padL-padR), ih=Math.max(40,H-padT-padB);
  const n=rows.length;
  const totals=rows.map(r=>stacked? keys.reduce((s,k)=>s+(Number(r[k.key])||0),0) : Math.max.apply(null,keys.map(k=>Number(r[k.key])||0)));
  const rawMax=Math.max.apply(null, totals.length?totals:[0]);
  const max=rawMax>0?rawMax*1.14:1;
  const slot=n?iw/n:iw;
  const bw=Math.max(5, Math.min(stacked?46:34, (slot*0.62)/(stacked?1:keys.length)));
  const Y=v=>padT+ih-(Math.max(0,v)/max)*ih;
  const ticks=[0,.25,.5,.75,1].map(f=>max*f);
  const xStep=Math.ceil(n/Math.max(3,Math.floor(w/68)));
  const xTicks=icMemo(()=>icPickTicks(n,xStep),[n,xStep]);
  if(!n) return <ICEmpty h={H}/>;
  return (
    <div ref={wrap} className="ic-chart" style={{position:'relative'}}>
      <div ref={animRef}>
      <svg width="100%" height={H} viewBox={'0 0 '+w+' '+H} style={{display:'block',overflow:'visible'}}>
        {ticks.map((v,i)=>(<g key={i}>
          <line x1={padL} x2={padL+iw} y1={Y(v)} y2={Y(v)} stroke={i===0?'#E5E3E0':'#F1EFEC'}/>
          <text x={padL-8} y={Y(v)+3.5} textAnchor="end" fontSize="9.5" fill="#A8A4A0" fontWeight="500">{icShort(v)}</text>
        </g>))}
        {rows.map((r,i)=>{
          const cx=padL+slot*i+slot/2;
          const grp=[];
          if(stacked){
            let acc=0;
            keys.forEach((k,ki)=>{
              const v=Number(r[k.key])||0; const y0=Y(acc), y1=Y(acc+v); acc+=v;
              const h=(y0-y1)*t;
              grp.push(<rect key={ki} x={cx-bw/2} y={y0-h} width={bw} height={Math.max(0,h)} fill={k.color}
                rx={ki===keys.length-1?3:0} style={{transition:'opacity .16s',opacity:hi!=null&&hi!==i?0.42:1}}/>);
            });
          } else {
            keys.forEach((k,ki)=>{
              const v=Number(r[k.key])||0; const y=Y(v); const h=(padT+ih-y)*t;
              const x=cx-(keys.length*bw+ (keys.length-1)*3)/2 + ki*(bw+3);
              grp.push(<rect key={ki} x={x} y={padT+ih-h} width={bw} height={Math.max(0,h)} fill={k.color} rx="3"
                style={{transition:'opacity .16s',opacity:hi!=null&&hi!==i?0.42:1, filter:hi===i?'drop-shadow(0 4px 10px '+k.color+'55)':null}}/>);
            });
          }
          return (
            <g key={i} onMouseEnter={()=>{if(hi!==i)setHi(i);}} onMouseLeave={()=>{setHi(null);icTip.hide();}}
               onMouseMove={e=>icTip.show(e,{ title:r.label, foot:(subLabels&&r.sub)||(onClick?'Click to drill down':''),
                 rows: keys.map(k=>({dot:k.color,k:k.label,v:icFmt(r[k.key],yKind||'short'),color:k.color})) },
                 { avoidRect: wrap.current&&wrap.current.getBoundingClientRect(), axisBand: padB })}
               onClick={()=>onClick&&onClick(r,i)} style={{cursor:onClick?'pointer':'default'}}>
              <rect x={padL+slot*i} y={padT} width={slot} height={ih} fill={hi===i?'#FAF9F7':'transparent'}/>
              {grp}
            </g>
          );
        })}
        {rows.map((r,i)=>{
          if(!xTicks.has(i)) return null;
          return <text key={i} x={padL+slot*i+slot/2} y={H-8} textAnchor="middle" fontSize="9.5" fontWeight={hi===i?700:500} fill={hi===i?'#1A1917':'#A8A4A0'}>{r.label}</text>;
        })}
      </svg>
      </div>
    </div>
  );
}

/* ══ RANKING BARS (horizontal) ════════════════════════════════════════════ */
function ICRank({ rows, valueKind, onClick, max, colorFor, right, limit }){
  const list=(limit?rows.slice(0,limit):rows);
  const [t,animRef]=useIcAnim(JSON.stringify(list.map(r=>[r.label,r.value])),780);
  const top=max||Math.max.apply(null, list.length?list.map(r=>Math.abs(r.value)):[1])||1;
  if(!list.length) return <ICEmpty h={120}/>;
  return (
    <div className="ic-rank" ref={animRef}>
      {list.map((r,i)=>{
        const c=colorFor?colorFor(r,i):IC_PAL[i%IC_PAL.length];
        const pct=Math.max(0,Math.min(100, Math.abs(r.value)/top*100));
        return (
          <div className={'ic-rank-row'+(onClick?' click':'')} key={r.key||r.id||i} onClick={()=>onClick&&onClick(r)}
               style={{animationDelay:(i*38)+'ms'}}>
            <div className="ic-rank-i">{i+1}</div>
            <div className="ic-rank-main">
              <div className="ic-rank-top">
                <span className="ic-rank-lbl" title={r.label}>{r.label}</span>
                <span className="ic-rank-val">{icFmt(r.value, valueKind||'short')}</span>
              </div>
              <div className="ic-rank-track"><div className="ic-rank-fill" style={{width:(pct*t)+'%',background:c}}></div></div>
              {(r.sub||right) && <div className="ic-rank-sub">{r.sub}{right?<span className="ic-rank-right">{right(r)}</span>:null}</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ══ DONUT ════════════════════════════════════════════════════════════════ */
function ICDonut({ slices, size, thickness, center, sub, onClick, valueKind }){
  const S=size||190, TH=thickness||24;
  const [t,animRef]=useIcAnim(JSON.stringify(slices.map(s=>[s.label,s.value])),920);
  const [hi,setHi]=icSt(null);
  const cx=S/2, cy=S/2, r=(S-TH)/2-2, circ=2*Math.PI*r;
  const total=slices.reduce((s,x)=>s+Math.max(0,x.value),0);
  let off=0;
  const arcs=slices.map((s,i)=>{ const pct=total>0?Math.max(0,s.value)/total:0;
    const a={ ...s, pct, off, color:s.color||IC_PAL[i%IC_PAL.length] }; off+=pct; return a; });
  return (
    <div className="ic-donut" ref={animRef} style={{position:'relative',width:S,height:S,flexShrink:0}}>
      <svg width={S} height={S} viewBox={'0 0 '+S+' '+S} style={{display:'block',overflow:'visible'}}>
        <circle cx={cx} cy={cy} r={r} fill="none" stroke="#F1EFEC" strokeWidth={TH}/>
        {total>0 && arcs.map((a,i)=>{
          const gap=slices.length>1?2.5:0;
          const dash=Math.max(0,a.pct*circ*t-gap);
          return <circle key={i} cx={cx} cy={cy} r={r} fill="none" stroke={a.color}
            strokeWidth={hi===i?TH+5:TH} strokeDasharray={dash+' '+(circ-dash)} strokeDashoffset={-(a.off*circ*t)}
            transform={'rotate(-90 '+cx+' '+cy+')'} style={{transition:'stroke-width .18s, opacity .18s',opacity:hi!=null&&hi!==i?0.5:1,cursor:onClick?'pointer':'default'}}
            onMouseEnter={()=>{if(hi!==i)setHi(i);}} onMouseLeave={()=>{setHi(null);icTip.hide();}}
            onMouseMove={e=>icTip.show(e,{title:a.label,rows:[{k:'Value',v:icFmt(a.value,valueKind||'cur'),color:a.color},{k:'Share',v:(a.pct*100).toFixed(1)+'%'}],foot:onClick?'Click to drill down':''})}
            onClick={()=>onClick&&onClick(a)}/>;
        })}
        <text x={cx} y={cy-2} textAnchor="middle" fontSize={S>170?15:13} fontWeight="800" fill="#1A1917" style={{fontVariantNumeric:'tabular-nums'}}>{center}</text>
        <text x={cx} y={cy+14} textAnchor="middle" fontSize="9.5" fill="#A8A4A0" fontWeight="600" letterSpacing=".04em">{sub}</text>
      </svg>
    </div>
  );
}

/* ══ WATERFALL ════════════════════════════════════════════════════════════ */
function ICWaterfall({ steps, height, onClick }){
  const H=height||250;
  const [w,wrap]=useIcWidth(660);
  const [t,animRef]=useIcAnim(JSON.stringify(steps.map(s=>[s.label,s.value])),920);
  const padL=54,padR=10,padT=16,padB=34;
  const iw=Math.max(40,w-padL-padR), ih=Math.max(40,H-padT-padB);
  let run=0; const bars=[];
  steps.forEach(s=>{
    if(s.total){ bars.push({...s, y0:0, y1:s.value, isTotal:true}); }
    else { const y0=run; run+= s.value; bars.push({...s, y0, y1:run}); }
  });
  const vals=bars.reduce((a,b)=>a.concat([b.y0,b.y1]),[0]);
  const lo=Math.min.apply(null,vals), hiV=Math.max.apply(null,vals);
  const span=(hiV-lo)||1;
  const Y=v=>padT+ih-((v-lo)/span)*ih;
  const slot=iw/bars.length, bw=Math.min(50,slot*0.56);
  if(!steps.length) return <ICEmpty h={H}/>;
  return (
    <div ref={wrap} className="ic-chart" style={{position:'relative'}}>
      <div ref={animRef}>
      <svg width="100%" height={H} viewBox={'0 0 '+w+' '+H} style={{display:'block',overflow:'visible'}}>
        <line x1={padL} x2={padL+iw} y1={Y(0)} y2={Y(0)} stroke="#D1CEC9"/>
        {bars.map((b,i)=>{
          const yA=Y(b.y0), yB=Y(b.y1);
          const top=Math.min(yA,yB), h=Math.abs(yA-yB);
          const cx=padL+slot*i+slot/2;
          const hh=h*t;
          const yy = b.y1>=b.y0 ? (top+h-hh) : top;
          return (
            <g key={i} style={{cursor:onClick?'pointer':'default'}} onClick={()=>onClick&&onClick(b)}
               onMouseMove={e=>icTip.show(e,{title:b.label,rows:[{k:b.isTotal?'Total':'Impact',v:icCur(b.value),color:b.color},{k:'Running',v:icCur(b.y1)}],foot:onClick?'Click to drill down':''})}
               onMouseLeave={()=>icTip.hide()}>
              <rect x={cx-bw/2} y={yy} width={bw} height={Math.max(1.5,hh)} rx="3" fill={b.color}
                style={{filter:'drop-shadow(0 3px 8px '+b.color+'33)'}}/>
              {i<bars.length-1 && <line x1={cx+bw/2} x2={padL+slot*(i+1)+slot/2-bw/2} y1={Y(b.y1)} y2={Y(b.y1)} stroke="#D1CEC9" strokeDasharray="3 3" opacity={t}/>}
              <text x={cx} y={H-20} textAnchor="middle" fontSize="9" fill="#6B7068" fontWeight="600">{b.short||b.label}</text>
              <text x={cx} y={H-8} textAnchor="middle" fontSize="9" fill={b.color} fontWeight="700">{icShort(b.value)}</text>
            </g>
          );
        })}
      </svg>
      </div>
    </div>
  );
}

/* ══ GAUGE (health score) ═════════════════════════════════════════════════ */
function ICGauge({ value, size, label, sub, color }){
  const S=size||188;
  const [t,animRef]=useIcAnim(String(value),1200);
  const r=S/2-16, cx=S/2, cy=S/2;
  const start=-215, end=35, span=end-start;
  const pol=(ang,rr)=>[cx+rr*Math.cos(ang*Math.PI/180), cy+rr*Math.sin(ang*Math.PI/180)];
  const arc=(a0,a1,rr)=>{ const [x0,y0]=pol(a0,rr),[x1,y1]=pol(a1,rr); const large=Math.abs(a1-a0)>180?1:0;
    return 'M'+x0.toFixed(1)+' '+y0.toFixed(1)+' A'+rr+' '+rr+' 0 '+large+' 1 '+x1.toFixed(1)+' '+y1.toFixed(1); };
  const v=Math.max(0,Math.min(100,Number(value)||0));
  const ang=start+span*(v/100)*t;
  const col=color||(v>=70?'#16A34A':v>=55?'#F97316':'#DC2626');
  return (
    <div ref={animRef} style={{position:'relative',width:S,height:S*0.78}}>
      <svg width={S} height={S*0.86} viewBox={'0 0 '+S+' '+(S*0.86)} style={{display:'block',overflow:'visible'}}>
        <defs>
          <linearGradient id="icgauge" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%" stopColor="#DC2626"/><stop offset="45%" stopColor="#F97316"/><stop offset="100%" stopColor="#16A34A"/>
          </linearGradient>
        </defs>
        <path d={arc(start,end,r)} fill="none" stroke="#F1EFEC" strokeWidth="13" strokeLinecap="round"/>
        <path d={arc(start,end,r)} fill="none" stroke="url(#icgauge)" strokeWidth="13" strokeLinecap="round" opacity=".2"/>
        <path d={arc(start,ang,r)} fill="none" stroke={col} strokeWidth="13" strokeLinecap="round" style={{filter:'drop-shadow(0 3px 10px '+col+'55)'}}/>
        {(()=>{ const [x,y]=pol(ang,r); return <circle cx={x} cy={y} r="6.5" fill="#fff" stroke={col} strokeWidth="3"/>; })()}
        <text x={cx} y={cy+6} textAnchor="middle" fontSize="34" fontWeight="800" fill="#1A1917" style={{fontVariantNumeric:'tabular-nums',letterSpacing:'-.03em'}}>{Math.round(v*t)}</text>
        <text x={cx} y={cy+24} textAnchor="middle" fontSize="10" fontWeight="700" fill={col} letterSpacing=".07em">{label}</text>
        {sub && <text x={cx} y={cy+40} textAnchor="middle" fontSize="9" fill="#A8A4A0">{sub}</text>}
      </svg>
    </div>
  );
}

/* ══ CALENDAR HEATMAP ═════════════════════════════════════════════════════
   Adaptive sizing: the grid measures its own card width and solves cell
   size FROM that width and the column count (weeks), targeting ~85% of the
   available width — a 4-column month view and a 52-column year view both
   fill their card proportionally instead of one rendering as a tiny island
   or the other overflowing. Cell size is clamped to a comfortable readable
   band; only truly dense histories (many years) fall back to horizontal
   scroll rather than shrinking past legibility. Row count (7) is fixed, so
   the card's height simply follows the solved cell size — never fixed,
   never ballooned by an artificial aspect-ratio.                         */
function ICHeat({ cells, onClick, valueKind, weeks }){
  const [t,animRef]=useIcAnim(String(cells.length)+cells.reduce((s,c)=>s+c.value,0),700);
  const [w,widthRef]=useIcWidth(560);
  const setRefs=icCb(node=>{ animRef.current=node; widthRef.current=node; },[]);
  const wcols = weeks||Math.ceil(cells.length/7)||1;
  /* the canvas owns its own padding; the grid is solved from the padded inner
     box (border-box width minus padding and the 1px hairline on each side) so
     no column can ever land under an edge or be clipped by the scroll box. */
  const padX = w<420?20:28, padY = w<420?18:22;
  const inner = Math.max(0, w - padX*2 - 2);
  const gap = wcols<=13?8:wcols<=26?6:4;
  const minCell=9, maxCell=30;
  const target = Math.max(0, inner - gap*(wcols-1));
  const cell = Math.max(minCell, Math.min(maxCell, target/wcols));
  const radius = Math.max(3, Math.min(11, cell*0.34));
  const max=Math.max.apply(null, cells.length?cells.map(c=>c.value):[1])||1;
  const col=v=>{ if(v<=0) return '#F4F2EF'; const f=Math.pow(v/max,0.62);
    return 'rgba(249,115,22,'+(0.14+f*0.86).toFixed(3)+')'; };
  return (
    <div className="ic-heat" ref={setRefs} style={{position:'relative'}}>
      <div className="ic-heat-scroll" style={{padding:padY+'px '+padX+'px'}}>
        <div className="ic-heat-grid" style={{gridTemplateColumns:'repeat('+wcols+', '+cell+'px)',gridTemplateRows:'repeat(7, '+cell+'px)',gap:gap+'px'}}>
          {cells.map((c,i)=>(
            <div key={i} className="ic-heat-c" title={c.label}
              style={{background:col(c.value), opacity:t, transitionDelay:(i*4)+'ms', cursor:onClick?'pointer':'default', borderRadius:radius+'px'}}
              onMouseMove={e=>icTip.show(e,{title:c.label,rows:[{k:'Value',v:icFmt(c.value,valueKind||'cur')},{k:'Records',v:String(c.count||0)}],foot:onClick?'Click to drill down':''})}
              onMouseLeave={()=>icTip.hide()} onClick={()=>onClick&&onClick(c)}></div>
          ))}
        </div>
      </div>
      <div className="ic-heat-legend" style={{padding:'0 '+padX+'px '+padY+'px'}}><span>Low</span><i style={{background:col(max*0.1)}}></i><i style={{background:col(max*0.35)}}></i><i style={{background:col(max*0.62)}}></i><i style={{background:col(max)}}></i><span>High</span></div>
    </div>
  );
}

/* ══ RADAR ════════════════════════════════════════════════════════════════
   Responsive and clip-proof. The plot radius is solved FROM the measured
   container and the real label extents, so labels can never leave the card
   however many axes arrive or however long they are. Labels wrap to two
   lines, are centred on their own axis, and the whole figure re-solves on
   resize. `size` is the preferred height, not a fixed box.              */
function icWrapLabel(s, per){
  const words=String(s||'').split(/\s+/); const out=[]; let cur='';
  words.forEach(w=>{ if((cur+' '+w).trim().length>per && cur){ out.push(cur); cur=w; } else cur=(cur+' '+w).trim(); });
  if(cur) out.push(cur);
  return out.slice(0,2);
}
function ICRadar({ axes, series, size, height, tone, notes }){
  const H=height||size||260;
  const [w,wrap]=useIcWidth(320);
  const [t,animRef]=useIcAnim(JSON.stringify([axes,series.map(s=>s.values)]),960);
  const [hov,setHov]=icSt(null);
  const n=axes.length;
  const lines=icMemo(()=>axes.map(a=>icWrapLabel(a, n>10?12:14)),[JSON.stringify(axes),n]);
  const labW=icMemo(()=>Math.max.apply(null,lines.map(L=>Math.max.apply(null,L.map(s=>s.length))*5.15+8).concat([40])),[lines]);
  const cx=w/2, cy=H/2;
  const r=Math.max(44, Math.min(w/2-labW-8, H/2-(n>2?30:22)));
  const pt=(i,f)=>{ const a=(-90+360*i/n)*Math.PI/180; return [cx+r*f*Math.cos(a), cy+r*f*Math.sin(a)]; };
  if(!n) return <ICEmpty h={H}/>;
  return (
    <div ref={wrap} className="ic-radar" style={{position:'relative',width:'100%'}}>
      <div ref={animRef}>
      <svg width="100%" height={H} viewBox={'0 0 '+w+' '+H} style={{display:'block',overflow:'hidden'}} role="img">
        {[0.25,0.5,0.75,1].map((f,i)=>(
          <polygon key={'g'+i} points={axes.map((a,j)=>pt(j,f).join(',')).join(' ')} fill={i===3?'#FCFBFA':'none'} stroke={i===3?'#E7E4E0':'#F1EFEC'} strokeWidth="1"/>
        ))}
        {axes.map((a,i)=>{ const [x,y]=pt(i,1); return <line key={'a'+i} x1={cx} y1={cy} x2={x} y2={y} stroke={hov===i?'#D8D4CF':'#F1EFEC'} strokeWidth="1"/>; })}
        {[0.5,1].map((f,i)=>(
          <text key={'r'+i} x={cx+3} y={cy-r*f+3} fontSize="8" fill="#C3BFB9" fontWeight="700">{f*100}</text>
        ))}
        {series.map((s,si)=>(
          <g key={'s'+si}>
            <polygon points={s.values.map((v,i)=>pt(i,Math.max(0,Math.min(1,v))*t).join(',')).join(' ')}
              fill={s.color} fillOpacity={series.length>1?'.10':'.15'} stroke={s.color} strokeWidth="2" strokeLinejoin="round"/>
            {s.values.map((v,i)=>{ const [x,y]=pt(i,Math.max(0,Math.min(1,v))*t);
              return <circle key={i} cx={x} cy={y} r={hov===i?4.6:3.4} fill="#fff" stroke={s.color} strokeWidth="2" style={{transition:'r .18s'}}
                onMouseMove={e=>{ setHov(i); icTip.show(e,{title:axes[i],rows:series.map(z=>({k:z.label,v:Math.round(Math.max(0,Math.min(1,z.values[i]))*100)+' / 100',color:z.color})),foot:notes&&notes[i]?notes[i]:''}); }}
                onMouseLeave={()=>{ setHov(null); icTip.hide(); }}/>; })}
          </g>
        ))}
        {axes.map((a,i)=>{
          const [x,y]=pt(i,1); const dx=(x-cx)/r, dy=(y-cy)/r;
          const lx=cx+(r+11)*dx, ly=cy+(r+11)*dy;
          const anchor=dx<-0.25?'end':dx>0.25?'start':'middle';
          const L=lines[i]; const top=ly-(L.length-1)*5.2+(dy>0.6?4:dy<-0.6?-2:3);
          return <g key={'l'+i}>{L.map((s,k)=>(
            <text key={k} x={lx} y={top+k*10.4} textAnchor={anchor} fontSize="9.3" fontWeight={hov===i?800:600} fill={hov===i?'#1A1917':'#6B7068'}>{s}</text>
          ))}</g>;
        })}
      </svg>
      </div>
      {series.length>1 && (
        <div className="ic-legend">
          {series.map((s,i)=><span key={i}><i style={{background:s.color}}></i>{s.label}</span>)}
        </div>
      )}
    </div>
  );
}

/* ══ TREEMAP ══════════════════════════════════════════════════════════════ */
function ICTree({ rows, height, onClick, valueKind }){
  const H=height||230;
  const [w,wrap]=useIcWidth(640);
  const [t,animRef]=useIcAnim(JSON.stringify(rows.map(r=>[r.label,r.value])),820);
  const list=rows.filter(r=>r.value>0).slice(0,14);
  const total=list.reduce((s,r)=>s+r.value,0);
  const boxes=icMemo(()=>{
    // squarified-ish slice & dice
    const out=[]; let x=0,y=0,cw=w,ch=H; let items=list.slice(); let horiz=w>=H;
    while(items.length){
      const rest=items.reduce((s,r)=>s+r.value,0);
      const it=items.shift();
      const f=rest>0?it.value/rest:1;
      if(items.length===0){ out.push({...it, x,y,w:cw,h:ch}); break; }
      if(horiz){ const bw=cw*f; out.push({...it,x,y,w:bw,h:ch}); x+=bw; cw-=bw; }
      else { const bh=ch*f; out.push({...it,x,y,w:cw,h:bh}); y+=bh; ch-=bh; }
      horiz=!horiz;
    }
    return out;
  },[w,H,JSON.stringify(list.map(r=>[r.label,r.value]))]);
  if(!list.length) return <ICEmpty h={H}/>;
  return (
    <div ref={wrap} style={{position:'relative'}}>
      <div ref={animRef}>
      <svg width="100%" height={H} viewBox={'0 0 '+w+' '+H} style={{display:'block'}}>
        {boxes.map((b,i)=>{
          const c=IC_PAL[i%IC_PAL.length];
          const pad=1.6;
          return (
            <g key={i} style={{cursor:onClick?'pointer':'default',opacity:t,transition:'opacity .3s'}}
               onClick={()=>onClick&&onClick(b)}
               onMouseMove={e=>icTip.show(e,{title:b.label,rows:[{k:'Value',v:icFmt(b.value,valueKind||'cur'),color:c},{k:'Share',v:(total>0?b.value/total*100:0).toFixed(1)+'%'},{k:'Quantity',v:icTon(b.qty)}],foot:onClick?'Click to drill down':''})}
               onMouseLeave={()=>icTip.hide()}>
              <rect x={b.x+pad} y={b.y+pad} width={Math.max(0,b.w-pad*2)} height={Math.max(0,b.h-pad*2)} rx="5" fill={c} fillOpacity={0.92}/>
              {b.w>62 && b.h>28 && <text x={b.x+9} y={b.y+19} fontSize="10.5" fontWeight="700" fill="#fff">{b.label.length>16?b.label.slice(0,15)+'…':b.label}</text>}
              {b.w>62 && b.h>44 && <text x={b.x+9} y={b.y+33} fontSize="9.5" fill="#fff" opacity=".85">{icShort(b.value)}</text>}
            </g>
          );
        })}
      </svg>
      </div>
    </div>
  );
}

/* ══ SPARKLINE ════════════════════════════════════════════════════════════ */
function ICSpark({ values, color, height, width, area }){
  const H=height||34, W=width||96;
  const [t,animRef]=useIcAnim(JSON.stringify(values),700);
  const v=(values||[]).map(x=>Number(x)||0);
  if(v.length<2) return <div style={{height:H,width:W}}></div>;
  const max=Math.max.apply(null,v), min=Math.min.apply(null,v);
  const span=(max-min)||1;
  const X=i=>i/(v.length-1)*W, Y=x=>H-2-((x-min)/span)*(H-6);
  let d='';
  v.forEach((x,i)=>{ const px=X(i), py=Y(x); d+= i===0?('M'+px.toFixed(1)+' '+py.toFixed(1)):(' L'+px.toFixed(1)+' '+py.toFixed(1)); });
  const last=v[v.length-1];
  return (
    <svg ref={animRef} width={W} height={H} viewBox={'0 0 '+W+' '+H} style={{display:'block',overflow:'visible'}}>
      {area && <path d={d+' L'+W+' '+H+' L0 '+H+' Z'} fill={color} opacity={0.10*t}/>}
      <path d={d} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"
        style={{strokeDasharray:400,strokeDashoffset:400*(1-t)}}/>
      <circle cx={X(v.length-1)} cy={Y(last)} r="2.6" fill={color} opacity={t}/>
    </svg>
  );
}

/* ══ FUNNEL ═══════════════════════════════════════════════════════════════ */
function ICFunnel({ rows, onClick, valueKind }){
  const [t,animRef]=useIcAnim(JSON.stringify(rows.map(r=>[r.label,r.value])),840);
  const max=Math.max.apply(null, rows.length?rows.map(r=>r.value):[1])||1;
  if(!rows.length) return <ICEmpty h={140}/>;
  return (
    <div className="ic-funnel" ref={animRef}>
      {rows.map((r,i)=>{
        const pct=r.value/max*100;
        const c=r.color||IC_PAL[i%IC_PAL.length];
        const drop=i>0 && rows[i-1].value>0 ? (r.value-rows[i-1].value)/rows[i-1].value*100 : null;
        return (
          <div className={'ic-funnel-row'+(onClick?' click':'')} key={i} onClick={()=>onClick&&onClick(r)}>
            <div className="ic-funnel-bar" style={{width:Math.max(6,pct*t)+'%',background:'linear-gradient(90deg,'+c+','+c+'CC)'}}>
              <span>{r.label}</span>
            </div>
            <div className="ic-funnel-meta">
              <b>{icFmt(r.value,valueKind||'short')}</b>
              {drop!=null && <em style={{color:drop>=0?'#16A34A':'#DC2626'}}>{drop>=0?'+':''}{drop.toFixed(0)}%</em>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ══ BULLET (target vs actual) ════════════════════════════════════════════ */
function ICBullet({ value, target, kind, color }){
  const [t,animRef]=useIcAnim(String(value)+'/'+String(target),760);
  const max=Math.max(value,target)*1.1||1;
  const c=color||'#F97316';
  return (
    <div className="ic-bullet" ref={animRef}>
      <div className="ic-bullet-track">
        <div className="ic-bullet-fill" style={{width:(value/max*100*t)+'%',background:c}}></div>
        <div className="ic-bullet-target" style={{left:(target/max*100)+'%'}}></div>
      </div>
      <div className="ic-bullet-meta"><b>{icFmt(value,kind||'short')}</b><span>vs {icFmt(target,kind||'short')}</span></div>
    </div>
  );
}

Object.assign(window, { IC: {
  Area:ICArea, Bars:ICBars, Rank:ICRank, Donut:ICDonut, Waterfall:ICWaterfall, Gauge:ICGauge,
  Heat:ICHeat, Radar:ICRadar, Tree:ICTree, Spark:ICSpark, Funnel:ICFunnel, Bullet:ICBullet,
  Num:ICNum, Empty:ICEmpty, useAnim:useIcAnim, useWidth:useIcWidth, pickTicks:icPickTicks,
  fmt:icFmt, short:icShort, cur:icCur, ton:icTon, int:icInt, pct:icPct, PAL:IC_PAL, MODE_COLOR:IC_MODE_COLOR,
}});
