/* ============================================================
   SILVERLINES — core: icons, primitives, cinematic dummy data
   ============================================================ */
const { useState, useEffect, useRef, useMemo, createContext, useContext } = React;

/* ---------- Lucide icon helper (UMD -> React) ---------- */
function Icon({ name, size = 18, sw = 2, className = '', style }) {
  const lib = window.lucide && window.lucide.icons ? window.lucide.icons : {};
  const node = lib[name] || lib.Circle || ['svg', {}, []];
  const kids = Array.isArray(node) && Array.isArray(node[2]) ? node[2] : [];
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round"
      className={className} style={style} aria-hidden="true">
      {kids.map((c, i) => {
        const tag = c[0]; const attrs = c[1] || {};
        return React.createElement(tag, { key: i, ...attrs });
      })}
    </svg>
  );
}

/* ---------- small classnames helper ---------- */
const cx = (...a) => a.filter(Boolean).join(' ');

/* ---------- ProFlow automation pill ---------- */
function ProFlowPill({ label = 'Auto', tip }) {
  return (
    <span title={tip || 'Auto-populated by ProFlow'}
      className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-proflow/40 bg-proflow/10 px-2.5 py-[3px] text-[10px] font-700 uppercase tracking-[0.12em] text-proflow-soft">
      <span className="proflow-pulse h-1 w-1 rounded-full bg-proflow"></span>
      {label}
    </span>
  );
}

/* ---------- iOS-grade toggle switch — tap OR swipe the knob ---------- */
const SWITCH_ACCENT = { electric: 'var(--electric)', proflow: 'var(--proflow)', mint: 'var(--mint)', amber: 'var(--amber)' };
function Switch({ on, onChange, accent = 'electric', size = 'md', disabled = false, 'aria-label': ariaLabel }) {
  const S = size === 'sm'
    ? { w: 40, h: 24, knob: 20, pad: 2 }
    : { w: 47, h: 28, knob: 24, pad: 2 };
  const travel = S.w - S.knob - S.pad * 2;
  const onCol = `rgb(${SWITCH_ACCENT[accent] || SWITCH_ACCENT.electric})`;

  const drag = React.useRef({ active: false, moved: false, startX: 0, x: 0 });
  const [pos, setPos] = React.useState(null);   // px during a drag, else null
  const [press, setPress] = React.useState(false);

  const restX = on ? travel : 0;
  const curX = pos == null ? restX : pos;
  const lit = pos == null ? on : pos > travel / 2;   // track colour follows the knob mid-drag

  const end = (commit) => {
    window.removeEventListener('pointermove', onMove);
    window.removeEventListener('pointerup', onUp);
    window.removeEventListener('pointercancel', onUp);
    const d = drag.current;
    const wasMoved = d.moved, finalX = d.x;
    d.active = false;
    setPress(false);
    setPos(null);
    if (!commit) return;
    if (!wasMoved) { onChange(!on); return; }   // a tap
    const next = finalX > travel / 2;
    if (next !== on) onChange(next);            // a swipe past the midpoint
  };
  const onMove = (e) => {
    const d = drag.current;
    if (!d.active) return;
    const dx = e.clientX - d.startX;
    if (Math.abs(dx) > 3) d.moved = true;
    const nx = Math.min(travel, Math.max(0, restX + dx));
    d.x = nx;
    setPos(nx);
  };
  const onUp = () => end(true);
  const onDown = (e) => {
    if (disabled) return;
    e.preventDefault();
    drag.current = { active: true, moved: false, startX: e.clientX, x: restX };
    setPress(true);
    setPos(restX);
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    window.addEventListener('pointercancel', onUp);
  };
  const onKey = (e) => {
    if (disabled) return;
    if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onChange(!on); }
  };

  const knobW = S.knob + (press && !disabled ? 4 : 0);     // iOS-style stretch while held
  const left = Math.min(S.pad + curX, S.w - S.pad - knobW);
  const dragging = pos != null;

  return (
    <button type="button" role="switch" aria-checked={!!on} aria-label={ariaLabel}
      disabled={disabled} onPointerDown={onDown} onKeyDown={onKey}
      style={{
        position: 'relative', flexShrink: 0, width: S.w, height: S.h, padding: 0, border: 0,
        borderRadius: 999, cursor: disabled ? 'default' : 'pointer', touchAction: 'none',
        background: lit ? onCol : 'rgb(var(--ink-600))',
        boxShadow: lit ? 'none' : 'inset 0 0 0 1px rgb(var(--line) / .9)',
        opacity: disabled ? 0.45 : 1,
        transition: dragging ? 'background-color .12s linear' : 'background-color .25s ease',
        WebkitTapHighlightColor: 'transparent',
      }}>
      <span style={{
        position: 'absolute', top: S.pad, left, width: knobW, height: S.knob,
        borderRadius: 999, background: '#fff',
        boxShadow: '0 1px 1px rgb(0 0 0 / .04), 0 3px 8px rgb(0 0 0 / .28)',
        transition: dragging ? 'width .14s ease' : 'left .26s cubic-bezier(.32,.72,0,1), width .14s ease',
      }} />
    </button>
  );
}

/* ---------- Pill / status chip ---------- */
function Chip({ children, tone = 'neutral', className = '' }) {
  const tones = {
    neutral: 'bg-ink-700 text-zinc-300 border-line',
    electric:'bg-electric/12 text-electric-soft border-electric/30',
    proflow: 'bg-proflow/12 text-proflow-soft border-proflow/30',
    mint:    'bg-mint/12 text-mint border-mint/30',
    amber:   'bg-amber/12 text-amber border-amber/30',
  };
  return <span className={cx('inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[11px] font-semibold', tones[tone], className)}>{children}</span>;
}

/* ---------- Section header ---------- */
function SectionTitle({ kicker, title, icon, action }) {
  return (
    <div className="mb-5 flex items-end justify-between gap-4">
      <div>
        {kicker && <div className="mb-1.5 flex items-center gap-2 whitespace-nowrap text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-500">
          {icon && <Icon name={icon} size={13} />}{kicker}</div>}
        <h2 className="font-display text-[26px] font-800 leading-none tracking-[-0.01em] text-head">{title}</h2>
      </div>
      {action}
    </div>
  );
}

/* ---------- Card shell ---------- */
function Panel({ children, className = '', pad = true, hover = false }) {
  return (
    <div className={cx('relative rounded-2xl border border-line bg-ink-850',
      pad && 'p-5', hover && 'transition-colors hover:border-ink-600', className)}>
      {children}
    </div>
  );
}

/* ---------- Avatar ---------- */
function Avatar({ name, color, size = 30, ring = false }) {
  const initials = name.split(' ').map(w => w[0]).slice(0, 2).join('');
  /* dark ink on light brand colors, white on deep ones */
  const hx = (color || '#888').replace('#', '');
  const [r, g, b] = [0, 2, 4].map(i => parseInt(hx.slice(i, i + 2) || '88', 16));
  const ink = (0.2126 * r + 0.7152 * g + 0.0722 * b) > 150 ? '#06080F' : '#FFFFFF';
  return (
    <span className={cx('inline-flex shrink-0 items-center justify-center rounded-full font-display font-600 text-[11px]',
      ring && 'ring-2 ring-ink-850')}
      style={{ width: size, height: size, background: color, color: ink, fontSize: size * 0.36 }}>
      {initials}
    </span>
  );
}

