/* WBR Scorecard · MDC Brand · React app */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

const LS_KEY = 'wbr-state-v7';
const SNAPSHOT_KEY = 'wbr-last-close-v1';
const VIEW_COVER = '__cover', VIEW_PURPOSE='__purpose', VIEW_OVERVIEW='__overview';
const VIEW_MISSING='__missing', VIEW_IDS='__ids', VIEW_COMMITS='__commits', VIEW_LOCK='__lock';

/* Build next-week's starting state from a closed-week snapshot.
   - this-week's actual/color become next-week's priorActual/priorColor
   - this-week's open commits carry as priorCommitments[] w/ status='open'
   - this-week's DONE commits drop off (self-cleaning)
   - this-week's actual/color/commits/overrideReason/items reset to blank
   - week increments, priorWeekLabel updates
*/
function buildNextWeekState(snap){
  const next = JSON.parse(JSON.stringify(snap));
  const closedWeek = next.meta.week;
  next.meta.priorWeekLabel = `WK${String(closedWeek).padStart(2,'0')}`;
  next.meta.week = closedWeek + 1;
  // Update week dates: bump 7 days
  try {
    if(next.meta.date){
      const d = new Date(next.meta.date);
      if(!Number.isNaN(d.getTime())){
        d.setDate(d.getDate()+7);
        next.meta.date = d.toISOString().slice(0,10);
      }
    }
  } catch(e){}
  next.savedAt = null;
  next.idsQueue = [];
  next.commitments = [];
  next.missing = next.missing || {prompts:[], captured:[]};
  next.missing.captured = [];

  next.kpis = next.kpis.map(k=>{
    // Carry open/missed commits forward as priorCommitments
    const carried = (k.commits||[])
      .filter(c=>!c.done)                         // drop done
      .map(c=>({owner:c.owner, action:c.action, due:c.due, status:'open'}));
    return {
      ...k,
      priorActual: k.actual || k.priorActual || '',
      priorColor:  k.color  || k.priorColor  || '',
      priorItems:  k.items && k.items.length ? JSON.parse(JSON.stringify(k.items)) : (k.priorItems||[]),
      priorCommitments: carried,
      actual: '',
      color: '',
      items: [],
      commits: [],
      committedAction: '',
      overrideReason: '',
      notes: '',
      _rosterExpanded: false,
    };
  });
  return next;
}

/* Capture a pristine deep-clone of the seed at module-load time so Reset
   always has a clean copy — even if something later mutates window.__WBR_STATE
   or if the reference is swapped by a bundler. */
function getWbrSeed(){ return window.__WBR_STATE ? JSON.parse(JSON.stringify(window.__WBR_STATE)) : null; }
const WBR_SEED = null;

function loadState(){
  const seed = getWbrSeed();
  try{
    const cached = localStorage.getItem(LS_KEY);
    if(cached){
      const parsed = JSON.parse(cached);
      if(parsed && parsed.meta && seed && parsed.meta.week === seed.meta.week) return parsed;
    }
  }catch(e){}
  return JSON.parse(JSON.stringify(seed));
}

function useWBRState(){
  const [state, setState] = useState(loadState);
  useEffect(()=>{ try{ localStorage.setItem(LS_KEY, JSON.stringify(state)); }catch(e){} }, [state]);
  const update = useCallback((mutator)=>{
    setState(prev=>{
      const next = JSON.parse(JSON.stringify(prev));
      mutator(next);
      return next;
    });
  }, []);
  return [state, update, setState];
}

/* ─── Helpers ──────────────────────────────────────────────────── */
function Arrow({dir}){
  // dir: 'up' | 'down' | 'flat'
  if(dir==='up')   return <span className="arrow">↑</span>;
  if(dir==='down') return <span className="arrow">↓</span>;
  return <span className="arrow">→</span>;
}

function extractNumber(s){
  if(!s) return null;
  const cleaned = String(s).replace(/[^0-9.\-]/g,'');
  if(!cleaned) return null;
  const n = parseFloat(cleaned);
  return isNaN(n) ? null : n;
}

/* Parse a threshold text string into {min, max} (either may be Infinity).
   Handles these shapes seen in the data:
     "≤ 10%"              → {min:-Inf, max:10}
     "< 10%"              → {min:-Inf, max:10}          (strict, but we fold strictness below)
     "0 triggers"         → {min:0, max:0}
     "10 – 25%"           → {min:10, max:25}
     "11 – 15% off"       → {min:11, max:15}   (relative — % off target — handled by caller)
     "8 – 21"             → {min:8, max:21}
     "> 25%"              → {min:25, max:Inf}
     "> 30%"              → {min:30, max:Inf}
     "22+"                → {min:22, max:Inf}
     "9+ flagged"         → {min:9, max:Inf}
     "Any Critical Blocker" → null (non-numeric RED — treated as "catch-all")
*/
function parseThresholdText(s){
  if(!s) return null;
  const raw = String(s);
  // Non-numeric catch-all (e.g. "Any Critical Blocker") — let caller decide.
  if(!/\d/.test(raw)) return {nonNumeric:true};
  // Range with en-dash or hyphen: "A – B" or "A - B"
  const range = raw.match(/(-?\d+(?:\.\d+)?)\s*[–-]\s*(-?\d+(?:\.\d+)?)/);
  if(range) return {min:parseFloat(range[1]), max:parseFloat(range[2])};
  // "A+" or "A or more"
  const plus = raw.match(/(-?\d+(?:\.\d+)?)\s*\+/);
  if(plus) return {min:parseFloat(plus[1]), max:Infinity};
  // "> A" / "≥ A"
  const gt = raw.match(/[>≥]\s*(-?\d+(?:\.\d+)?)/);
  if(gt) return {min:parseFloat(gt[1]), max:Infinity};
  // "< A" / "≤ A"
  const lt = raw.match(/[<≤]\s*(-?\d+(?:\.\d+)?)/);
  if(lt) return {min:-Infinity, max:parseFloat(lt[1])};
  // Single number: "0 triggers", "5 flagged" — treat as exact.
  const one = raw.match(/(-?\d+(?:\.\d+)?)/);
  if(one){ const n = parseFloat(one[1]); return {min:n, max:n}; }
  return null;
}

/* Does a threshold text string hint that its numbers are RELATIVE
   (percent off target) rather than absolute? MRC uses this. */
function isRelativeThreshold(s){
  return /off\s*target|% off/i.test(String(s||''));
}

/* Suggest a GREEN/YELLOW/RED call from the numeric reading vs target.
   Uses the KPI's own thresholds[] when present — each threshold's text
   is parsed to numeric bounds. Falls back to a generic ±10% band. */
function suggestColor(actual, target, betterDirection, thresholds){
  const a = extractNumber(actual);
  if(a===null) return null;

  if(Array.isArray(thresholds) && thresholds.length){
    // Are ALL threshold texts relative (% off target)? MRC case.
    const relative = thresholds.every(t=>isRelativeThreshold(t.text));
    let testValue;
    if(relative){
      const t = extractNumber(target);
      if(t===null || t===0) return null;
      // Direction matters: being past target in the good direction is always GREEN,
      // regardless of "% off" bands (those describe MISSING by X%, not exceeding).
      const beating = betterDirection==='down' ? (a <= t) : (a >= t);
      if(beating) return 'GREEN';
      testValue = Math.abs(a - t) / Math.abs(t) * 100; // % off target (shortfall)
    } else {
      testValue = a;
    }
    // Check each threshold. Try numeric ranges first; pick the one that contains testValue.
    // "Any X" catch-alls (non-numeric) belong to their color bucket as fallbacks.
    let catchAll = null;
    for(const t of thresholds){
      const p = parseThresholdText(t.text);
      if(!p) continue;
      if(p.nonNumeric){ catchAll = t.color; continue; }
      if(testValue >= p.min && testValue <= p.max) return t.color;
      // Also handle ranges expressed exclusively at the edges:
      // if "< 10%" means strictly less, 10 itself should not be GREEN. We accept
      // <= for simplicity — the band definitions in data use ≤ interchangeably.
    }
    // No numeric bucket matched — fall to catch-all if any, else out-of-bounds.
    if(catchAll) return catchAll;
    // If we got here, the value is outside all declared buckets. Pick
    // the extreme color based on betterDirection.
    return betterDirection==='down' ? 'RED' : 'GREEN';
  }

  // No thresholds — generic ±10% band around target.
  const t = extractNumber(target);
  if(t===null) return null;
  if(t===0){
    if(a===0) return 'GREEN';
    return betterDirection==='down' ? 'RED' : 'GREEN';
  }
  const hit = betterDirection==='down' ? (a <= t) : (a >= t);
  if(hit) return 'GREEN';
  const gap = Math.abs(a - t) / Math.abs(t);
  if(gap <= 0.10) return 'YELLOW';
  return 'RED';
}

/* Format a raw user-typed value to match the unit implied by a reference
   string (usually the KPI target or prior reading). Non-destructive:
   if the user already typed a formatted value (has $, %, commas) we leave
   it alone. Only called on blur so typing is never hijacked mid-entry. */
function formatToMatchRef(raw, refStr){
  if(!raw) return raw;
  const s = String(raw).trim();
  if(!s) return s;
  // If user clearly formatted it themselves, don't touch.
  if(/[$,%]/.test(s)) return s;
  const n = parseFloat(s.replace(/[^0-9.\-]/g,''));
  if(isNaN(n)) return raw;
  const ref = String(refStr||'');
  const wantsDollar  = /\$/.test(ref);
  const wantsPercent = /%/.test(ref);
  const isInt = Number.isInteger(n);
  const fmtNum = isInt
    ? n.toLocaleString('en-US')
    : n.toLocaleString('en-US',{minimumFractionDigits:1, maximumFractionDigits:2});
  if(wantsDollar)  return '$' + fmtNum;
  if(wantsPercent) return fmtNum + '%';
  // No unit implied — still add thousands separators for readability.
  return Math.abs(n) >= 1000 ? fmtNum : s;
}

/* Split a prior/current label into HEAD (big number) + TAIL (context chip).
   Examples:
     "36% (6/17)"       → {head:"36%",     tail:"6/17"}
     "156 ODD"          → {head:"156",     tail:"ODD"}
     "8 flagged"        → {head:"8",       tail:"flagged"}
     "$11,550"          → {head:"$11,550", tail:""}      ← commas inside numbers don't split
     "HIGH"             → {head:"HIGH",    tail:""}
   Rules (in priority order):
   1. Parenthetical — always splits.
   2. Middle-dot " · ".
   3. Comma + space " , ", but only if the LHS isn't a continuing number
      (i.e. the comma isn't between digits, which is a thousands separator).
   4. "<number-ish> <trailing word(s)>" — splits number from unit word.
   5. Otherwise, treat whole thing as head. */
function splitHeadline(raw){
  if(!raw) return {head:'', tail:''};
  const s = String(raw).trim();
  // 1. Parenthetical
  const mParen = s.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
  if(mParen) return {head:mParen[1].trim(), tail:mParen[2].trim()};
  // 2. Middle-dot
  const mDot = s.match(/^(.+?)\s+·\s+(.+)$/);
  if(mDot) return {head:mDot[1].trim(), tail:mDot[2].trim()};
  // 3. Comma+space — but NOT a thousands separator like "11,550"
  const mComma = s.match(/^(.+?)(?<!\d),\s+(.+)$/);
  if(mComma) return {head:mComma[1].trim(), tail:mComma[2].trim()};
  // 4. Number followed by trailing unit word(s)
  const mUnit = s.match(/^([\-+]?[\$€£]?[\d,.]+%?)\s+(.+)$/);
  if(mUnit) return {head:mUnit[1].trim(), tail:mUnit[2].trim()};
  return {head:s, tail:''};
}

function computeTrend(prior, current, betterDirection){
  const p = extractNumber(prior), c = extractNumber(current);
  if(p===null || c===null) return {dir:null, kind:null, delta:null, pct:null};
  if(p===c) return {dir:'flat', kind:'flat', delta:'no change', pct:'0%'};
  const up = c > p;
  const dir = up ? 'up' : 'down';
  const good = (betterDirection==='up' && up) || (betterDirection==='down' && !up);
  const abs = Math.abs(c-p);

  // Sniff the unit from the CURRENT value so the delta matches the readings.
  const cur = String(current||'');
  const hasDollar = /\$/.test(cur);
  const hasPercent = /%/.test(cur);
  const isInt = Number.isInteger(abs);

  // Format the absolute magnitude with thousands separators.
  const fmtNum = isInt
    ? abs.toLocaleString('en-US')
    : abs.toLocaleString('en-US',{minimumFractionDigits:1, maximumFractionDigits:1});

  let body;
  if(hasDollar)      body = '$' + fmtNum;
  else if(hasPercent) body = fmtNum + ' pts';   // percentage-point delta, not %
  else                body = fmtNum;

  const delta = (up?'+':'−') + body;
  // Also compute a relative % change for context (e.g. "+56%")
  const pct = p !== 0
    ? ((up?'+':'−') + Math.round((abs/Math.abs(p))*100) + '%')
    : null;
  return {dir, kind: good?'good':'bad', delta, pct};
}

/* ─── Toast host ─────────────────────────────────────────────── */
function useToasts(){
  const [toasts, setToasts] = useState([]);
  const push = useCallback((msg, kind='info')=>{
    const id = Math.random().toString(36).slice(2);
    setToasts(t=>[...t,{id,msg,kind}]);
    setTimeout(()=>setToasts(t=>t.filter(x=>x.id!==id)), 2600);
  }, []);
  const node = (
    <div className="toast-wrap">
      {toasts.map(t=><div key={t.id} className={`toast ${t.kind}`}>{t.msg}</div>)}
    </div>
  );
  return [push, node];
}

/* ─── DuePicker — compact 7-day chip picker tuned for the WBR cadence.
   The rhythm is a 7-day commitment-to-next-meeting window, so a full
   month calendar would be overkill — one row of day chips is faster.
   Falls back to a native date input for anything further out. ─── */
function DuePicker({value, onChange, compact=false, onSubmit}){
  const [open, setOpen] = useState(false);
  const [showNative, setShowNative] = useState(false);
  const wrapRef = useRef(null);

  useEffect(()=>{
    if(!open) return;
    const onDoc = (e)=>{ if(wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return ()=>document.removeEventListener('mousedown', onDoc);
  }, [open]);

  const isISO = /^\d{4}-\d{2}-\d{2}$/.test(value||'');
  const parsed = isISO ? new Date(value+'T00:00:00') : null;
  const today = new Date(); today.setHours(0,0,0,0);
  const days = [];
  for(let i=0;i<8;i++){
    const d = new Date(today); d.setDate(today.getDate()+i);
    days.push(d);
  }
  const iso = (d)=>{
    const y=d.getFullYear(), m=String(d.getMonth()+1).padStart(2,'0'), dd=String(d.getDate()).padStart(2,'0');
    return `${y}-${m}-${dd}`;
  };
  const wkday = ['SUN','MON','TUE','WED','THU','FRI','SAT'];
  const fmtLabel = (d)=>{
    const t = new Date(today); t.setHours(0,0,0,0);
    const diff = Math.round((d-t)/86400000);
    if(diff===0) return 'TODAY';
    if(diff===1) return 'TMRW';
    return wkday[d.getDay()];
  };
  const fmtDisplay = (d)=>{
    if(!d) return 'Set date';
    const t = new Date(today); t.setHours(0,0,0,0);
    const diff = Math.round((d-t)/86400000);
    const w = wkday[d.getDay()];
    const md = `${d.getMonth()+1}/${d.getDate()}`;
    if(diff===0) return `Today · ${md}`;
    if(diff===1) return `Tmrw · ${md}`;
    if(diff>1 && diff<=7) return `${w} ${md}`;
    return iso(d);
  };

  const displayText = parsed ? fmtDisplay(parsed) : 'Set date';
  const isOverdue = parsed && parsed < today;

  return (
    <span ref={wrapRef} style={{position:'relative',display:'inline-flex',alignItems:'center'}}>
      <button
        type="button"
        onClick={()=>setOpen(o=>!o)}
        onFocus={()=>setOpen(true)}
        onKeyDown={(e)=>{
          if(e.key==='Enter' && !open && onSubmit){ e.preventDefault(); onSubmit(); }
        }}
        aria-label="Set due date"
        style={{
          background:'transparent',border:'none',outline:'none',padding:'2px 4px',cursor:'pointer',
          font:'inherit',fontFamily:'var(--mdc-font-mono)',fontSize:compact?13:14,
          letterSpacing:'.08em',textTransform:'uppercase',fontWeight:500,
          color:isOverdue?'var(--wbr-red)':(parsed?'var(--mdc-ink)':'var(--mdc-ink-faint)'),
          borderBottom:'1px dashed var(--mdc-line-strong)',borderRadius:0
        }}
      >
        {displayText} <span style={{opacity:.55,marginLeft:4}}>▾</span>
      </button>
      {open && (
        <div style={{
          position:'absolute',top:'calc(100% + 6px)',left:0,zIndex:20,
          background:'var(--mdc-white)',border:'1px solid var(--mdc-line-strong)',
          borderRadius:'var(--mdc-radius-md)',padding:10,
          boxShadow:'0 12px 28px -8px rgba(0,0,0,.22), 0 4px 10px -4px rgba(0,0,0,.08)',
          display:'flex',flexDirection:'column',gap:8,minWidth:340
        }} onClick={e=>e.stopPropagation()}>
          <div style={{fontFamily:'var(--mdc-font-mono)',fontSize:11,letterSpacing:'.14em',color:'var(--mdc-ink-soft)',textTransform:'uppercase'}}>
            Due by next WBR
          </div>
          <div style={{display:'grid',gridTemplateColumns:'repeat(8, minmax(0,1fr))',gap:4}}>
            {days.map((d,i)=>{
              const selected = parsed && iso(d)===iso(parsed);
              return (
                <button key={i} type="button"
                  onClick={()=>{ onChange(iso(d)); setOpen(false); }}
                  style={{
                    display:'flex',flexDirection:'column',alignItems:'center',gap:2,
                    padding:'8px 4px',cursor:'pointer',
                    background:selected?'var(--mdc-blue)':'var(--mdc-paper)',
                    color:selected?'var(--mdc-white)':'var(--mdc-ink)',
                    border:'1px solid '+(selected?'var(--mdc-blue)':'var(--mdc-line)'),
                    borderRadius:'var(--mdc-radius-sm)',
                    fontFamily:'var(--mdc-font-mono)'
                  }}
                >
                  <span style={{fontSize:10,letterSpacing:'.1em',opacity:selected?.9:.65,fontWeight:600}}>{fmtLabel(d)}</span>
                  <span style={{fontSize:18,fontWeight:600,letterSpacing:'-.01em',lineHeight:1}}>{d.getDate()}</span>
                </button>
              );
            })}
          </div>
          <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:8,paddingTop:6,borderTop:'1px dashed var(--mdc-line)'}}>
            {showNative ? (
              <input type="date" value={isISO?value:''}
                onChange={e=>{ if(e.target.value){ onChange(e.target.value); setOpen(false); setShowNative(false); } }}
                style={{font:'inherit',fontFamily:'var(--mdc-font-mono)',fontSize:13,padding:'4px 8px',border:'1px solid var(--mdc-line-strong)',borderRadius:'var(--mdc-radius-sm)',background:'var(--mdc-white)',color:'var(--mdc-ink)',outline:'none'}}
                autoFocus
              />
            ) : (
              <button type="button" onClick={()=>setShowNative(true)}
                style={{background:'transparent',border:'none',outline:'none',padding:'2px 0',cursor:'pointer',font:'inherit',fontFamily:'var(--mdc-font-mono)',fontSize:11,letterSpacing:'.14em',textTransform:'uppercase',color:'var(--mdc-ink-soft)',textDecoration:'underline',textUnderlineOffset:3}}>
                Pick later date
              </button>
            )}
            {parsed && (
              <button type="button" onClick={()=>{ onChange(''); setOpen(false); }}
                style={{background:'transparent',border:'none',outline:'none',padding:'2px 0',cursor:'pointer',font:'inherit',fontFamily:'var(--mdc-font-mono)',fontSize:11,letterSpacing:'.14em',textTransform:'uppercase',color:'var(--mdc-ink-faint)'}}>
                Clear
              </button>
            )}
          </div>
        </div>
      )}
    </span>
  );
}

/* ─── Topnav ─────────────────────────────────────────────────── */
function Topnav({state, onExport, onReset, onPrint, onPresenter, clock}){
  return (
    <div className="topnav">
      <div className="brand">
        <img src={window.__MDC_LOGO || "brand/logo-color.png"} alt="MDC" />
        <div className="brand-meta">
          <span className="t">{state.meta.title}</span>
          <span className="s">Week {state.meta.week} · {state.meta.date}</span>
        </div>
      </div>
      <div className="actions">
        <span className="clock">{clock}</span>
        <button className="btn btn-secondary" onClick={onPresenter}>Presenter</button>
        <button className="btn btn-secondary" onClick={onPrint}>Print</button>
        <button className="btn btn-secondary" onClick={onReset}>Reset</button>
        <button className="btn btn-primary" onClick={onExport}>Export</button>
      </div>
    </div>
  );
}