/* ---------- CSV parser + drag-drop importer (shared) ---------- */
function parseCSV(text) {
  const lines = text.replace(/\r/g, '').split('\n').filter(l => l.trim());
  if (lines.length < 2) return [];
  const split = l => {
    const out = []; let cur = '', q = false;
    for (const ch of l) {
      if (ch === '"') q = !q;
      else if (ch === ',' && !q) { out.push(cur); cur = ''; }
      else cur += ch;
    }
    out.push(cur); return out.map(s => s.trim());
  };
  const head = split(lines[0]).map(h => h.toLowerCase());
  return lines.slice(1).map(l => {
    const cells = split(l), row = {};
    head.forEach((h, i) => row[h] = cells[i] || '');
    return row;
  });
}
const pick = (row, keys) => { for (const k of keys) { const f = Object.keys(row).find(h => h.includes(k)); if (f && row[f]) return row[f]; } return ''; };

function DropImport({ label, hint, onRows, sample, icon = 'UploadCloud', compact }) {
  const [drag, setDrag] = useState(false);
  const [files, setFiles] = useState([]);
  const inputRef = useRef(null);

  const ingest = async list => {
    const arr = [...list]; const added = [];
    for (const f of arr) {
      const ext = (f.name.split('.').pop() || '').toLowerCase();
      let rows = [];
      if (['csv', 'tsv', 'txt'].includes(ext)) { try { rows = parseCSV(await f.text()); } catch (e) {} }
      if (!rows.length && sample) rows = sample(f.name, ext);   // PDF / XLSX / JPEG → demo-extract
      added.push({ name: f.name, ext, n: rows.length, parsed: ['csv','tsv','txt'].includes(ext) });
      onRows && onRows(rows, f);
    }
    setFiles(fs => [...added, ...fs]);
  };

  const extIcon = e => e === 'csv' || e === 'tsv' ? 'FileSpreadsheet' : e === 'pdf' ? 'FileText'
    : ['jpg','jpeg','png','webp'].includes(e) ? 'FileImage' : e === 'xlsx' || e === 'xls' ? 'FileSpreadsheet' : 'File';

  return (
    <div>
      <div
        onDragOver={e => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={e => { e.preventDefault(); setDrag(false); ingest(e.dataTransfer.files); }}
        onClick={() => inputRef.current && inputRef.current.click()}
        className={cx('flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-xl border-2 border-dashed text-center transition-colors',
          compact ? 'py-4 px-3' : 'py-6 px-4',
          drag ? 'border-electric bg-electric/8' : 'border-ink-600 hover:border-zinc-500')}>
        <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-electric/12 text-electric-soft"><Icon name={icon} size={18} /></span>
        <div className="text-[12.5px] font-700 text-zinc-200">{label}</div>
        <div className="text-[11px] text-zinc-500">{hint}</div>
        <div className="mt-0.5 flex items-center gap-1 text-[10px] font-600 text-zinc-600">
          {['CSV','XLSX','PDF','JPEG'].map(t => <span key={t} className="rounded border border-line px-1.5 py-0.5">{t}</span>)}
        </div>
      </div>
      <input ref={inputRef} type="file" multiple accept=".csv,.tsv,.txt,.pdf,.xlsx,.xls,.jpg,.jpeg,.png" className="hidden"
        onChange={e => ingest(e.target.files)} />
      {files.length > 0 && (
        <div className="mt-2 space-y-1.5">
          {files.map((f, i) => (
            <div key={i} className="flex items-center gap-2 rounded-lg border border-line bg-ink-900 px-2.5 py-2">
              <Icon name={extIcon(f.ext)} size={15} className="shrink-0 text-zinc-400" />
              <span className="min-w-0 flex-1 truncate text-[12px] font-600 text-zinc-200">{f.name}</span>
              <span className={cx('shrink-0 rounded-full px-2 py-0.5 text-[10px] font-700',
                f.parsed ? 'bg-mint/15 text-mint' : 'bg-amber/15 text-amber')}>
                {f.parsed ? `${f.n} parsed` : `${f.n} extracted`}
              </span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------- Striped image placeholder ---------- */
function PhotoSlot({ label, h = 140, tone, className = '' }) {
  return (
    <div className={cx('scan relative flex items-end overflow-hidden rounded-xl border border-line', className)}
      style={{ height: h, background: tone }}>
      <div className="absolute inset-0 bg-gradient-to-t from-black/55 to-transparent" />
      <span className="relative z-10 m-2.5 rounded-md bg-black/55 px-2 py-1 font-mono text-[10px] tracking-wide text-zinc-300 backdrop-blur-sm">{label}</span>
    </div>
  );
}

/* ============================================================
   DATA — "Crimson Skies — Commercial Campaign"
   ============================================================ */
const PROJECT = {
  name: 'Crimson Skies',
  type: 'Commercial Campaign',
  code: 'CRSK-2026',
  client: 'Aether Aviation Co.',
  status: 'In Production',
  day: 'Day 1 of 4',
};

const PEOPLE = [
  { name: 'Dahlia Mercer', role: 'Director', dept: 'Direction', color:'#F57F45', phone:'+1 310 555 0148', email:'dahlia@crimsonskies.co', online:true },
  { name: 'Idris Vance', role: 'Director of Photography', dept:'Camera', color:'#BD2555', phone:'+1 310 555 0192', email:'idris@lensworks.co', online:true },
  { name: 'Noor Haddad', role: 'Producer', dept:'Production', color:'#22D48A', phone:'+1 213 555 0177', email:'noor@producer.app', online:true },
  { name: 'Marco Reyes', role: 'Client Lead', dept:'Client', color:'#FFB020', phone:'+1 415 555 0110', email:'marco@aetheraviation.com', online:false },
  { name: 'Suki Tanaka', role: '1st AD', dept:'Production', color:'#F43F3E', phone:'+1 323 555 0163', email:'suki@producer.app', online:true },
  { name: 'Theo Nkemelu', role: 'Gaffer', dept:'Lighting', color:'#F9A06E', phone:'+1 818 555 0124', email:'theo@gridlight.co', online:false },
  { name: 'Lena Frost', role: 'Lead Talent', dept:'Cast', color:'#F7A8CF', phone:'+1 310 555 0199', email:'lena@frosttalent.co', online:true },
  { name: 'Ravi Anand', role: 'Production Designer', dept:'Art', color:'#5B0952', phone:'+1 626 555 0140', email:'ravi@setpiece.co', online:false },
  { name: 'Petra Sol', role: 'Stills Photographer', dept:'Stills', color:'#5FB0E8', phone:'+1 310 555 0204', email:'petra@solstills.co', online:true },
];

/* A project carries several SPOTS — separate films + a stills shoot — each with
   its own scenes/looks. Every scene is tagged with a `spot` id and a `kind`
   (film | stills). `asks` = client / photographer / director requests that hang
   off a scene (distinct from department `req` requirements). */
const SCENES = [
  /* ── Spot A · Hero Film ───────────────────────────── */
  { no:'12A', title:'Cockpit — Dawn Ignition', loc:'Stage 4 · Cockpit Build', tod:'Dawn', pages:'1 3/8', cast:['Lena Frost'], dur:'1h 30m', color:'#3a3450', spot:'hero', kind:'film' },
  { no:'12B', title:'Tarmac Walk — Hero Shot', loc:'Backlot · Runway', tod:'Dawn', pages:'2/8', cast:['Lena Frost','Talent x4'], dur:'2h 00m', color:'#503a3a', spot:'hero', kind:'film',
    asks:[{ id:'as-12b-1', source:'Client', text:'Need a clean vertical 9:16 of the hero walk for paid social.', status:'planned' }] },
  { no:'14',  title:'Hangar Reveal', loc:'Hangar 7 · Practical', tod:'Day', pages:'1 1/8', cast:['Lena Frost'], dur:'1h 10m', color:'#34404f', spot:'hero', kind:'film',
    asks:[{ id:'as-14-1', source:'Director', text:'Grab a slow-mo insert of the hangar doors — reuse in the cutdown.', status:'requested' }] },
  { no:'18',  title:'Ascent — VFX Plate', loc:'Stage 4 · Bluescreen', tod:'Day', pages:'5/8', cast:['Lena Frost'], dur:'0h 45m', color:'#3a4d3f', spot:'hero', kind:'film' },
  /* ── Spot B · Social Cutdowns ─────────────────────── */
  { no:'21',  title:'Sunset Glide', loc:'Backlot · Runway', tod:'Magic Hour', pages:'7/8', cast:['Lena Frost'], dur:'0h 40m', color:'#4f4434', spot:'cut', kind:'film',
    asks:[{ id:'as-21-1', source:'Agency', text:'Two takes — one with logo lockup room top-left for the :15.', status:'requested' }] },
  { no:'23',  title:'Touchdown — Final Beat', loc:'Hangar 7', tod:'Dusk', pages:'1 2/8', cast:['Lena Frost','Crew'], dur:'1h 20m', color:'#3f3450', spot:'cut', kind:'film' },
  /* ── Spot S · Hero Billboard Stills (looks, not scenes) ── */
  { no:'S1', title:'Hero Aircraft — 3/4 Front', loc:'Hangar 7 · Practical', tod:'Day', pages:'—', cast:['Lena Frost'], dur:'1h 00m', color:'#1f3a4a', spot:'stills', kind:'stills',
    asks:[
      { id:'as-s1-1', source:'Photographer', text:'Hold 20 min of golden light on the 3/4 front — needs the long lens.', status:'requested' },
      { id:'as-s1-2', source:'Client', text:'Logo on the fuselage must be tack-sharp and readable at billboard scale.', status:'captured' }
    ] },
  { no:'S2', title:'Pilot Portrait — Heroic', loc:'Stage 4 · Seamless', tod:'Day', pages:'—', cast:['Lena Frost'], dur:'0h 45m', color:'#24323f', spot:'stills', kind:'stills' },
  { no:'S3', title:'Product — Watch & Instruments', loc:'Stage 4 · Tabletop', tod:'Day', pages:'—', cast:[], dur:'0h 50m', color:'#2a2f3a', spot:'stills', kind:'stills',
    asks:[{ id:'as-s3-1', source:'Client', text:'Macro of the altimeter face — must match the watch dial finish.', status:'planned' }] },
];

/* ---- SPOTS — the films + stills that make up one commercial job ---- */
const SPOTS = [
  { id:'hero',   code:'A', name:'Hero Film',            type:'film',   color:'#F57F45', format:'60s · 16:9',         deliverable:'Broadcast + online hero film', lead:'Dahlia Mercer' },
  { id:'cut',    code:'B', name:'Social Cutdowns',      type:'film',   color:'#22D48A', format:'3× 15s · 9:16 / 1:1', deliverable:'Reels, TikTok & Shorts cutdowns', lead:'Dahlia Mercer' },
  { id:'stills', code:'S', name:'Hero Billboard Stills', type:'stills', color:'#5FB0E8', format:'48-sheet + press',    deliverable:'Key art — billboard, OOH & press kit', lead:'Petra Sol' },
];
const DEFAULT_SPOT = { '12A':'hero','12B':'hero','14':'hero','18':'hero','21':'cut','23':'cut','S1':'stills','S2':'stills','S3':'stills' };

/* ---- requests / “asks” taxonomy (who asked + where it is in the pipe) ---- */
const ASK_SOURCES = {
  Client:       { color:'#FFB020', icon:'Briefcase' },
  Photographer: { color:'#5FB0E8', icon:'Camera' },
  Director:     { color:'#F57F45', icon:'Megaphone' },
  Agency:       { color:'#F7A8CF', icon:'PenTool' },
  DP:           { color:'#BD2555', icon:'Aperture' },
};
const ASK_STATUS = ['requested','planned','captured','delivered'];
const ASK_STATUS_META = {
  requested:{ label:'Requested', tone:'amber',    icon:'Inbox' },
  planned:  { label:'Planned',   tone:'electric', icon:'CalendarCheck' },
  captured: { label:'Captured',  tone:'proflow',  icon:'CircleCheck' },
  delivered:{ label:'Delivered', tone:'mint',     icon:'Check' },
};

/* ProFlow-derived shot list */
const SHOTS = [
  { scene:'12A', shot:'A', type:'CU', angle:'Eye-level', lens:'50mm', move:'Static', notes:'Ignition switch detail', auto:true },
  { scene:'12A', shot:'B', type:'MS', angle:'Low', lens:'32mm', move:'Slow push', notes:'Hero in seat', auto:true },
  { scene:'12B', shot:'A', type:'WS', angle:'Eye-level', lens:'24mm', move:'Dolly track', notes:'Tarmac hero walk', auto:true },
  { scene:'12B', shot:'B', type:'MCU', angle:'High', lens:'85mm', move:'Steadicam', notes:'Profile, lens flare', auto:false },
  { scene:'14',  shot:'A', type:'WS', angle:'Eye-level', lens:'18mm', move:'Crane up', notes:'Hangar door reveal', auto:true },
  { scene:'18',  shot:'A', type:'MS', angle:'Eye-level', lens:'40mm', move:'Static', notes:'Bluescreen plate', auto:true },
];

const UPLOADS = [
  { name:'Rushes — Day 1 A-Cam', size:'248.5 GB', kind:'ProRes 4444 · 6K', pct:78, speed:'412 MB/s', color:'#F57F45' },
  { name:'Rushes — Day 1 B-Cam', size:'191.2 GB', kind:'ProRes 4444 · 6K', pct:54, speed:'388 MB/s', color:'#F57F45' },
  { name:'Casting Tapes — Round 2', size:'12.8 GB', kind:'H.264 · 1080p', pct:100, speed:'—', color:'#22D48A' },
  { name:'VFX Stripouts — Hangar', size:'64.0 GB', kind:'EXR Sequence', pct:31, speed:'276 MB/s', color:'#BD2555' },
  { name:'Sound — Lav + Boom', size:'4.2 GB', kind:'WAV · 96kHz', pct:12, speed:'120 MB/s', color:'#FFB020' },
];

const MOOD = [
  { label:'Aerial reference · dawn', h:200, tone:'#2a2f44' },
  { label:'Color palette · crimson/steel', h:130, tone:'#4a2a2f' },
  { label:'Wardrobe · flight suit', h:240, tone:'#2f3a44' },
  { label:'Location · Hangar 7', h:160, tone:'#34402f' },
  { label:'Lighting · hard backlight', h:150, tone:'#443a2a' },
  { label:'Lens · anamorphic flare', h:220, tone:'#3a2f44' },
  { label:'Prop · vintage altimeter', h:140, tone:'#2a3a3f' },
  { label:'Texture · brushed steel', h:180, tone:'#33333a' },
];

const REVIEW_COMMENTS = [
  { t: 6,  by:'Marco Reyes', color:'#FFB020', role:'Client', text:'Can we warm the grade here? Skin feels a touch cyan.', x:18, y:34 },
  { t: 14, by:'Dahlia Mercer', color:'#F57F45', role:'Director', text:'Hold this frame 6 more frames — let the flare breathe.', x:62, y:48 },
  { t: 23, by:'Marco Reyes', color:'#FFB020', role:'Client', text:'Logo on the fuselage needs to be readable by here.', x:40, y:70 },
  { t: 31, by:'Idris Vance', color:'#BD2555', role:'DP', text:'Re-rack focus to foreground gauge on this beat.', x:74, y:28 },
];

const NAV = [
  { id:'spots',      label:'Spots', sub:'Films, cutdowns & stills', icon:'Film' },
  { id:'develop',    label:'Development', sub:'Briefing & moodboard', icon:'PencilRuler' },
  { id:'script',     label:'Script & Breakdown', sub:'Import & tag elements', icon:'ScrollText' },
  { id:'preprod',    label:'Pre-Production', sub:'Storyboard & ProFlow', icon:'Clapperboard' },
  { id:'schedule',   label:'Stripboard',   sub:'Scene scheduling board', icon:'CalendarRange' },
  { id:'production', label:'Production',  sub:'Call sheets & schedule', icon:'Video' },
  { id:'dpr',        label:'Daily Report', sub:'Scenes, pages & setups', icon:'ClipboardCheck' },
  { id:'post',       label:'Post & Review', sub:'Approvals & comments', icon:'MessageSquareReply' },
  { id:'postver',    label:'Post Versions', sub:'Cuts & deliverables', icon:'Layers3' },
];

/* ---------- Inline editable text (shared) ---------- */
function Edit({ value, onChange, className = '', multiline = false, placeholder = 'Add…', align = 'left' }) {
  const [editing, setEditing] = useState(false);
  const ref = useRef(null);
  useEffect(() => { if (editing && ref.current) { ref.current.focus(); ref.current.select && ref.current.select(); } }, [editing]);
  if (editing) {
    const common = {
      ref, value,
      onChange: e => onChange(e.target.value),
      onBlur: () => setEditing(false),
      onKeyDown: e => { if (e.key === 'Enter' && !multiline) { e.preventDefault(); setEditing(false); } if (e.key === 'Escape') setEditing(false); },
      className: cx('block w-full rounded-md bg-ink-950 px-1.5 py-0.5 outline-none ring-1 ring-electric/70', className)
    };
    return multiline ? <textarea {...common} rows={2} /> : <input {...common} type="text" />;
  }
  return (
    <span onClick={() => setEditing(true)} title="Click to edit"
      className={cx('inline-block cursor-text rounded-md -mx-1 px-1 underline decoration-dotted decoration-zinc-700 underline-offset-4 transition-colors hover:bg-white/5 hover:decoration-electric-soft',
        align === 'center' && 'text-center', className)}>
      {value || <span className="text-zinc-600">{placeholder}</span>}
    </span>
  );
}

/* ---------- Money helper + shared access context ---------- */
/* project-wide currency — settable in Settings or the Budget module.
   money() reads a cached code refreshed on the 'sl-currency' event so
   every figure across the app re-renders when the currency changes. */
const CURRENCIES = {
  USD: { sym: '$',    label: 'US Dollar' },
  EUR: { sym: '€',    label: 'Euro' },
  GBP: { sym: '£',    label: 'British Pound' },
  CHF: { sym: 'CHF ', label: 'Swiss Franc' },
  CAD: { sym: 'C$',   label: 'Canadian Dollar' },
  AUD: { sym: 'A$',   label: 'Australian Dollar' },
  JPY: { sym: '¥',    label: 'Japanese Yen' },
};
let __curCode = 'USD';
function curRefresh() {
  try {
    /* per-project brief currency wins; global settings.v1 is only a default so
       currency can't leak from one production into another */
    const b = window.SLStore.get('silverlines.brief.v1', {});
    if (b.currency) {
      const hit = Object.keys(CURRENCIES).find(k => k === b.currency || CURRENCIES[k].sym.trim() === String(b.currency).trim());
      if (hit) { __curCode = hit; return; }
    }
    const st = window.SLStore.get('silverlines.settings.v1', {});
    if (st.currency && CURRENCIES[st.currency]) { __curCode = st.currency; return; }
  } catch (e) {}
}
curRefresh();
if (typeof window !== 'undefined') window.addEventListener('sl-currency', curRefresh);
const curCode = () => __curCode;
const setCurrency = code => {
  if (!CURRENCIES[code]) return;
  try {
    /* store per-project (brief.v1 is namespaced) so it can't leak across
       productions; settings.v1 stays only a legacy global default */
    const b = window.SLStore.get('silverlines.brief.v1', {});
    b.currency = code;
    window.SLStore.set('silverlines.brief.v1', b);
  } catch (e) {}
  __curCode = code;
  window.dispatchEvent(new CustomEvent('sl-currency'));
};
const money = n => {
  const amt = Math.round(Number(n) || 0);
  const code = CURRENCIES[__curCode] ? __curCode : 'USD';
  /* locale from the active UI language (Swiss) so grouping + symbol placement are
     correct per currency: CHF 106'600, 106 600 € (FR), $106,600 (EN) */
  const l = (window.getLang && window.getLang()) || 'en';
  const loc = l === 'fr' ? 'fr-CH' : l === 'de' ? 'de-CH' : 'en-US';
  try {
    return new Intl.NumberFormat(loc, { style: 'currency', currency: code, maximumFractionDigits: 0 }).format(amt);
  } catch (e) {
    return (CURRENCIES[code] || CURRENCIES.USD).sym + amt.toLocaleString(loc);
  }
};
const AccessContext = createContext(null);

/* ---------- live notification store (real events only — no demo data) ---------- */
const NOTIF_KEY = 'silverlines.notifs.v1';
const readNotifs = () => window.SLStore.get(NOTIF_KEY, []);
const writeNotifs = list => {
  window.SLStore.set(NOTIF_KEY, list.slice(0, 30));
  window.dispatchEvent(new CustomEvent('sl-notifs'));
};
window.notify = (text, icon = 'Bell', moduleId = null) => {
  writeNotifs([{ id: Date.now() + Math.random(), text, icon, moduleId, ts: Date.now(), read: false }, ...readNotifs()]);
};
function useNotifs() {
  const [list, setList] = useState(readNotifs);
  useEffect(() => {
    const h = () => setList(readNotifs());
    window.addEventListener('sl-notifs', h);
    return () => window.removeEventListener('sl-notifs', h);
  }, []);
  const markAll = () => writeNotifs(readNotifs().map(n => ({ ...n, read: true })));
  const markOne = id => writeNotifs(readNotifs().map(n => n.id === id ? { ...n, read: true } : n));
  return { list, markAll, markOne };
}
const notifAgo = ts => {
  const m = Math.round((Date.now() - ts) / 60000);
  if (m < 1) return 'now';
  if (m < 60) return m + 'm';
  const h = Math.round(m / 60);
  return h < 24 ? h + 'h' : Math.round(h / 24) + 'd';
};

/* ---------- Empty state — friendly placeholder for a blank list/module ---------- */
function EmptyState({ icon = 'Inbox', title, sub, action }) {
  return (
    <div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-ink-600 px-6 py-14 text-center">
      <span className="flex h-12 w-12 items-center justify-center rounded-full bg-ink-800 text-zinc-500"><Icon name={icon} size={22} /></span>
      <div>
        <div className="font-display text-[15px] font-700 text-zinc-200">{title}</div>
        {sub && <div className="mx-auto mt-1 max-w-[360px] text-[12.5px] leading-relaxed text-zinc-500">{sub}</div>}
      </div>
      {action}
    </div>
  );
}

/* ---------- Global undo stack — destructive actions only ----------
   Any module can call window.pushUndo(label, restoreFn) right after a
   delete/remove. Surfaces a dismissible "Undo" toast (bottom-right,
   stacked, 7s) and Cmd/Ctrl+Z restores the most recent one. Kept
   separate from window.toast (bottom-center, no action) so the two
   never collide visually. */
function UndoToasts() {
  const [list, setList] = useState([]);
  const listRef = useRef([]);
  listRef.current = list;
  useEffect(() => {
    window.pushUndo = (label, restore) => {
      const id = Date.now() + Math.random();
      const entry = { id, label, restore };
      setList(l => [...l.slice(-2), entry]);
      setTimeout(() => setList(l => l.filter(t => t.id !== id)), 7000);
    };
    const onKey = e => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'z') {
        const cur = listRef.current;
        if (!cur.length) return;
        e.preventDefault();
        const last = cur[cur.length - 1];
        last.restore();
        window.toast && window.toast(`Undone — ${last.label}`, 'Undo2');
        setList(l => l.filter(t => t.id !== last.id));
      }
    };
    window.addEventListener('keydown', onKey);
    return () => { window.pushUndo = () => {}; window.removeEventListener('keydown', onKey); };
  }, []);
  const fire = e => { e.restore(); setList(l => l.filter(x => x.id !== e.id)); window.toast && window.toast(`Undone — ${e.label}`, 'Undo2'); };
  return (
    <div className="pointer-events-none fixed bottom-6 right-6 z-[92] flex flex-col items-end gap-2">
      {list.map(e => (
        <div key={e.id} className="fade-up pointer-events-auto flex items-center gap-3 rounded-xl border border-line bg-ink-850/95 py-2.5 pl-4 pr-2.5 text-[12.5px] font-600 text-zinc-100 shadow-2xl backdrop-blur">
          <Icon name="Trash2" size={14} className="shrink-0 text-zinc-500" /><span className="max-w-[240px] truncate">{e.label}</span>
          <button onClick={() => fire(e)} className="ml-1 flex shrink-0 items-center gap-1.5 rounded-lg bg-ink-700 px-2.5 py-1.5 text-[11.5px] font-700 text-electric-soft transition-colors hover:bg-ink-600">
            <Icon name="Undo2" size={12} /> Undo
          </button>
        </div>
      ))}
    </div>
  );
}

/* ---------- Toast notifications (global bus: window.toast) ---------- */
function Toasts() {
  const [list, setList] = useState([]);
  useEffect(() => {
    window.toast = (msg, icon = 'Check') => {
      const id = Date.now() + Math.random();
      setList(l => [...l.slice(-2), { id, msg, icon }]);
      setTimeout(() => setList(l => l.filter(t => t.id !== id)), 3200);
    };
    return () => { window.toast = () => {}; };
  }, []);
  return (
    <div className="pointer-events-none fixed bottom-6 left-1/2 z-[90] flex w-max max-w-[90vw] -translate-x-1/2 flex-col items-center gap-2">
      {list.map(t => (
        <div key={t.id} className="fade-up flex items-center gap-2.5 rounded-xl border border-line bg-ink-850/95 px-4 py-2.5 text-[12.5px] font-600 text-zinc-100 shadow-2xl backdrop-blur">
          <Icon name={t.icon} size={14} className="shrink-0 text-electric-soft" />{t.msg}
        </div>
      ))}
    </div>
  );
}

/* ---------- Department phone-access roster ---------- */
const ACCESS = [
  { id:'gaffer', member:'Theo Nkemelu', role:'Gaffer', dept:'Lighting', color:'#F9A06E', tool:'floorplan', toolLabel:'Floor Plan Designer',
    scope:['Edit floor plan','View moodboard','View call sheet'], lastActive:'2 min ago', device:'iPhone 15 Pro' },
  { id:'art', member:'Ravi Anand', role:'Production Designer', dept:'Art', color:'#5B0952', tool:'purchase', toolLabel:'Purchasing List',
    scope:['Add purchase items','Suggest to producer','View set list'], lastActive:'just now', device:'iPad Air' },
  { id:'dop', member:'Idris Vance', role:'Director of Photography', dept:'Camera', color:'#BD2555', tool:'floorplan', toolLabel:'Floor Plan (review)',
    scope:['View floor plan','Comment on lighting','View shot list'], lastActive:'14 min ago', device:'iPhone 14' },
  { id:'ad', member:'Suki Tanaka', role:'1st AD', dept:'Production', color:'#F43F3E', tool:'production', toolLabel:'Call Sheet & Schedule',
    scope:['View call sheet','Edit schedule','Message crew'], lastActive:'5 min ago', device:'iPhone 15' },
  { id:'mua', member:'Coraline Bisset', role:'Key MUA', dept:'Hair & Makeup', color:'#FF5D92', tool:'production', toolLabel:'Call Sheet (view)',
    scope:['View call sheet','View talent list'], lastActive:'1 hr ago', device:'Android · S24' },
];

/* ---------- Floor-plan fixture palette (Shot-Designer style) ---------- */
const FIXTURES = [
  { key:'hmi',       label:'HMI 1.2k',    cat:'Lighting',    icon:'Sun',                 color:'#FFB020', beam:true },
  { key:'tungsten',  label:'Tungsten',    cat:'Lighting',    icon:'Flame',               color:'#F2B24A', beam:true },
  { key:'led',       label:'LED Panel',   cat:'Lighting',    icon:'Square',              color:'#22D48A', beam:true },
  { key:'kino',      label:'Kino Flo',    cat:'Lighting',    icon:'AlignJustify',        color:'#22D48A', beam:true },
  { key:'china',     label:'China Ball',  cat:'Lighting',    icon:'Circle',              color:'#EFDD58', beam:false },
  { key:'practical', label:'Practical',   cat:'Lighting',    icon:'Lightbulb',           color:'#EFDD58', beam:false },
  { key:'camera',    label:'Camera',      cat:'Camera',      icon:'Video',               color:'#F57F45', beam:true },
  { key:'dolly',     label:'Dolly',       cat:'Camera',      icon:'CircleDot',           color:'#F57F45', beam:false },
  { key:'move',      label:'Movement',    cat:'Camera',      icon:'MoveRight',           color:'#F9A06E', beam:false, arrow:true },
  { key:'flag',      label:'Flag',        cat:'Grip',        icon:'Flag',                color:'#9aa0aa', beam:false },
  { key:'bounce',    label:'Bounce',      cat:'Grip',        icon:'RectangleHorizontal', color:'#cfd3da', beam:false },
  { key:'diff',      label:'Diffusion',   cat:'Grip',        icon:'Grid2x2',             color:'#aeb4be', beam:false },
  { key:'wall',      label:'Wall',        cat:'Set & Props', icon:'Minus',               color:'#7b8290', beam:false },
  { key:'door',      label:'Door',        cat:'Set & Props', icon:'DoorOpen',            color:'#b08d63', beam:false },
  { key:'window',    label:'Window',      cat:'Set & Props', icon:'RectangleVertical',   color:'#7aa0c9', beam:false },
  { key:'furniture', label:'Furniture',   cat:'Set & Props', icon:'Armchair',            color:'#a78b6b', beam:false },
  { key:'mark',      label:'Talent Mark', cat:'Set & Props', icon:'MapPin',              color:'#F43F3E', beam:false },
];
const FIXTURE_CATS = ['Lighting','Camera','Grip','Set & Props'];

/* segment / path tools (adjustable endpoints) */
const SEGMENTS = [
  { key:'wall',      label:'Wall',        color:'#8b93a3', w:6, kind:'segment' },
  { key:'cammove',   label:'Camera Move', color:'#F57F45', w:3, kind:'segment', arrow:true },
  { key:'actormove', label:'Actor Move',  color:'#F43F3E', w:3, kind:'segment', arrow:true, dash:true },
  { key:'blackflag', label:'Black / neg fill', color:'#86C98B', w:2, kind:'segment', lens:true },
];
const segDef = k => SEGMENTS.find(s => s.key === k);

const FLOORPLAN_INIT = [
  // walls forming the cockpit set
  { id:'s1', kind:'segment', type:'wall', x1:14, y1:10, x2:86, y2:10 },
  { id:'s2', kind:'segment', type:'wall', x1:86, y1:10, x2:86, y2:74 },
  { id:'s3', kind:'segment', type:'wall', x1:14, y1:10, x2:14, y2:74 },
  // moves
  { id:'s4', kind:'segment', type:'cammove',  x1:50, y1:92, x2:50, y2:64, label:'Dolly in' },
  { id:'s5', kind:'segment', type:'actormove', x1:38, y1:46, x2:62, y2:46, label:'Hero cross' },
  { id:'s6', kind:'segment', type:'blackflag', x1:16, y1:30, x2:28, y2:54 },
  // annotations
  { id:'n1', kind:'note', x:25, y:13, text:'F21X rigged on autopoles', tone:'yellow' },
  // fixtures
  { id:1, kind:'fixture', type:'camera', x:50, y:84, rot:-90 },
  { id:2, kind:'fixture', type:'dolly',  x:50, y:92, rot:0 },
  { id:3, kind:'fixture', type:'mark',   x:50, y:46, rot:0 },
  { id:4, kind:'fixture', type:'hmi',    x:22, y:22, rot:38, label:'300C' },
  { id:5, kind:'fixture', type:'led',    x:78, y:24, rot:142, label:'720B · 4x4 + Grid' },
  { id:6, kind:'fixture', type:'china',  x:50, y:16, rot:0 },
  { id:7, kind:'fixture', type:'flag',   x:33, y:60, rot:0 },
  { id:8, kind:'fixture', type:'kino',   x:70, y:60, rot:205 },
];

/* ---------- Call sheet — locations, hospital, crew & cast call ---------- */
const LOCATIONS = [
  { no:1, name:'Backlot Stage 4', kind:'Cockpit Build', address:'5800 Aviation Blvd, Bay 4, Los Angeles CA', ll:[33.9785,-118.3782], map:'https://maps.google.com/?q=5800+Aviation+Blvd', parking:'Lot C · shuttle every 10m' },
  { no:2, name:'Runway B', kind:'Tarmac · Hero Walk', address:'Whiteman Airpark, Pacoima CA', ll:[34.2593,-118.4134], map:'https://maps.google.com/?q=Whiteman+Airpark', parking:'East apron · drive-on pass' },
  { no:3, name:'Hangar 7', kind:'Reveal · Practical', address:'12200 Airway Rd, Pacoima CA', ll:[34.2558,-118.4170], map:'https://maps.google.com/?q=12200+Airway+Rd', parking:'Adjacent gravel lot' },
];
const HOSPITAL = { name:'Cedars Aero Medical Center', address:'4900 Sunset Blvd, Los Angeles CA', phone:'+1 323 555 0911', distance:'4.2 mi · 11 min', map:'https://maps.google.com/?q=Cedars+Aero+Medical' };
const CREW_CALL = [
  { dept:'Camera',   name:'Idris Vance',   role:'DP',          call:'06:30' },
  { dept:'Camera',   name:'Mia Sorenson',  role:'1st AC',      call:'06:30' },
  { dept:'Lighting', name:'Theo Nkemelu',  role:'Gaffer',      call:'06:00' },
  { dept:'Grip',     name:'Russ Calderon', role:'Key Grip',    call:'06:00' },
  { dept:'Sound',    name:'Pia Lindqvist', role:'Mixer',       call:'07:00' },
  { dept:'Art',      name:'Ravi Anand',    role:'Designer',    call:'05:30' },
  { dept:'MUA',      name:'Coraline Bisset',role:'Key MUA',    call:'05:45' },
  { dept:'Wardrobe', name:'Juno Park',     role:'Costume',     call:'05:45' },
  { dept:'Prod',     name:'Suki Tanaka',   role:'1st AD',      call:'05:45' },
  { dept:'SFX',       name:'Gunnar Holt',    role:'SFX Supervisor',    call:'06:00' },
  { dept:'Transport', name:'Rosa Delgado',   role:'Transport Captain', call:'05:30' },
  { dept:'VFX',       name:'Kenji Watanabe', role:'VFX Supervisor',    call:'08:00' },
];
const CAST_CALL = [
  { no:1, actor:'Lena Frost',   character:'The Aviator', pickup:'05:10', mu:'06:00', set:'07:00', status:'SWF' },
  { no:2, actor:'Dorian Wells', character:'Ground Chief', pickup:'07:30', mu:'08:00', set:'09:15', status:'SW' },
  { no:3, actor:'Aria Bloom',   character:'Co-Pilot',     pickup:'07:30', mu:'08:10', set:'09:15', status:'W' },
];

/* ---------- Art department pre-buy / purchasing ---------- */
const ART_BUDGET = 18000;
const PURCHASE_SETS = [
  { set:'Cockpit Build — Stage 4', location:'Stage 4', items:[
    { id:'p1', name:'Vintage altimeter (hero prop)', cat:'Props',        qty:1,  price:480, status:'purchased', vendor:'AeroSalvage' },
    { id:'p2', name:'Brushed-steel switch panels',   cat:'Set Dressing', qty:3,  price:135, status:'approved',  vendor:'SetPiece Co.' },
    { id:'p3', name:'Leather seat reupholster',      cat:'Construction', qty:2,  price:620, status:'approved',  vendor:'Hide & Co.' },
    { id:'p4', name:'Instrument bezel LEDs',         cat:'Practical FX', qty:12, price:14,  status:'suggested', vendor:'Voltflux' },
  ] },
  { set:'Hangar 7 — Reveal', location:'Hangar 7', items:[
    { id:'p5', name:'Industrial floor fans (atmos)', cat:'Set Dressing', qty:4,  price:210, status:'approved',  vendor:'WindWorks' },
    { id:'p6', name:'Hazard tape & floor decals',    cat:'Set Dressing', qty:1,  price:90,  status:'purchased', vendor:'MarkIt' },
    { id:'p7', name:'Vintage tool chest dressing',   cat:'Props',        qty:2,  price:340, status:'suggested', vendor:'AeroSalvage' },
  ] },
  { set:'Tarmac — Hero Walk', location:'Runway B', items:[
    { id:'p8', name:'Runway marker cones (period)',  cat:'Props',        qty:20, price:18,  status:'suggested', vendor:'MarkIt' },
    { id:'p9', name:'Fuel cart dressing (non-prac)', cat:'Set Dressing', qty:1,  price:750, status:'suggested', vendor:'SetPiece Co.' },
  ] },
];

const NAV2 = [
  { id:'crewdb',    label:'Crew Directory',    sub:'Everyone, every ability', icon:'BookUser' },
  { id:'crewlog',   label:'Crew Logistics',    sub:'Rates, memos & call times', icon:'ReceiptText' },
  { id:'messages',  label:'Messages',          sub:'Crew channels & broadcast', icon:'MessagesSquare' },
  { id:'gear',      label:'Gear & Prep',       sub:'Rental, prep & return days', icon:'PackageOpen' },
  { id:'casting',   label:'Casting',           sub:'Roles & auditions', icon:'Drama' },
  { id:'location',  label:'Location Library',  sub:'Scouting, permits & contacts', icon:'MapPinned' },
  { id:'access',    label:'Department Access', sub:'Phone access & permissions', icon:'KeyRound' },
  { id:'floorplan', label:'Floor Plan',        sub:'Gaffer · lighting & camera', icon:'Lightbulb' },
  { id:'purchase',  label:'Purchasing',        sub:'Art dept · pre-buy & costs', icon:'ReceiptText' },
];

/* ---- blank project: export empty data so the app starts from zero ---- */
const IS_BLANK = !!window.PROJ_BLANK;
const E = a => IS_BLANK ? [] : a;

/* Real production meta: pull the name/type/client the user set from the GLOBAL
   owned-productions registry and MUTATE the `PROJECT` const in place. Every bare
   `PROJECT.x` reference across the app (print headers, ProjectSwitcher, topbar
   breadcrumb, crew phone…) resolves to this same object through the shared
   script scope, so mutating it is what surfaces the real production name.
   Crimson (the demo) keeps its hardcoded const untouched. */
function slResolveProjectMeta() {
  const base = { name: 'Untitled Production', type: 'New project', code: 'UNT-0001', client: '—', status: 'In Development', day: '—' };
  if (!IS_BLANK) return null;
  try {
    const list = window.SLStore.get('silverlines.projects.v1', []);
    const rec = list.find(p => p && p.pid === window.PROJ_ID);
    if (rec) return {
      name: rec.name || base.name,
      type: rec.type || base.type,
      code: rec.codeTag || base.code,
      client: rec.client || base.client,
      status: rec.status || base.status,
      day: base.day,
    };
  } catch (e) {}
  return base;
}
if (IS_BLANK) Object.assign(PROJECT, slResolveProjectMeta());
const PROJECT_OUT = PROJECT; // always the same (now-mutated) object
window.projectMetaRefresh = function () {
  if (!IS_BLANK) return;
  Object.assign(PROJECT, slResolveProjectMeta());
  try { window.dispatchEvent(new CustomEvent('sl-project')); } catch (e) {}
};

const HOSPITAL_OUT = IS_BLANK
  ? { name: '—', address: 'Set when locations lock', phone: '—', distance: '—', map: '#' }
  : HOSPITAL;

/* ============================================================
   SPOTS — store, active-spot filter, and shared UI
   A project carries many spots; scenes are tagged with spot id.
   ============================================================ */
const SPOTS_KEY = 'silverlines.spots.v1';
let __spots = null; const __spotsSubs = new Set();
function spotsLoad() {
  if (__spots) return __spots;
  const s = window.SLStore.get(SPOTS_KEY); if (Array.isArray(s) && s.length) { __spots = s; return s; }
  __spots = IS_BLANK ? [] : SPOTS.map(s => ({ ...s }));
  return __spots;
}
function spotsSave(next) {
  __spots = typeof next === 'function' ? next(spotsLoad()) : next;
  window.SLStore.set(SPOTS_KEY, __spots);
  __spotsSubs.forEach(fn => fn());
}
function useSpots() {
  const [, force] = useState(0);
  useEffect(() => { const fn = () => force(n => n + 1); __spotsSubs.add(fn); return () => __spotsSubs.delete(fn); }, []);
  return [spotsLoad(), spotsSave];
}
const spotById = id => spotsLoad().find(s => s.id === id) || null;

/* active-spot filter — persisted, shared across every scene-based module */
const ACTIVE_SPOT_KEY = 'silverlines.activespot';
let __activeSpot = null; const __asSubs = new Set();
function activeSpotLoad() {
  if (__activeSpot != null) return __activeSpot;
  try { __activeSpot = localStorage.getItem(ACTIVE_SPOT_KEY) || 'all'; } catch (e) { __activeSpot = 'all'; }
  return __activeSpot;
}
function setActiveSpot(v) {
  __activeSpot = v;
  try { localStorage.setItem(ACTIVE_SPOT_KEY, v); } catch (e) {}
  __asSubs.forEach(fn => fn());
}
function useActiveSpot() {
  const [, force] = useState(0);
  useEffect(() => { const fn = () => force(n => n + 1); __asSubs.add(fn); return () => __asSubs.delete(fn); }, []);
  return [activeSpotLoad(), setActiveSpot];
}
const sceneSpot = s => {
  if (s.spot) return s.spot;
  /* DEFAULT_SPOT only maps the demo's scene numbers — don't apply it to real
     productions (a real scene numbered "14" must not inherit the demo's 'hero') */
  if (!window.PROJ_BLANK && DEFAULT_SPOT[s.no]) return DEFAULT_SPOT[s.no];
  /* fall back to the first ACTUAL spot, never a literal 'hero' that matches none
     of a real production's spot ids (which would hide the scene from every filter) */
  const sp = spotsLoad();
  return (sp[0] && sp[0].id) || 'hero';
};
const inActiveSpot = (s, active) => active === 'all' || sceneSpot(s) === active;
const openAsks = s => (s.asks || []).filter(a => a.status !== 'delivered').length;

/* ---- small spot pill used by the SpotBar filter ---- */
function SpotPill({ active, onClick, color, code, name, type, count, asks, icon }) {
  return (
    <button onClick={onClick}
      className={cx('group inline-flex items-center gap-2 rounded-xl border px-2.5 py-1.5 text-left transition-colors',
        active ? 'bg-ink-800' : 'border-line bg-ink-900 hover:border-ink-600')}
      style={active ? { borderColor: color, boxShadow: `inset 0 0 0 1px ${color}` } : undefined}>
      <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg font-display text-[12px] font-700"
        style={{ background: color + '24', color }}>
        {icon ? <Icon name={icon} size={13} /> : code}
      </span>
      <span className="min-w-0 leading-tight">
        <span className="flex items-center gap-1.5">
          <span className={cx('whitespace-nowrap text-[12.5px] font-700', active ? 'text-head' : 'text-zinc-300')}>{name}</span>
          {type === 'stills' && <Icon name="Camera" size={11} className="text-zinc-500" />}
        </span>
        <span className="font-mono text-[9.5px] text-zinc-500">
          {count} {type === 'stills' ? 'look' : 'scene'}{count === 1 ? '' : 's'}{asks ? ` · ${asks} ask${asks === 1 ? '' : 's'}` : ''}
        </span>
      </span>
    </button>
  );
}

/* ---- SpotBar: the filter strip that makes a module spot-aware ---- */
function SpotBar({ scenes = [], showAll = true, className = '' }) {
  const [spots] = useSpots();
  const [active, setActive] = useActiveSpot();
  if (!spots.length) return null;
  const cnt = id => scenes.filter(s => sceneSpot(s) === id).length;
  const asks = id => scenes.filter(s => sceneSpot(s) === id).reduce((a, s) => a + openAsks(s), 0);
  return (
    <div className={cx('no-print mb-4 flex flex-wrap items-center gap-2', className)}>
      <span className="mr-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500">Spot</span>
      {showAll && <SpotPill active={active === 'all'} onClick={() => setActive('all')} color="#9aa0aa" icon="LayoutGrid"
        name="All spots" count={scenes.length} asks={scenes.reduce((a, s) => a + openAsks(s), 0)} />}
      {spots.map(sp => <SpotPill key={sp.id} active={active === sp.id} onClick={() => setActive(sp.id)}
        color={sp.color} code={sp.code} name={sp.name} type={sp.type} count={cnt(sp.id)} asks={asks(sp.id)} />)}
    </div>
  );
}

/* ---- compact spot badge for scene rows / strips / call sheets ---- */
function SpotBadge({ spotId, dot = true, className = '' }) {
  const [spots] = useSpots();
  const sp = spots.find(s => s.id === spotId) || (spots.length ? spots[0] : null);
  if (!sp) return null;
  return (
    <span className={cx('inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-[9.5px] font-700', className)}
      style={{ background: sp.color + '1f', color: sp.color }} title={sp.name + ' · ' + sp.format}>
      {dot && <span className="h-1.5 w-1.5 rounded-full" style={{ background: sp.color }} />}
      {sp.code}{sp.type === 'stills' ? ' · Stills' : ''}
    </span>
  );
}

/* ---- source tag for a request/ask ---- */
function AskSourceTag({ source }) {
  const m = ASK_SOURCES[source] || { color: '#9aa0aa', icon: 'User' };
  return (
    <span className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-700"
      style={{ background: m.color + '1f', color: m.color }}>
      <Icon name={m.icon} size={10} />{source}
    </span>
  );
}

/* ---- real QR code (encoder lives in qr.js); honest placeholder if unavailable ---- */
function QrCode({ value, size = 180, className = '' }) {
  const mat = (window.SLQr && value) ? window.SLQr.matrix(value) : null;
  if (!mat) {
    return (
      <div className={cx('flex items-center justify-center rounded-xl bg-white', className)} style={{ width: size, height: size }}>
        <span className="px-4 text-center text-[11px] font-600 leading-snug text-zinc-500">QR unavailable — use the link below</span>
      </div>
    );
  }
  const n = mat.length, quiet = 4, dim = n + quiet * 2;
  let d = '';
  for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (mat[y][x]) d += 'M' + (x + quiet) + ' ' + (y + quiet) + 'h1v1h-1z';
  return (
    <svg width={size} height={size} viewBox={'0 0 ' + dim + ' ' + dim} shapeRendering="crispEdges"
      className={cx('block rounded-xl', className)} role="img" aria-label="QR code" style={{ background: '#fff' }}>
      <path d={d} fill="#06080F" />
    </svg>
  );
}

/* expose globally */
Object.assign(window, {
  SPOTS: E(SPOTS), DEFAULT_SPOT, useSpots, spotsSave, spotById, useActiveSpot, setActiveSpot,
  sceneSpot, inActiveSpot, openAsks, SpotPill, SpotBar, SpotBadge, AskSourceTag,
  ASK_SOURCES, ASK_STATUS, ASK_STATUS_META,
  Icon, cx, ProFlowPill, Switch, Chip, SectionTitle, Panel, Avatar, PhotoSlot, Edit, money, EmptyState, UndoToasts, QrCode,
  CURRENCIES, curCode, setCurrency,
  AccessContext, ACCESS: E(ACCESS), FIXTURES, FIXTURE_CATS, SEGMENTS, segDef, FLOORPLAN_INIT: E(FLOORPLAN_INIT), ART_BUDGET, PURCHASE_SETS: E(PURCHASE_SETS), NAV2,
  LOCATIONS: E(LOCATIONS), HOSPITAL: HOSPITAL_OUT, CREW_CALL: E(CREW_CALL), CAST_CALL: E(CAST_CALL), DropImport, parseCSV, pick,
  PROJECT: PROJECT_OUT, PEOPLE: E(PEOPLE), SCENES: E(SCENES), SHOTS: E(SHOTS), UPLOADS: E(UPLOADS), MOOD: E(MOOD), REVIEW_COMMENTS: E(REVIEW_COMMENTS), NAV,
  useState, useEffect, useRef, useMemo, useContext, createContext
});