/* ─── Tab rail ───────────────────────────────────────────────── */
function TabRail({state, active, onGo}){
  const tabs = [
    {id:VIEW_COVER,    idx:'00', name:'Cover'},
    {id:VIEW_PURPOSE,  idx:'01', name:'Purpose'},
    {id:VIEW_OVERVIEW, idx:'02', name:'Scorecard'},
    ...state.kpis.map((k,i)=>({id:k.id, idx:String(i+3).padStart(2,'0'), name:k.name, color:k.color})),
    {id:VIEW_MISSING, idx:String(state.kpis.length+3).padStart(2,'0'), name:'Missing KPIs'},
    {id:VIEW_IDS,     idx:String(state.kpis.length+4).padStart(2,'0'), name:'IDS Queue'},
    {id:VIEW_COMMITS, idx:String(state.kpis.length+5).padStart(2,'0'), name:'Commitments'},
    {id:VIEW_LOCK,    idx:String(state.kpis.length+6).padStart(2,'0'), name:'Lock'},
  ];
  return (
    <div className="rail" role="tablist">
      {tabs.map(t=>(
        <div key={t.id} className="tab" role="tab" aria-current={active===t.id} onClick={()=>onGo(t.id)} tabIndex={0}
             onKeyDown={e=>{if(e.key==='Enter'||e.key===' ')onGo(t.id);}}>
          <span className="tab-idx">{t.idx}</span>
          <span className="tab-name">
            {t.color && <span className={`tab-dot ${t.color}`}></span>}
            {t.name}
          </span>
        </div>
      ))}
    </div>
  );
}

/* ─── Cover ──────────────────────────────────────────────────── */
function Cover({state}){
  return (
    <div className="view cover" data-screen-label="00 Cover" data-om-validate="has-title">
      <div className="cover-inner">
        <p className="eyebrow">Weekly Business Review</p>
        <h1>WBR <b>Scorecard.</b></h1>
        <div className="date-row">
          <span>Week <b>{String(state.meta.week).padStart(2,'0')}</b></span>
          <span><b>{state.meta.date}</b></span>
          <span>Team <b>{state.meta.team}</b></span>
        </div>
      </div>
    </div>
  );
}

/* ─── Purpose ────────────────────────────────────────────────── */
function Purpose({state}){
  const [before, afterRaw] = state.purpose.split('—');
  const after = afterRaw ? afterRaw.trim() : '';
  return (
    <div className="view purpose" data-screen-label="01 Purpose">
      <div>
        <p className="eyebrow"><span className="idx">01 /</span>Purpose</p>
      </div>
      <div className="purpose-grid">
        <div>
          <blockquote>
            {before.trim()}{after && ' — '}{after && <b>{after}</b>}
          </blockquote>
          <p className="caption">The scorecard is not a report card — it's a conversation trigger. A RED only requires a number, not a defense; the fix comes in the IDS block.</p>
          {state.meta.priorWeekNote && (
            <div className="note">
              <b>Context ·</b> {state.meta.priorWeekNote}
            </div>
          )}
        </div>
        <div className="protocol">
          <h3>Color protocol · time budget</h3>
          {state.protocol.map(p=>(
            <div className="proto-row" key={p.key}>
              <div className={`swatch ${p.key}`}>{p.label}</div>
              <div>
                <div className="cue">{p.cue}</div>
                <div className="action">{p.action}</div>
              </div>
              <div className="budget">{p.budget}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ─── Overview ───────────────────────────────────────────────── */
function Overview({state, onOpen}){
  // These counts reflect LAST WEEK's closing scores — frozen history,
  // the contract the room is walking in with. This week's rollup lives on the Lock slide.
  const counts = useMemo(()=>{
    const c = {GREEN:0, YELLOW:0, RED:0, NA:0};
    state.kpis.forEach(k=>{
      const pc = k.priorColor;
      if(pc==='GREEN'||pc==='YELLOW'||pc==='RED') c[pc]++;
      else c.NA++;
    });
    return c;
  }, [state.kpis]);

  const commitStats = useMemo(()=>{
    let total=0, done=0, missed=0, open=0;
    state.kpis.forEach(k=>{
      (k.priorCommitments||[]).forEach(c=>{
        total++;
        if(c.status==='done') done++;
        else if(c.status==='missed') missed++;
        else open++;
      });
    });
    return {total, done, missed, open};
  }, [state.kpis]);

  const priorWk = state.meta.priorWeekLabel || `WK${String(state.meta.week-1).padStart(2,'0')}`;

  return (
    <div className="view" data-screen-label="02 Where We Left It">
      <div>
        <p className="eyebrow"><span className="idx">02 /</span>Where We Left It · {priorWk} Close</p>
        <h1 style={{fontSize:'clamp(64px,7vw,104px)',lineHeight:.94,letterSpacing:'-0.035em',margin:'10px 0 6px',fontWeight:500,color:'var(--mdc-ink)'}}>
          The scoreboard <span style={{color:'var(--mdc-ink-faint)',fontStyle:'italic',fontWeight:400}}>we're walking in with.</span>
        </h1>
        <p style={{fontSize:20,color:'var(--mdc-ink-soft)',maxWidth:'68ch',lineHeight:1.4,margin:'4px 0 0'}}>
          Frozen from last Thursday's close. Nothing on this slide changes during the meeting — this is the contract we're here to move.
        </p>
      </div>

      <div className="ov-counts">
        <div className="ov-stat GREEN"><div className="bar"></div><div className="n">{counts.GREEN}</div><div className="l">Green · Hold</div></div>
        <div className="ov-stat YELLOW"><div className="bar"></div><div className="n">{counts.YELLOW}</div><div className="l">Yellow · Resolve</div></div>
        <div className="ov-stat RED"><div className="bar"></div><div className="n">{counts.RED}</div><div className="l">Red · Move</div></div>
        <div className="ov-stat NA"><div className="bar"></div><div className="n">{counts.NA}</div><div className="l">No prior data</div></div>
      </div>

      {commitStats.total>0 && (
        <div className="ov-commits-line">
          <span className="ovcl-label">Plus · open from {priorWk}</span>
          <span className="ovcl-val"><b>{commitStats.total}</b> actions committed</span>
          <span className="ovcl-sep">→</span>
          {commitStats.done>0 && <span className="ovcl-done"><b>{commitStats.done}</b> done</span>}
          {commitStats.missed>0 && <span className="ovcl-missed"><b>{commitStats.missed}</b> missed</span>}
          {commitStats.open>0 && <span className="ovcl-open"><b>{commitStats.open}</b> still open</span>}
        </div>
      )}

      <div className="ov-contract">
        <span className="ovc-label">This meeting's job:</span>
        <span className="ovc-body">
          Hold the {counts.GREEN} green{counts.GREEN===1?'':'s'}
          {counts.YELLOW>0 && <>, resolve the {counts.YELLOW} yellow{counts.YELLOW===1?'':'s'}</>}
          {counts.RED>0 && <>, move at least one of the {counts.RED} red{counts.RED===1?'':'s'}.</>}
        </span>
      </div>

      <div className="ov-table">
        <div className="ov-row head">
          <span>#</span>
          <span>KPI · Owner</span>
          <span>{priorWk} reading</span>
          <span>Target</span>
          <span>{priorWk} call</span>
          <span></span>
        </div>
        {state.kpis.map((k,i)=>(
          <div className="ov-row" key={k.id} onClick={()=>onOpen(k.id)}>
            <span className="idx">{String(i+1).padStart(2,'0')}</span>
            <div>
              <div className="nm">{k.name}</div>
              <div className="ow">{k.owner}</div>
            </div>
            <div className="now">
              <span>{k.priorActual||'—'}</span>
            </div>
            <div className="ow">{k.target}</div>
            <div className="prior">
              <span className={`ctile ${k.priorColor||'NA'}`}>{k.priorColor||'—'}</span>
            </div>
            <span className="chev">›</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ─── KPI VIEW — HERO IS PRIOR vs CURRENT ───────────────────── */
function KpiView({kpi, state, idx, update, goTo}){
  const priorWk = state.meta.priorWeekLabel || `WK${String(state.meta.week-1).padStart(2,'0')}`;
  const thisWk  = `WK${String(state.meta.week).padStart(2,'0')}`;
  const trend = useMemo(()=>computeTrend(kpi.priorActual, kpi.actual, kpi.betterDirection), [kpi]);
  const filled = Boolean(kpi.committedAction && kpi.committedAction.trim());

  const setActual = (v)=>update(s=>{ const t=s.kpis.find(x=>x.id===kpi.id); t.actual=v; });
  const setColor  = (c)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    const prev = t.color;
    t.color = (t.color===c) ? '' : c;
    // If the new call matches the math, clear any stale override reason.
    const suggested = suggestColor(t.actual, t.target, t.betterDirection, t.thresholds);
    if(!t.color || t.color === suggested){
      t.overrideReason = '';
    }
    // Auto IDS sync
    if(t.color==='RED'){
      if(!s.idsQueue.find(x=>x.kpiId===t.id)) s.idsQueue.push({kpiId:t.id, question:'', addedAt:new Date().toISOString()});
    } else {
      s.idsQueue = s.idsQueue.filter(x=>x.kpiId!==t.id);
    }
  });
  const setOverrideReason = (v)=>update(s=>{ const t=s.kpis.find(x=>x.id===kpi.id); t.overrideReason=v; });
  const setNotes  = (v)=>update(s=>{ const t=s.kpis.find(x=>x.id===kpi.id); t.notes=v; });
  const [draftAction, setDraftAction] = useState('');
  const [draftOwner, setDraftOwner] = useState('');
  const [draftDue, setDraftDue] = useState('');
  const actionRef = useRef(null);
  const addCommit = ()=>{
    const text = draftAction.trim(); if(!text) return;
    // Default due date: next Monday in YYYY-MM-DD.
    const d = new Date();
    const daysUntilMon = (8 - d.getDay()) % 7 || 7; // always a future Monday
    d.setDate(d.getDate() + daysUntilMon);
    const defaultDue = d.toISOString().slice(0,10);
    update(s=>{
      const t=s.kpis.find(x=>x.id===kpi.id);
      if(!Array.isArray(t.commits)) t.commits = [];
      t.commits.push({
        id: Math.random().toString(36).slice(2),
        action: text,
        owner: draftOwner.trim() || t.owner,
        due: draftDue.trim() || defaultDue,
        done: false,
        createdAt: new Date().toISOString()
      });
    });
    setDraftAction(''); setDraftOwner(''); setDraftDue('');
    setTimeout(()=>actionRef.current?.focus(), 0);
  };
  const toggleDone = (cid)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    const c=(t.commits||[]).find(x=>x.id===cid);
    if(c) c.done = !c.done;
  });
  const removeCommit = (cid)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    t.commits = (t.commits||[]).filter(x=>x.id!==cid);
  });
  const editCommit = (cid, field, val)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    const c=(t.commits||[]).find(x=>x.id===cid);
    if(c) c[field] = val;
  });
  const setPcStatus = (i, val)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    t.priorCommitments[i].status = (t.priorCommitments[i].status===val) ? 'progress' : val;
  });
  const editPc = (i, field, val)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    t.priorCommitments[i][field] = val;
  });
  const removePc = (i)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    t.priorCommitments.splice(i,1);
  });
  const [draftPc, setDraftPc] = useState({owner:'', action:'', due:''});
  const pcOwnerRef = useRef(null);
  const addPc = ()=>{
    const a = draftPc.action.trim(); if(!a) return;
    update(s=>{
      const t=s.kpis.find(x=>x.id===kpi.id);
      if(!Array.isArray(t.priorCommitments)) t.priorCommitments=[];
      t.priorCommitments.push({
        owner: draftPc.owner.trim() || (state.meta && state.meta.owner) || 'JUAN',
        action: a,
        due: draftPc.due.trim() || '—',
        status: 'progress',
      });
    });
    setDraftPc({owner:'', action:'', due:''});
    setTimeout(()=>pcOwnerRef.current?.focus(), 0);
  };

  // ── Roster (list-shaped KPIs only) ──────────────────────────
  const isList = kpi.kind === 'list';
  const itemFields = kpi.itemFields || {primary:'Item', owner:'Owner', reason:'Reason'};
  const [draftItem, setDraftItem] = useState({primary:'', owner:'', reason:''});
  const primaryRef = useRef(null);
  const addItem = ()=>{
    const p = draftItem.primary.trim(); if(!p) return;
    update(s=>{
      const t=s.kpis.find(x=>x.id===kpi.id);
      if(!Array.isArray(t.items)) t.items=[];
      t.items.push({
        id: Math.random().toString(36).slice(2),
        primary: p,
        owner: draftItem.owner.trim() || t.owner,
        reason: draftItem.reason.trim()
      });
    });
    setDraftItem({primary:'', owner:'', reason:''});
    // Return focus to the first field so the user can add another item immediately.
    setTimeout(()=>primaryRef.current?.focus(), 0);
  };
  const removeItem = (iid)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    t.items = (t.items||[]).filter(x=>x.id!==iid);
  });
  const editItem = (iid, field, val)=>update(s=>{
    const t=s.kpis.find(x=>x.id===kpi.id);
    const it=(t.items||[]).find(x=>x.id===iid);
    if(it) it[field]=val;
  });

  const parts = kpi.name.split(' ');
  const lastWord = parts.pop();

  return (
    <div className="view kpi-view" data-color={kpi.color||''} data-prior-color={kpi.priorColor||''} data-screen-label={`${String(idx+3).padStart(2,'0')} ${kpi.name}`} data-om-validate="kpi">
      <div className="kpi-head">
        <div className="names">
          <p className="eyebrow">
            <span className="idx">{String(idx+1).padStart(2,'0')} /</span>
            KPI · Owner <b style={{color:'var(--mdc-ink)',fontWeight:600}}>{kpi.owner}</b>
          </p>
          <h2>{parts.join(' ')} <b>{lastWord}.</b></h2>
          <p className="sub">{kpi.subtitle}</p>
        </div>
        <div className="meta">
          <div><b>{kpi.betterDirection==='down' ? '↓ lower is better' : '↑ higher is better'}</b></div>
          <div>{thisWk} · {state.meta.date}</div>
        </div>
      </div>

      {/* PRIOR-WEEK COMMITMENTS — accountability check, full width, before this week's number */}
      {/* HERO — prior vs current numbers */}
      <div>
        <div className="kpi-hero">
          <div className="col prior" data-pc={kpi.priorColor||''}>
            <div className="col-lbl"><span>Prior</span><span className="week">{priorWk}</span></div>
            <div className={`num prior-val ${kpi.priorActual?'':'empty'}`}>{splitHeadline(kpi.priorActual).head || '—'}</div>
            {splitHeadline(kpi.priorActual).tail && <div className="num-context">{splitHeadline(kpi.priorActual).tail}</div>}
            <div className="col-foot">
              {kpi.priorColor ? <>Called <span className={`ctile ${kpi.priorColor}`} style={{marginLeft:6}}>{kpi.priorColor}</span></> : 'No prior reading'}
            </div>
          </div>
          <div className={`connector ${trend.kind||'flat'}`}>
            <div className="tgt-lbl">Target</div>
            <div className="tgt-val">{splitHeadline(kpi.target).head || kpi.target || '—'}</div>
            {splitHeadline(kpi.target).tail && <div className="tgt-sub">{splitHeadline(kpi.target).tail}</div>}
            <div className="tgt-rule"></div>
            <Arrow dir={trend.dir||'flat'} />
            <div className="delta">{trend.delta || (kpi.actual ? '—' : 'awaiting this wk')}</div>
            {trend.pct && trend.delta && trend.kind!=='flat' && (
              <div className="delta-sub">{trend.pct} vs prior</div>
            )}
          </div>
          <div className="col current">
            <div className="col-lbl"><span>This Week</span><span className="week">{thisWk}</span></div>
            <div className={`num ${kpi.actual?'':'empty'}`}>
              <input
                className="num-input"
                value={kpi.actual||''}
                placeholder={isList ? `Count…` : 'Enter…'}
                onChange={e=>setActual(e.target.value)}
                onBlur={e=>{
                  const formatted = formatToMatchRef(e.target.value, kpi.target || kpi.priorActual);
                  if(formatted !== e.target.value) setActual(formatted);
                  // Auto-suggest color only if the user hasn't already called one.
                  if(!kpi.color){
                    const s = suggestColor(formatted, kpi.target, kpi.betterDirection, kpi.thresholds);
                    if(s) setColor(s);
                  }
                }}
                aria-label={`${kpi.name} current value`}
              />
            </div>
            {isList && (kpi.items||[]).length>0 && <div className="num-context">{(kpi.items||[]).length} item{(kpi.items||[]).length===1?'':'s'} listed below</div>}
            <div className="col-foot">
              {kpi.color ? <>Called <span className={`ctile ${kpi.color}`} style={{marginLeft:6}}>{kpi.color}</span></> : <span style={{color:'var(--mdc-ink-faint)'}}>Not yet called</span>}
            </div>
          </div>
        </div>

        {/* Color call — big segmented buttons */}
        {(() => {
          const suggested = suggestColor(kpi.actual, kpi.target, kpi.betterDirection, kpi.thresholds);
          const mismatch = suggested && kpi.color && kpi.color !== suggested;
          return (
            <>
              <div className="color-call" style={{marginTop:16}}>
                {state.protocol.map(p=>(
                  <button
                    key={p.key}
                    className="color-btn"
                    data-k={p.key}
                    data-on={kpi.color===p.key}
                    data-suggested={suggested===p.key && kpi.color!==p.key}
                    onClick={()=>setColor(p.key)}
                    title={suggested===p.key ? `Suggested · ${p.action}` : p.action}
                  >
                    <span>{p.label}</span>
                    <span className="hint">{p.cue}</span>
                    {suggested===p.key && kpi.color!==p.key && <span className="sugg-dot" aria-hidden>math →</span>}
                  </button>
                ))}
              </div>
              {/* Threshold strip — horizontal, sits directly under the color call it explains */}
              <div className="thresh-strip" aria-label="Thresholds">
                {kpi.thresholds.map((t,i)=>(
                  <div className={`thresh-pill ${t.color}`} key={i} data-active={kpi.color===t.color}>
                    <span className={`chip ${t.color}`}>{t.color}</span>
                    <span className="text">{t.text}</span>
                  </div>
                ))}
              </div>
              {mismatch && (
                <div className="color-mismatch" data-state={kpi.overrideReason ? 'noted' : 'pending'}>
                  <div className="cm-head">
                    <span className="cm-icon" aria-hidden>{kpi.overrideReason ? '✓' : '!'}</span>
                    <div className="cm-body">
                      <div className="cm-title">
                        {kpi.overrideReason
                          ? <>Override <b>noted</b> · math says <b>{suggested}</b>, called <b>{kpi.color}</b></>
                          : <>Math says <b>{suggested}</b>, you called <b>{kpi.color}</b>. <b>Why?</b></>
                        }
                      </div>
                      <div className="cm-sub">
                        {kpi.overrideReason
                          ? 'Recorded with this week\'s reading — will export with the scorecard.'
                          : suggested==='RED'
                            ? 'A one-line reason is required. Non-red overrides skip the IDS queue.'
                            : 'A one-line reason is required before this call is locked in.'}
                      </div>
                    </div>
                  </div>
                  <input
                    className="cm-input"
                    value={kpi.overrideReason||''}
                    onChange={e=>setOverrideReason(e.target.value)}
                    placeholder={suggested==='RED'
                      ? 'e.g. Known recovery in motion — collected $8k Monday, reclassifying as YELLOW'
                      : 'e.g. Trend is drifting — calling YELLOW as early warning'}
                    aria-label="Override reason"
                  />
                </div>
              )}
              {/* YELLOW DRIFT GATE — if yellow + trending worse (or flat),
                  the owner must commit an action. Confirmation if trending better. */}
              {kpi.color==='YELLOW' && (() => {
                const tr = computeTrend(kpi.priorActual, kpi.actual, kpi.betterDirection);
                // kind is 'good' | 'bad' | 'flat' | null
                const hasCommit = (kpi.commits||[]).length > 0;
                if(tr.kind === 'good'){
                  return (
                    <div className="yellow-drift" data-state="ok">
                      <span className="yd-icon" aria-hidden>↗</span>
                      <div className="yd-body">
                        <div className="yd-title">Yellow, but <b>trending in</b> ({tr.delta}).</div>
                        <div className="yd-sub">No action required this week — momentum is toward target.</div>
                      </div>
                    </div>
                  );
                }
                if(tr.kind === 'bad' || tr.kind === 'flat'){
                  return (
                    <div className="yellow-drift" data-state={hasCommit ? 'satisfied' : 'pending'}>
                      <span className="yd-icon" aria-hidden>{hasCommit ? '✓' : '!'}</span>
                      <div className="yd-body">
                        <div className="yd-title">
                          {hasCommit
                            ? <>Action committed · drift acknowledged.</>
                            : tr.kind==='flat'
                              ? <>Yellow · <b>flat trend</b>. Commit an action to pull it back toward green.</>
                              : <>Yellow · <b>drifting toward red</b> ({tr.delta}). Commit an action to pull it back.</>}
                        </div>
                        <div className="yd-sub">
                          {hasCommit
                            ? 'The room expects this action by next WBR.'
                            : 'Rule: yellow trending worse requires an owner action. Add one in the Actions Committed zone below.'}
                        </div>
                      </div>
                    </div>
                  );
                }
                return null;
              })()}
            </>
          );
        })()}
      </div>

      {/* ROSTER — list-shaped KPIs only */}
      {isList && (() => {
        const itemCount = (kpi.items||[]).length;
        // ODD is list-heavy by nature — never collapse, always show the roster.
        const alwaysShowRoster = kpi.id === 'odd';
        const collapsible = !alwaysShowRoster && kpi.color==='GREEN' && itemCount===0;
        // Local expansion state lives on the kpi row itself so switches persist across renders.
        const expanded = !!kpi._rosterExpanded;
        const toggleExpand = () => update(s=>{
          const t = s.kpis.find(x=>x.id===kpi.id);
          t._rosterExpanded = !t._rosterExpanded;
        });

        if(collapsible && !expanded){
          return (
            <div className="roster-collapsed">
              <span className="rc-icon" aria-hidden>✓</span>
              <div className="rc-body">
                <span className="rc-title">No {(kpi.itemLabel||'items').toLowerCase()}s this week.</span>
                <span className="rc-sub">Nothing to review — passing to next block.</span>
              </div>
              <button className="rc-add" onClick={toggleExpand}>+ Add if one comes up</button>
            </div>
          );
        }
        return (
        <div className="roster" data-count={itemCount}>
          <div className="roster-head">
            <div>
              <div className="roster-lbl">Driving this call · {itemCount} {kpi.itemLabel || 'item'}{itemCount===1?'':'s'}</div>
              <div className="roster-sub">The list behind the number. Each row = one item the room should know about.</div>
            </div>
            <div className="roster-count">{itemCount}</div>
          </div>

          {itemCount>0 && (
            <div className="roster-table">
              <div className="roster-th">
                <span>{itemFields.primary}</span>
                <span>{itemFields.owner}</span>
                <span>{itemFields.reason}</span>
                <span></span>
              </div>
              {(kpi.items||[]).map((it, i)=>(
                <div className="roster-tr" key={it.id}>
                  <span className="r-idx">{String(i+1).padStart(2,'0')}</span>
                  <input className="r-field r-primary" value={it.primary} onChange={e=>editItem(it.id,'primary',e.target.value)} />
                  <input className="r-field r-owner" value={it.owner} onChange={e=>editItem(it.id,'owner',e.target.value)} />
                  <input className="r-field r-reason" value={it.reason} onChange={e=>editItem(it.id,'reason',e.target.value)} placeholder="Why this matters…" />
                  <button className="x-btn" onClick={()=>removeItem(it.id)} aria-label="remove">×</button>
                </div>
              ))}
            </div>
          )}

          <div className="roster-add">
            <input
              ref={primaryRef}
              className="r-field"
              value={draftItem.primary}
              onChange={e=>setDraftItem({...draftItem, primary:e.target.value})}
              onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); addItem(); }}}
              placeholder={`+ Add ${(kpi.itemLabel||'item').toLowerCase()}… (${itemFields.primary.toLowerCase()})`}
            />
            <input
              className="r-field r-owner"
              value={draftItem.owner}
              onChange={e=>setDraftItem({...draftItem, owner:e.target.value})}
              onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); addItem(); }}}
              placeholder={itemFields.owner}
            />
            <input
              className="r-field r-reason"
              value={draftItem.reason}
              onChange={e=>setDraftItem({...draftItem, reason:e.target.value})}
              onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); addItem(); }}}
              placeholder={itemFields.reason}
            />
            <button className="btn btn-primary" onClick={addItem}>Add</button>
          </div>
        </div>
        );
      })()}

      {/* Body: prior commits (left) + actions committed (right) — side by side, equal weight */}
      <div className="kpi-body">
        {(() => {
          const pcs = kpi.priorCommitments || [];
          const counts = pcs.reduce((acc, pc) => {
            const s = pc.status === 'completed' ? 'done' : pc.status === 'missed' ? 'miss' : 'wip';
            acc[s]++; acc.total++; return acc;
          }, {done:0, miss:0, wip:0, total:0});
          return (
            <div className="prior-zone" data-empty={pcs.length===0 ? 'true' : 'false'}>
              <div className="pz-label">
                <span>Prior Commitments · {priorWk} ({pcs.length})</span>
                {pcs.length > 0 ? (
                  <span className="pz-tally">
                    <span className="pcb-pill done"><b>{counts.done}</b> done</span>
                    <span className="pcb-pill wip"><b>{counts.wip}</b> WIP</span>
                    <span className="pcb-pill miss"><b>{counts.miss}</b> miss</span>
                  </span>
                ) : (
                  <span className="hint">None carried in.</span>
                )}
              </div>

              <div className="pc-rows">
                {pcs.map((pc, i) => (
                  <div className="pc-row" key={i} data-status={pc.status||'progress'}>
                    <div style={{flex:1, minWidth:0}}>
                      <input className="pc-edit pc-owner-edit" value={pc.owner||''}
                        onChange={e=>editPc(i,'owner',e.target.value.toUpperCase())}
                        placeholder="OWNER" />
                      <input className="pc-edit pc-action-edit" value={pc.action||''}
                        onChange={e=>editPc(i,'action',e.target.value)}
                        placeholder="What was committed…" />
                      <input className="pc-edit pc-due-edit" value={pc.due||''}
                        onChange={e=>editPc(i,'due',e.target.value)}
                        placeholder="Due WK__ ___" />
                    </div>
                    <div className="pc-controls">
                      <div className="status-seg">
                        <button data-s="progress"  data-on={pc.status==='progress'||!pc.status} onClick={()=>setPcStatus(i,'progress')}>⏳ WIP</button>
                        <button data-s="completed" data-on={pc.status==='completed'} onClick={()=>setPcStatus(i,'completed')}>✓ Done</button>
                        <button data-s="missed"    data-on={pc.status==='missed'}    onClick={()=>setPcStatus(i,'missed')}>✗ Miss</button>
                      </div>
                      <button className="pc-del" title="Remove this commitment" onClick={()=>removePc(i)}>×</button>
                    </div>
                  </div>
                ))}
              </div>

              <div className="pc-add">
                <div className="pc-add-fields">
                  <input ref={pcOwnerRef} className="pc-edit pc-owner-edit" value={draftPc.owner}
                    onChange={e=>setDraftPc({...draftPc, owner:e.target.value.toUpperCase()})}
                    onKeyDown={e=>e.key==='Enter'&&addPc()}
                    placeholder="OWNER" />
                  <input className="pc-edit pc-action-edit" value={draftPc.action}
                    onChange={e=>setDraftPc({...draftPc, action:e.target.value})}
                    onKeyDown={e=>e.key==='Enter'&&addPc()}
                    placeholder={`+ Add a ${priorWk} commitment you forgot to log…`} />
                  <input className="pc-edit pc-due-edit" value={draftPc.due}
                    onChange={e=>setDraftPc({...draftPc, due:e.target.value})}
                    onKeyDown={e=>e.key==='Enter'&&addPc()}
                    placeholder="Due WK__ ___" />
                </div>
                <button className="btn btn-primary pc-add-btn" onClick={addPc} disabled={!draftPc.action.trim()}>+ Add</button>
              </div>
            </div>
          );
        })()}

        <div className="action-zone" data-filled={(kpi.commits||[]).length>0}>
          <div className="az-label">
            <span>Actions Committed ({(kpi.commits||[]).length})</span>
            <span className="hint">Add each bite-sized action. Click ✓ when done.</span>
          </div>

          <ul style={{listStyle:'none',padding:0,margin:'4px 0 0',display:'flex',flexDirection:'column',gap:10}}>
            {(kpi.commits||[]).map(c=>(
              <li key={c.id} style={{
                display:'grid',gridTemplateColumns:'34px 1fr auto',gap:14,alignItems:'start',
                padding:'14px 18px',background:'var(--mdc-white)',border:'1px solid var(--mdc-line)',borderRadius:'var(--mdc-radius-md)'
              }}>
                <button onClick={()=>toggleDone(c.id)}
                  aria-label={c.done?'mark open':'mark done'}
                  style={{
                    width:28,height:28,borderRadius:'50%',marginTop:4,
                    border:c.done?'none':'2px solid var(--mdc-line-strong)',
                    background:c.done?'var(--wbr-green)':'transparent',
                    color:'#fff',fontSize:15,cursor:'pointer',lineHeight:1,padding:0
                  }}>{c.done?'✓':''}</button>
                <div style={{minWidth:0}}>
                  <input
                    value={c.action}
                    onChange={e=>editCommit(c.id,'action',e.target.value)}
                    aria-label="action"
                    style={{
                      width:'100%',display:'block',
                      background:'transparent',border:'none',outline:'none',padding:0,
                      font:'inherit',fontFamily:'var(--mdc-font-sans)',
                      fontSize:22,fontWeight:500,lineHeight:1.3,letterSpacing:'-0.005em',
                      color:'var(--mdc-ink)',
                      textDecoration:c.done?'line-through':'none',
                      textDecorationColor:'var(--wbr-green)',textDecorationThickness:2
                    }}
                  />
                  <div style={{display:'flex',alignItems:'center',gap:6,marginTop:6,fontFamily:'var(--mdc-font-mono)',fontSize:13,letterSpacing:'.08em',textTransform:'uppercase',color:'var(--mdc-ink-soft)'}}>
                    <input
                      value={c.owner||''}
                      onChange={e=>editCommit(c.id,'owner',e.target.value.toUpperCase())}
                      aria-label="owner"
                      style={{
                        background:'transparent',border:'none',outline:'none',padding:0,
                        font:'inherit',fontFamily:'var(--mdc-font-mono)',fontSize:13,
                        letterSpacing:'.08em',textTransform:'uppercase',
                        color:'var(--mdc-blue)',fontWeight:600,
                        width:`${Math.max((c.owner||'').length, 4)}ch`
                      }}
                    />
                    <span>· Due</span>
                    <DuePicker
                      value={c.due||''}
                      onChange={v=>editCommit(c.id,'due',v)}
                      compact
                    />
                  </div>
                </div>
                <button onClick={()=>removeCommit(c.id)} className="x-btn" aria-label="remove" style={{fontSize:22}}>×</button>
              </li>
            ))}
          </ul>

          <div style={{marginTop:14,paddingTop:14,borderTop:'1px dashed var(--mdc-line-strong)',display:'flex',flexDirection:'column',gap:10}}>
            <textarea
              ref={actionRef}
              value={draftAction}
              onChange={e=>setDraftAction(e.target.value)}
              onKeyDown={e=>{ if(e.key==='Enter' && (e.metaKey||e.ctrlKey)){ e.preventDefault(); addCommit(); }}}
              placeholder={kpi.color==='RED' ? 'Required — one next step to move this to yellow.' : 'Type an action, then press + Add (or Cmd+Enter).'}
              style={{background:'transparent',border:'none',color:'var(--mdc-ink)',font:'inherit',resize:'none',outline:'none',fontFamily:'var(--mdc-font-sans)',fontWeight:500,fontSize:24,lineHeight:1.3,minHeight:60,letterSpacing:'-0.01em',width:'100%'}}
            />
            <div style={{display:'flex',gap:12,alignItems:'center',flexWrap:'wrap'}}>
              <label style={{fontFamily:'var(--mdc-font-mono)',fontSize:14,color:'var(--mdc-ink-soft)',letterSpacing:'.06em',display:'flex',alignItems:'center',gap:8}}>
                Owner <input value={draftOwner} onChange={e=>setDraftOwner(e.target.value)} placeholder={kpi.owner}
                  style={{background:'var(--mdc-white)',border:'1px solid var(--mdc-line)',color:'var(--mdc-ink)',borderRadius:'var(--mdc-radius-sm)',padding:'8px 12px',font:'inherit',fontFamily:'inherit',fontSize:14,outline:'none',width:140}}/>
              </label>
              <label style={{fontFamily:'var(--mdc-font-mono)',fontSize:14,color:'var(--mdc-ink-soft)',letterSpacing:'.06em',display:'flex',alignItems:'center',gap:8}}>
                Due <DuePicker value={draftDue} onChange={setDraftDue} onSubmit={addCommit} />
              </label>
              <button className="btn btn-primary" onClick={addCommit} style={{marginLeft:'auto'}}>+ Add action</button>
            </div>
          </div>
        </div>

      </div>
    </div>
  );
}

/* ─── Missing KPIs view ──────────────────────────────────────── */
function Missing({state, update}){
  const [draftText, setDraftText] = useState('');
  const [draftOwner, setDraftOwner] = useState('');
  const draftRef = useRef(null);
  const add = ()=>{
    const v = draftText.trim(); if(!v) return;
    const o = draftOwner.trim().toUpperCase() || (state.meta && state.meta.owner) || 'JUAN';
    update(s=>{ s.missing.captured.push({id:Math.random().toString(36).slice(2), text:v, owner:o}); });
    setDraftText(''); setDraftOwner('');
    setTimeout(()=>draftRef.current?.focus(), 0);
  };
  const editItem = (id, field, val)=>update(s=>{
    const t = s.missing.captured.find(x=>x.id===id);
    if(t) t[field] = val;
  });
  return (
    <div className="view" data-screen-label={`${String(state.kpis.length+3).padStart(2,'0')} Missing KPIs`}>
      <div className="missing-lead">
        <div>
          <span className="badge">? Missing KPIs</span>
          <h2>What <b>aren't</b> we measuring?</h2>
          <p className="sub">The scorecard is closed. This is the last moment to flag a blind spot before we move on — new KPIs go through the Five Gates before next week.</p>
        </div>
        <div>
          <ul className="missing-prompts">
            {state.missing.prompts.map((p,i)=><li key={i}>{p}</li>)}
          </ul>
        </div>
      </div>
      <div className="missing-captured">
        <h4>Captured this week <span className="cap-hint">Owner validates before it enters the scorecard.</span></h4>
        <div className="captured-row">
          <input ref={draftRef} value={draftText} onChange={e=>setDraftText(e.target.value)}
            placeholder="Type a candidate KPI or blind spot…"
            onKeyDown={e=>e.key==='Enter'&&add()} />
          <input className="cap-owner" value={draftOwner}
            onChange={e=>setDraftOwner(e.target.value.toUpperCase())}
            onKeyDown={e=>e.key==='Enter'&&add()}
            placeholder="VALIDATOR" maxLength={8} />
          <button className="btn btn-primary" onClick={add}>+ Capture</button>
        </div>
        <div className="captured-list">
          {state.missing.captured.length===0 && <div className="commits-empty">Nothing captured yet — a clean Missing KPIs run is a good thing.</div>}
          {state.missing.captured.map((c,i)=>(
            <div className="item" key={c.id}>
              <span className="idx">{String(i+1).padStart(2,'0')}</span>
              <input className="cap-edit-text" value={c.text}
                onChange={e=>editItem(c.id,'text',e.target.value)}
                placeholder="Candidate KPI…" />
              <div className="cap-owner-wrap">
                <span className="cap-owner-lbl">Validator</span>
                <input className="cap-edit-owner" value={c.owner||''}
                  onChange={e=>editItem(c.id,'owner',e.target.value.toUpperCase())}
                  placeholder="OWNER" maxLength={8} />
              </div>
              <button className="x-btn" onClick={()=>update(s=>{s.missing.captured=s.missing.captured.filter(x=>x.id!==c.id);})}>×</button>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ─── IDS Queue ──────────────────────────────────────────────── */
function IDS({state, update, goTo}){
  const reds = state.kpis.filter(k=>k.color==='RED');
  const queue = state.idsQueue;
  const slots = [0,1,2];
  const setQ = (kpiId, v)=>update(s=>{ const q=s.idsQueue.find(x=>x.kpiId===kpiId); if(q)q.question=v; });
  const remove = (kpiId)=>update(s=>{
    s.idsQueue = s.idsQueue.filter(x=>x.kpiId!==kpiId);
    const t = s.kpis.find(x=>x.id===kpiId); if(t && t.color==='RED') t.color='';
  });

  return (
    <div className="view" data-screen-label={`${String(state.kpis.length+4).padStart(2,'0')} IDS Queue`}>
      <div className="ids-head">
        <div>
          <p className="eyebrow"><span className="idx">{String(state.kpis.length+4).padStart(2,'0')} /</span>Block 2 · IDS</p>
          <h2>Identify <span style={{color:'var(--mdc-blue)'}}>·</span> Discuss <span style={{color:'var(--mdc-blue)'}}>·</span> <b>Solve.</b></h2>
          <p className="sub">One RED KPI per slot. Name the real issue in one sentence — the one we actually need to solve, not the symptom.</p>
        </div>
        <div className="count">REDS in queue <b>{reds.length}/3</b></div>
      </div>
      <div className="ids-grid">
        {slots.map(slotIdx=>{
          const item = queue[slotIdx];
          const kpi = item ? state.kpis.find(k=>k.id===item.kpiId) : null;
          return (
            <div key={slotIdx} className="ids-slot" data-filled={!!kpi}>
              <span className="num">
                <span className="slot-label">ISSUE · {String(slotIdx+1).padStart(2,'0')}</span>
                {kpi && <span className="qlink">↓ Q{String(slotIdx+1).padStart(2,'0')}</span>}
              </span>
              {!kpi && <div className="empty-msg">Open when a KPI is called RED on Block 1.</div>}
              {kpi && (
                <>
                  <button className="remove" onClick={()=>remove(kpi.id)} aria-label="remove">×</button>
                  <div className="kpi-nm">{kpi.name}</div>
                  <div className="kpi-owner">{kpi.owner} · {kpi.actual||'—'} vs {kpi.target}</div>
                  <textarea className="question" value={item.question||''} onChange={e=>setQ(kpi.id, e.target.value)}
                    placeholder={`What question do we need to answer about ${kpi.name}?\n\ne.g. "Why did ${kpi.owner||'we'} miss ${kpi.target||'plan'}?" or "What's blocking us from green next week?"`} />
                  <button className="open-kpi" onClick={()=>goTo(kpi.id)}>open kpi →</button>
                </>
              )}
            </div>
          );
        })}
      </div>
      <div className="ids-questions">
        <div className="ids-questions-head">
          <span className="tag">Questions on the table</span>
          <span className="hint">These are what we're here to answer. Read aloud before Discuss.</span>
        </div>
        {queue.filter(q=>q && q.question && q.question.trim()).length === 0 ? (
          <div className="ids-questions-empty">
            Frame each issue above as a question. It will appear here, in full, as the working agenda for Discuss &amp; Solve.
          </div>
        ) : (
          <div className="ids-questions-list">
            {queue.map((item,idx)=>{
              const kpi = item ? state.kpis.find(k=>k.id===item.kpiId) : null;
              if(!kpi || !item.question || !item.question.trim()) return null;
              return (
                <div className="ids-question-row" key={item.kpiId}>
                  <span className="qnum">Q{String(idx+1).padStart(2,'0')}</span>
                  <blockquote className="qtext">
                    {item.question.trim().replace(/\?*\s*$/, '')}<span className="qmark">?</span>
                  </blockquote>
                  <span className="qfrom">
                    <span className="qfrom-lbl">Issue {String(idx+1).padStart(2,'0')} ↑</span>
                    <span className="qfrom-kpi">{kpi.name}</span>
                    <span className="qfrom-owner">{kpi.owner||'—'}</span>
                  </span>
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

/* ─── Commitments · Final recap ──────────────────────────────── */
function Commitments({state, goTo, update}){
  const [groupBy, setGroupBy] = useState('owner'); // 'owner' | 'kpi' | 'due'

  // Format due like DuePicker does
  const today = new Date(); today.setHours(0,0,0,0);
  const wkday = ['SUN','MON','TUE','WED','THU','FRI','SAT'];
  const fmtDue = (due)=>{
    if(!due) return {label:'No date', order:99999, overdue:false};
    const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(due);
    if(!m) return {label:due, order:88888, overdue:false};
    const d = new Date(+m[1], +m[2]-1, +m[3]);
    const diff = Math.round((d-today)/86400000);
    const md = `${d.getMonth()+1}/${d.getDate()}`;
    const w = wkday[d.getDay()];
    let label;
    if(diff<0) label = `${w} ${md}`;
    else if(diff===0) label = `Today · ${md}`;
    else if(diff===1) label = `Tmrw · ${md}`;
    else if(diff<=7) label = `${w} ${md}`;
    else label = `${w} ${md}`;
    return {label, order:diff, overdue:diff<0};
  };

  // Flatten: one entry per commit
  const flat = [];
  state.kpis.forEach((k, idx)=>{
    (k.commits||[]).forEach(c=>{
      flat.push({
        id: c.id,
        kpi: k,
        kpiIdx: idx,
        action: c.action,
        owner: (c.owner||'').trim() || '—',
        due: c.due,
        dueFmt: fmtDue(c.due),
        done: !!c.done,
      });
    });
  });

  const total = flat.length;
  const doneCount = flat.filter(x=>x.done).length;
  const openCount = total - doneCount;
  const owners = Array.from(new Set(flat.map(x=>x.owner))).sort();
  const kpisTouched = Array.from(new Set(flat.map(x=>x.kpi.id))).length;
  const overdueCount = flat.filter(x=>!x.done && x.dueFmt.overdue).length;

  // Group
  const groups = [];
  if(groupBy==='owner'){
    const map = new Map();
    flat.forEach(x=>{
      if(!map.has(x.owner)) map.set(x.owner, []);
      map.get(x.owner).push(x);
    });
    Array.from(map.keys()).sort().forEach(owner=>{
      const items = map.get(owner).sort((a,b)=>a.dueFmt.order-b.dueFmt.order);
      groups.push({key:owner, label:owner, sub:`${items.length} action${items.length===1?'':'s'}`, items});
    });
  } else if(groupBy==='kpi'){
    const map = new Map();
    flat.forEach(x=>{
      const key = x.kpi.id;
      if(!map.has(key)) map.set(key, {kpi:x.kpi, kpiIdx:x.kpiIdx, items:[]});
      map.get(key).items.push(x);
    });
    // keep KPI order
    state.kpis.forEach(k=>{
      if(!map.has(k.id)) return;
      const g = map.get(k.id);
      groups.push({key:k.id, label:k.name, sub:`KPI ${String(g.kpiIdx+1).padStart(2,'0')} · ${g.items.length} action${g.items.length===1?'':'s'}`, items:g.items, kpiId:k.id, colorChip:k.color});
    });
  } else {
    // due
    const sorted = [...flat].sort((a,b)=>a.dueFmt.order-b.dueFmt.order);
    const buckets = new Map();
    sorted.forEach(x=>{
      const key = x.dueFmt.label;
      if(!buckets.has(key)) buckets.set(key, {order:x.dueFmt.order, items:[]});
      buckets.get(key).items.push(x);
    });
    Array.from(buckets.entries())
      .sort((a,b)=>a[1].order-b[1].order)
      .forEach(([label, g])=>{
        groups.push({key:label, label, sub:`${g.items.length} action${g.items.length===1?'':'s'}`, items:g.items, isDue:true});
      });
  }

  const toggleDone = (kpiId, commitId)=>update(s=>{
    const k = s.kpis.find(x=>x.id===kpiId);
    if(!k) return;
    const c = (k.commits||[]).find(x=>x.id===commitId);
    if(c) c.done = !c.done;
  });

  const thisWk = `WK${String(state.meta.week).padStart(2,'0')}`;
  const nextWk = `WK${String(state.meta.week+1).padStart(2,'0')}`;

  return (
    <div className="view" data-screen-label={`${String(state.kpis.length+5).padStart(2,'0')} Commitments`}>
      <div className="commits-head">
        <div>
          <p className="eyebrow"><span className="idx">{String(state.kpis.length+5).padStart(2,'0')} /</span>Block 3 · Handoff</p>
          <h2>Who owns <b>what</b>, by <b>when.</b></h2>
          <p className="sub">Every action committed during this walk-through. Read aloud, confirm out loud, then lock.</p>
        </div>
        <div className="commits-metrics">
          <div className="cm-box cm-primary">
            <div className="cm-num">{total}</div>
            <div className="cm-lbl">commitments<br/>logged</div>
          </div>
          <div className="cm-row">
            <div className="cm-stat"><b>{owners.length}</b><span>owner{owners.length===1?'':'s'}</span></div>
            <div className="cm-stat"><b>{kpisTouched}</b><span>KPI{kpisTouched===1?'':'s'}</span></div>
            <div className="cm-stat"><b>{openCount}</b><span>open</span></div>
            {doneCount>0 && <div className="cm-stat cm-done"><b>{doneCount}</b><span>done</span></div>}
            {overdueCount>0 && <div className="cm-stat cm-overdue"><b>{overdueCount}</b><span>overdue</span></div>}
          </div>
        </div>
      </div>

      {total===0 ? (
        <div className="commits-empty">
          <div className="ce-mono">NOTHING COMMITTED · {thisWk}</div>
          <div className="ce-hd">No actions logged yet.</div>
          <div className="ce-sub">Walk back through each KPI and add at least one action wherever there's a red flag or a yellow drift. Handoff only works if it's written down.</div>
        </div>
      ) : (
        <>
          <div className="commits-toolbar">
            <span className="ct-label">Group by</span>
            <div className="ct-seg">
              {['owner','kpi','due'].map(g=>(
                <button key={g}
                  className={`ct-seg-btn ${groupBy===g?'on':''}`}
                  onClick={()=>setGroupBy(g)}>
                  {g==='owner'?'Owner':g==='kpi'?'KPI':'Due date'}
                </button>
              ))}
            </div>
            <span className="ct-hint">Due dates target <b>{nextWk}</b> WBR unless noted.</span>
          </div>

          <div className="commits-groups">
            {groups.map((g,gi)=>(
              <section key={g.key} className="cg">
                <header className="cg-head">
                  {groupBy==='kpi' && <div className="cg-kpi-chip" data-color={(g.colorChip||'').toLowerCase()}>{g.colorChip||'—'}</div>}
                  {groupBy==='due' && <div className="cg-due-chip">→</div>}
                  <div className="cg-title-wrap">
                    <div className="cg-title">{g.label}</div>
                    <div className="cg-sub">{g.sub}</div>
                  </div>
                  {groupBy==='kpi' && (
                    <button className="cg-jump" onClick={()=>goTo(g.kpiId)}>Open KPI →</button>
                  )}
                </header>
                <ol className="cg-list">
                  {g.items.map((x,xi)=>(
                    <li key={x.id} className="cg-item" data-done={x.done} data-overdue={!x.done && x.dueFmt.overdue}>
                      <button
                        className="cg-check"
                        aria-label={x.done?'mark open':'mark done'}
                        onClick={()=>toggleDone(x.kpi.id, x.id)}>
                        {x.done?'✓':''}
                      </button>
                      <div className="cg-num">{String(xi+1).padStart(2,'0')}</div>
                      <div className="cg-body">
                        <div className="cg-action">{x.action}</div>
                        <div className="cg-meta">
                          {groupBy!=='owner' && <>
                            <span className="cg-meta-owner">{x.owner}</span>
                            <span className="cg-meta-dot">·</span>
                          </>}
                          {groupBy!=='kpi' && <>
                            <button className="cg-meta-kpi" onClick={(e)=>{e.stopPropagation(); goTo(x.kpi.id);}} title="Open KPI">
                              KPI {String(x.kpiIdx+1).padStart(2,'0')} · {x.kpi.name}
                            </button>
                          </>}
                          {groupBy==='kpi' && <span className="cg-meta-kpi-static">from this KPI</span>}
                        </div>
                      </div>
                      <div className={`cg-due ${x.dueFmt.overdue?'cg-due-red':''}`}>
                        <span className="cg-due-arrow">→</span>
                        {x.dueFmt.label}
                      </div>
                    </li>
                  ))}
                </ol>
              </section>
            ))}
          </div>

          <div className="commits-footer">
            <div className="cf-mono">READ-ALOUD READY · {total} COMMITMENT{total===1?'':'S'} · {owners.length} OWNER{owners.length===1?'':'S'}</div>
            <div className="cf-line">
              <b>The handoff:</b> each owner leaves the room knowing exactly what they said they'd do. Next week, this list becomes the first thing you check.
            </div>
          </div>
        </>
      )}
    </div>
  );
}

/* ─── Lock ───────────────────────────────────────────────────── */
function Lock({state}){
  return (
    <div className="view lock" data-screen-label={`${String(state.kpis.length+6).padStart(2,'0')} Lock`}>
      <p className="eyebrow-c">Lock the scorecard · Week {String(state.meta.week).padStart(2,'0')}</p>
      <h1>
        <span className="ln">Numbers <b>locked.</b></span>
        <span className="ln">Owners <b>named.</b></span>
        <span className="ln">Clocks <b>running.</b></span>
      </h1>
      <div className="q">Same time <b>next Friday?</b></div>
      <div className="caption">
        <b>Meeting complete.</b> Export to lock this week's numbers · The scorecard becomes WK{String(state.meta.week+1).padStart(2,'0')}'s prior column at <b>export time.</b>
      </div>
    </div>
  );
}

/* ─── Root app ───────────────────────────────────────────────── */
function App(){
  const [state, update, setState] = useWBRState();
  const [active, setActive] = useState(VIEW_COVER);
  const [pushToast, toastsNode] = useToasts();
  const [clock, setClock] = useState('');
  const presenterRef = useRef(null);

  // Clock
  useEffect(()=>{
    const tick = ()=>{
      const d = new Date();
      const h = d.getHours().toString().padStart(2,'0');
      const m = d.getMinutes().toString().padStart(2,'0');
      setClock(`${h}:${m}`);
    };
    tick();
    const id = setInterval(tick, 30000);
    return ()=>clearInterval(id);
  },[]);

  const goTo = useCallback((id)=>setActive(id), []);

  // Tabs with titles for keyboard nav + Next button
  const tabsFull = useMemo(()=>{
    const kn = state.kpis.length;
    return [
      {id:VIEW_COVER,    title:'Cover'},
      {id:VIEW_PURPOSE,  title:'Purpose'},
      {id:VIEW_OVERVIEW, title:'Scorecard'},
      ...state.kpis.map(k=>({id:k.id, title:k.name})),
      {id:VIEW_MISSING, title:'Missing KPIs'},
      {id:VIEW_IDS,     title:'IDS Queue'},
      {id:VIEW_COMMITS, title:'Commitments'},
      {id:VIEW_LOCK,    title:'Lock'},
    ];
  }, [state.kpis]);
  const tabIds = useMemo(()=>[
    VIEW_COVER, VIEW_PURPOSE, VIEW_OVERVIEW,
    ...state.kpis.map(k=>k.id),
    VIEW_MISSING, VIEW_IDS, VIEW_COMMITS, VIEW_LOCK
  ], [state.kpis]);

  // Keyboard
  useEffect(()=>{
    const onKey = (e)=>{
      const inField = e.target.matches('input,textarea');
      const idx = tabIds.indexOf(active);

      // PageDown / PageUp — work from ANYWHERE, including inside inputs.
      // Conference-room clickers emit these. Blur first so the value commits
      // cleanly and the next slide doesn't inherit caret focus.
      if(e.key==='PageDown'){
        e.preventDefault();
        if(inField && typeof e.target.blur==='function') e.target.blur();
        setActive(tabIds[Math.min(idx+1, tabIds.length-1)]);
        return;
      }
      if(e.key==='PageUp'){
        e.preventDefault();
        if(inField && typeof e.target.blur==='function') e.target.blur();
        setActive(tabIds[Math.max(idx-1, 0)]);
        return;
      }

      // Everything else — only when focus is NOT in an input
      if(inField) return;

      if(e.key==='ArrowRight'){ e.preventDefault(); setActive(tabIds[Math.min(idx+1, tabIds.length-1)]); }
      else if(e.key==='ArrowLeft'){ e.preventDefault(); setActive(tabIds[Math.max(idx-1, 0)]); }
      else if(e.key==='Home'){ setActive(tabIds[0]); }
      else if(e.key==='End'){ setActive(tabIds[tabIds.length-1]); }
      else if(e.key==='Escape'){ setActive(VIEW_OVERVIEW); }
      else if(e.key==='q'||e.key==='Q'){ setActive(VIEW_IDS); }
      else if(e.key==='l'||e.key==='L'){ setActive(VIEW_LOCK); }
      else if(e.key==='k'||e.key==='K'){ setActive(VIEW_COMMITS); }
      else if(/^[1-9]$/.test(e.key)){
        const n = parseInt(e.key,10)-1;
        if(state.kpis[n]) setActive(state.kpis[n].id);
      }
      else if(['g','G','y','Y','r','R','n','N'].includes(e.key)){
        const kpi = state.kpis.find(k=>k.id===active);
        if(!kpi) return;
        const map = {g:'GREEN',y:'YELLOW',r:'RED',n:'NA'};
        const c = map[e.key.toLowerCase()];
        update(s=>{
          const t = s.kpis.find(x=>x.id===kpi.id);
          t.color = t.color===c ? '' : c;
          if(t.color==='RED'){
            if(!s.idsQueue.find(x=>x.kpiId===t.id)) s.idsQueue.push({kpiId:t.id, question:'', addedAt:new Date().toISOString()});
          } else {
            s.idsQueue = s.idsQueue.filter(x=>x.kpiId!==t.id);
          }
        });
      }
    };
    window.addEventListener('keydown', onKey);
    return ()=>window.removeEventListener('keydown', onKey);
  }, [active, tabIds, state.kpis, update]);

  // Speaker notes broadcast
  useEffect(()=>{
    try{ window.parent?.postMessage({slideIndexChanged: tabIds.indexOf(active)}, '*'); }catch(e){}
  }, [active, tabIds]);

  // Presenter channel
  const chanRef = useRef(null);
  useEffect(()=>{
    try{
      chanRef.current = new BroadcastChannel('wbr-presenter');
      chanRef.current.postMessage({type:'state', state, active});
    }catch(e){}
    return ()=>{ try{chanRef.current?.close();}catch(e){} };
  }, []);
  useEffect(()=>{
    try{ chanRef.current?.postMessage({type:'state', state, active}); }catch(e){}
  }, [state, active]);

  // Actions
  const doExport = ()=>{
    const out = JSON.parse(JSON.stringify(state));
    out.savedAt = new Date().toISOString();
    const blob = new Blob([JSON.stringify(out,null,2)], {type:'application/json'});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `wbr-state-wk${String(state.meta.week).padStart(2,'0')}.json`;
    a.click();
    pushToast('Exported WK'+String(state.meta.week).padStart(2,'0')+' snapshot');
  };
  const doReset = ()=>{
    if(!confirm('Clear all current-week inputs? Actuals, colors, commitments, and the IDS queue will be wiped.')) return;
    update(s=>{
      s.kpis.forEach(k=>{ k.actual=''; k.color=''; k.committedAction=''; k.overrideReason=''; });
      s.idsQueue = [];
    });
    pushToast('Cleared current-week inputs');
  };
  const doPrint = ()=>window.print();
  const doPresenter = ()=>{
    // Try the standalone guide first (what you download); fall back to the dev name.
    const candidates = ['WBR Presenter Guide (standalone).html', 'presenter.html'];
    const target = candidates[0];
    const w = window.open(target, 'wbr-presenter', 'width=900,height=700');
    if(w) presenterRef.current = w;
  };

  // Tweaks
  const defaults = (window.__TWEAK_DEFAULTS__) || {density:'normal', showHints:true, seedWK17:false};

  // Render active view
  const activeKpi = state.kpis.find(k=>k.id===active);
  const activeKpiIdx = state.kpis.findIndex(k=>k.id===active);
  let view;
  if(active===VIEW_COVER) view = <Cover state={state} />;
  else if(active===VIEW_PURPOSE) view = <Purpose state={state} />;
  else if(active===VIEW_OVERVIEW) view = <Overview state={state} onOpen={goTo} />;
  else if(active===VIEW_MISSING) view = <Missing state={state} update={update} />;
  else if(active===VIEW_IDS) view = <IDS state={state} update={update} goTo={goTo} />;
  else if(active===VIEW_COMMITS) view = <Commitments state={state} goTo={goTo} update={update} />;
  else if(active===VIEW_LOCK) view = <Lock state={state} />;
  else if(activeKpi) view = <KpiView kpi={activeKpi} idx={activeKpiIdx} state={state} update={update} goTo={goTo} />;

  return (
    <div className="app" data-density={(window.__TWEAKS||{}).density||'normal'}>
      <Topnav state={state} clock={clock}
        onExport={doExport} onReset={doReset} onPrint={doPrint} onPresenter={doPresenter}/>
      <TabRail state={state} active={active} onGo={goTo} />
      <div className="stage" role="main">{view}</div>
      <HintBar active={active} />
      <WBRTweaks state={state} update={update} setState={setState} pushToast={pushToast} />
      {toastsNode}
    </div>
  );
}

function HintBar({active}){
  return (
    <div className="hints">
      <div className="group">
        <span className="item"><span className="kbd">PgDn</span><span className="kbd">PgUp</span> next / prev (clicker-friendly)</span>
        <span className="item"><span className="kbd">←</span><span className="kbd">→</span> step</span>
        <span className="item"><span className="kbd">1</span>–<span className="kbd">6</span> jump to KPI</span>
        <span className="item"><span className="kbd">G</span><span className="kbd">Y</span><span className="kbd">R</span><span className="kbd">N</span> color call</span>
        <span className="item"><span className="kbd">Q</span> IDS · <span className="kbd">K</span> Commits · <span className="kbd">L</span> Lock</span>
      </div>
      <div className="group">
        <span className="item"><span className="kbd">Esc</span> overview</span>
      </div>
    </div>
  );
}

/* ─── Floating Next button — click-ready fallback ────────────── */
function NextButton({tabs, active, onGo}){
  const idx = tabs.findIndex(t=>t.id===active);
  const next = idx >= 0 && idx < tabs.length-1 ? tabs[idx+1] : null;
  const prev = idx > 0 ? tabs[idx-1] : null;
  if(!next && !prev) return null;
  return (
    <div className="next-fab">
      {prev && (
        <button className="next-fab-btn next-fab-btn-prev" onClick={()=>onGo(prev.id)} title="Previous (PgUp, ←)" aria-label={`Previous: ${prev.title||prev.name}`}>
          <span className="next-fab-arrow">←</span>
        </button>
      )}
      {next && (
        <button className="next-fab-btn next-fab-btn-next" onClick={()=>onGo(next.id)} title="Next (PgDn, →)" aria-label={`Next: ${next.title||next.name}`}>
          <span className="next-fab-lbl">
            <span className="next-fab-eyebrow">Next →</span>
            <span className="next-fab-title">{next.title || next.name || next.id}</span>
          </span>
          <span className="next-fab-arrow">→</span>
        </button>
      )}
    </div>
  );
}

/* ─── Tweaks panel ───────────────────────────────────────────── */
function WBRTweaks({state, update, setState, pushToast}){
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "density": "normal",
    "heroScale": 100,
    "showHintBar": true
  }/*EDITMODE-END*/;

  const hookResult = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, ()=>{}];
  const tweaks = Array.isArray(hookResult) ? hookResult[0] : (hookResult?.tweaks || TWEAK_DEFAULTS);
  const setTweak = Array.isArray(hookResult) ? hookResult[1] : (hookResult?.setTweak || (()=>{}));

  useEffect(()=>{
    if(!tweaks) return;
    document.querySelector('.app')?.setAttribute('data-density', tweaks.density || 'normal');
    document.documentElement.style.setProperty('--wbr-hero-scale', (tweaks.heroScale||100)/100);
    const hints = document.querySelector('.hints');
    if(hints) hints.style.display = tweaks.showHintBar ? '' : 'none';
  }, [tweaks]);

  const seedDemo = ()=>{
    update(s=>{
      const demo = [
        {actual:'18.2%',  color:'YELLOW'},
        {actual:'$14,200', color:'RED'},
        {actual:'2 triggers', color:'YELLOW'},
        {actual:'4 flagged', color:'YELLOW'},
        {actual:'24% (4/17)', color:'YELLOW'},
        {actual:'112 ODD', color:'RED'}
      ];
      s.kpis.forEach((k,i)=>{ if(demo[i]){ k.actual = demo[i].actual; k.color = demo[i].color; }});
      s.idsQueue = s.kpis.filter(k=>k.color==='RED').map(k=>({kpiId:k.id,question:'',addedAt:new Date().toISOString()}));
    });
    pushToast('Demo WK17 values filled');
  };
  const clearAll = ()=>{
    update(s=>{
      s.kpis.forEach(k=>{ k.actual=''; k.color=''; k.committedAction=''; k.overrideReason=''; });
      s.idsQueue = [];
    });
    pushToast('Cleared current-week inputs');
  };

  if(!window.TweaksPanel) return null;

  const {TweaksPanel, TweakSection, TweakRadio, TweakSlider, TweakToggle, TweakButton} = window;

  return (
    <TweaksPanel title="Tweaks">
      <TweakSection title="Display">
        <TweakRadio label="Density" value={tweaks.density} onChange={v=>setTweak('density', v)}
          options={[{value:'compact',label:'Compact'},{value:'normal',label:'Normal'},{value:'spacious',label:'Spacious'}]} />
        <TweakToggle label="Show hint bar" value={tweaks.showHintBar} onChange={v=>setTweak('showHintBar', v)} />
      </TweakSection>
      <TweakSection title="Demo data">
        <TweakButton label="Fill WK17 with demo values" onClick={seedDemo} />
        <TweakButton label="Clear current-week inputs" onClick={clearAll} />
      </TweakSection>
    </TweaksPanel>
  );
}

function __wbrMount(){ ReactDOM.createRoot(document.getElementById('root')).render(<App/>); }
if (window.__WBR_READY) __wbrMount();
else document.addEventListener('wbr:ready', __wbrMount, { once: true });
