
/* ===== mod_develop ===== */
/* ============================================================
   MODULE 1 — Development & Briefing
   Concept Brief Builder + Moodboard Canvas
   ============================================================ */
function BriefField({ label, icon, children, auto }) {
  return (
    <div>
      <div className="mb-2 flex items-center gap-2">
        <Icon name={icon} size={14} className="text-zinc-500" />
        <label className="text-[12px] font-600 uppercase tracking-[0.12em] text-zinc-400">{label}</label>
        {auto && <ProFlowPill label="From kickoff" tip="Pulled from the kickoff call transcript" />}
      </div>
      {children}
    </div>
  );
}

function ObjectiveRow({ text, done, onToggle, onEdit, onDelete }) {
  return (
    <div className="group flex w-full items-center gap-3 rounded-lg border border-line bg-ink-900 px-3 py-2.5 transition-colors hover:border-ink-600">
      <button onClick={onToggle}
        className={cx('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[6px] border transition-colors',
          done ? 'border-mint bg-mint text-ink-950' : 'border-zinc-600 text-transparent hover:border-zinc-400')}>
        <Icon name="Check" size={12} sw={3} />
      </button>
      <span className="min-w-0 flex-1">
        <Edit value={text} onChange={onEdit} className={cx('text-[13.5px]', done ? 'text-zinc-500 line-through' : 'text-zinc-200')} />
      </span>
      <button onClick={onDelete} title="Remove objective"
        className="shrink-0 text-zinc-600 opacity-0 transition-opacity hover:text-[#FF4D6D] group-hover:opacity-100">
        <Icon name="Trash2" size={13} />
      </button>
    </div>
  );
}

/* ---- the full commercial brief — everything the client throws at you ---- */
const BRIEF_DEFAULTS = {
  vision: 'A cinematic 60-second flagship spot positioning Aether Aviation as the pinnacle of private flight — crimson dawn light, brushed-steel surfaces, and one fearless pilot. Restraint over spectacle; every frame should feel like a held breath.',
  message: 'Flight, distilled. One pilot, one dawn — the quietest way to cross the sky.',
  audience: 'Charter decision-makers 35–60 and the planners who book for them. They skip ads — the spot has to feel like cinema, not media.',
  tone: ['Cinematic', 'Restrained', 'Tactile', 'Premium'],
  budget: 85000, currency: '$', days: 4, budgetFull: true,
  objectives: [
    { text: 'Establish "Crimson Skies" as a premium brand world', done: true },
    { text: 'Deliver hero :60, :30 cutdown, and 6× social verticals', done: true },
    { text: 'Capture real practical flare — minimal VFX augmentation', done: false },
    { text: 'Lock principal photography to a 4-day window', done: false },
  ],
  deliverables: [
    { name: 'Hero film', dur: ':60', ratio: '16:9', ch: 'TV · Cinema', status: 'approved' },
    { name: 'Cutdown', dur: ':30', ratio: '16:9', ch: 'TV · Pre-roll', status: 'client' },
    { name: 'Social verticals', dur: ':15 × 6', ratio: '9:16', ch: 'IG · TikTok', status: 'draft' },
    { name: 'Stills package', dur: '12 frames', ratio: '4:5 · 1:1', ch: 'OOH · Print', status: 'draft' },
  ],
  mandatories: [
    { text: 'Aether logo lockup on end card — hold ≥ 2 seconds', done: true },
    { text: 'Legal super: "Professional pilot. Closed runway."', done: true },
    { text: 'Brand crimson in every key frame of the grade', done: false },
    { text: 'No competitor aircraft or liveries in frame', done: false },
  ],
  usage: { territory: 'North America + EU', term: '18 months', channels: ['TV', 'Cinema', 'Online', 'Social'] },
  approvals: [
    { name: 'Marco Reyes', role: 'Client Lead · Aether', color: '#FFB020', status: 'signed' },
    { name: 'Dahlia Mercer', role: 'Director', color: '#F57F45', status: 'review' },
    { name: 'Noor Haddad', role: 'Producer', color: '#22D48A', status: 'signed' },
  ],
};
if (window.PROJ_BLANK) Object.assign(BRIEF_DEFAULTS, {
  vision: '', message: '', audience: '', tone: [], budget: 0, currency: '$', days: 1,
  objectives: [], deliverables: [], mandatories: [],
  usage: { territory: '', term: '', channels: [] }, approvals: [],
});

/* ---- deliverable status: click to advance ---- */
const DELIV_STATUS = {
  draft:    { label: 'Drafting',    tone: 'neutral', next: 'client' },
  client:   { label: 'With client', tone: 'amber',   next: 'approved' },
  approved: { label: 'Approved',    tone: 'mint',    next: 'draft' },
};
function DeliverableRow({ d, onCycle, onEdit, onDelete, mobile }) {
  const st = DELIV_STATUS[d.status] || DELIV_STATUS.draft;
  return (
    <div className="group flex items-center gap-3 rounded-lg border border-line bg-ink-900 px-3 py-2.5 transition-colors hover:border-ink-600">
      <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-zinc-400"><Icon name="Clapperboard" size={14} /></span>
      <div className="min-w-0 flex-1">
        <Edit value={d.name} onChange={v => onEdit({ name: v })} className="text-[13px] font-600 text-zinc-200" />
        <div className="mt-0.5 flex flex-wrap items-center gap-x-2 font-mono text-[10.5px] text-zinc-500">
          <Edit value={d.dur} onChange={v => onEdit({ dur: v })} />
          <span className="text-zinc-700">·</span>
          <Edit value={d.ratio} onChange={v => onEdit({ ratio: v })} />
          {!mobile && <span className="text-zinc-700">·</span>}
          {!mobile && <Edit value={d.ch} onChange={v => onEdit({ ch: v })} />}
        </div>
      </div>
      <button onClick={onCycle} title="Click to advance status" className="shrink-0"><Chip tone={st.tone}>{st.label}</Chip></button>
      <button onClick={onDelete} title="Remove deliverable"
        className="shrink-0 text-zinc-600 opacity-0 transition-opacity hover:text-[#FF4D6D] group-hover:opacity-100">
        <Icon name="Trash2" size={13} />
      </button>
    </div>
  );
}

/* ---- sign-off chain ---- */
const SIGN_STATUS = {
  pending: { label: 'Pending',    tone: 'neutral', next: 'review' },
  review:  { label: 'In review',  tone: 'amber',   next: 'signed' },
  signed:  { label: 'Signed off', tone: 'mint',    next: 'pending' },
};
function ApprovalRow({ a, onCycle }) {
  const st = SIGN_STATUS[a.status] || SIGN_STATUS.pending;
  return (
    <div className="flex items-center gap-2.5 rounded-lg border border-line bg-ink-900 px-3 py-2.5">
      <Avatar name={a.name} color={a.color} size={28} />
      <div className="min-w-0 flex-1">
        <div className="truncate text-[12.5px] font-600 text-zinc-200">{a.name}</div>
        <div className="truncate text-[11px] text-zinc-500">{a.role}</div>
      </div>
      <button onClick={onCycle} title="Click to advance" className="shrink-0"><Chip tone={st.tone}>{st.label}</Chip></button>
    </div>
  );
}

/* ---- removable keyword chips + adder ---- */
function KeywordChips({ items, onChange, placeholder = 'Add…' }) {
  const [v, setV] = useState('');
  const add = () => { const t = v.trim(); if (!t || items.includes(t)) { setV(''); return; } onChange([...items, t]); setV(''); };
  return (
    <div className="flex flex-wrap items-center gap-1.5">
      {items.map(t => (
        <span key={t} className="inline-flex items-center gap-1.5 rounded-full border border-line bg-ink-800 px-2.5 py-1 text-[11.5px] font-600 text-zinc-300">
          {t}
          <button onClick={() => onChange(items.filter(x => x !== t))} className="text-zinc-600 transition-colors hover:text-[#FF4D6D]"><Icon name="X" size={10} sw={3} /></button>
        </span>
      ))}
      <span className="inline-flex items-center gap-1 rounded-full border border-dashed border-ink-600 px-2.5 py-1 transition-colors focus-within:border-electric/50">
        <Icon name="Plus" size={10} className="text-zinc-500" />
        <input value={v} onChange={e => setV(e.target.value)} onKeyDown={e => e.key === 'Enter' && add()} onBlur={add}
          placeholder={placeholder} className="w-[70px] bg-transparent text-[11.5px] text-zinc-200 outline-none placeholder:text-zinc-600" />
      </span>
    </div>
  );
}

const USAGE_CHANNELS = ['TV', 'Cinema', 'Online', 'Social', 'OOH'];

function DevelopModule({ mobile }) {
  const [brief, setBriefState] = useState(() => {
    try {
      const raw = window.SLStore.get('silverlines.brief.v1', {});
      /* migrate legacy budgets stored in thousands (K) to full amounts, once */
      if (raw && !raw.budgetFull && typeof raw.budget === 'number' && raw.budget > 0 && raw.budget <= 2000) raw.budget *= 1000;
      return { ...BRIEF_DEFAULTS, ...raw };
    }
    catch (e) { return { ...BRIEF_DEFAULTS }; }
  });
  const setBrief = p => setBriefState(o => {
    const n = { ...o, ...p };
    window.SLStore.set('silverlines.brief.v1', n);
    return n;
  });
  const { vision, message, audience, tone, budget, currency, days, objectives, deliverables, mandatories, usage, approvals } = brief;
  const [newObj, setNewObj] = useState('');
  const [newMand, setNewMand] = useState('');
  const toggleObj = i => setBrief({ objectives: objectives.map((x, k) => k === i ? { ...x, done: !x.done } : x) });
  const editObj = (i, text) => setBrief({ objectives: objectives.map((x, k) => k === i ? { ...x, text } : x) });
  const delObj = i => setBrief({ objectives: objectives.filter((_, k) => k !== i) });
  const addObj = () => { const t = newObj.trim(); if (!t) return; setBrief({ objectives: [...objectives, { text: t, done: false }] }); setNewObj(''); };
  const fmtD = d => d.toLocaleDateString(slLocale(), { month: 'short', day: 'numeric' });
  const dateRange = window.PROJ_BLANK ? 'Dates TBD' : `${fmtD(new Date(2026, 5, 22))} → ${fmtD(new Date(2026, 5, 22 + Math.max(0, days - 1)))}, 2026`;

  /* deliverables / mandatories / usage / sign-off handlers */
  const patchDeliv = (i, p) => setBrief({ deliverables: deliverables.map((x, k) => k === i ? { ...x, ...p } : x) });
  const cycleDeliv = i => patchDeliv(i, { status: (DELIV_STATUS[deliverables[i].status] || DELIV_STATUS.draft).next });
  const delDeliv = i => setBrief({ deliverables: deliverables.filter((_, k) => k !== i) });
  const addDeliv = () => setBrief({ deliverables: [...deliverables, { name: 'New deliverable', dur: ':15', ratio: '16:9', ch: 'Online', status: 'draft' }] });
  const toggleMand = i => setBrief({ mandatories: mandatories.map((x, k) => k === i ? { ...x, done: !x.done } : x) });
  const editMand = (i, text) => setBrief({ mandatories: mandatories.map((x, k) => k === i ? { ...x, text } : x) });
  const delMand = i => setBrief({ mandatories: mandatories.filter((_, k) => k !== i) });
  const addMand = () => { const t = newMand.trim(); if (!t) return; setBrief({ mandatories: [...mandatories, { text: t, done: false }] }); setNewMand(''); };
  const cycleSign = i => setBrief({ approvals: approvals.map((x, k) => k === i ? { ...x, status: (SIGN_STATUS[x.status] || SIGN_STATUS.pending).next } : x) });
  const setUsage = p => setBrief({ usage: { ...usage, ...p } });
  const toggleChannel = c => setUsage({ channels: usage.channels.includes(c) ? usage.channels.filter(x => x !== c) : [...usage.channels, c] });

  const { subtabs, setSubtab } = useContext(AccessContext);
  const [sbLive] = useStoryboard();
  const unschedCount = sbLive.filter(s => sceneDay(s) === 0).length;
  const tab = (subtabs && subtabs.develop) || 'brief';
  const tabs = [
    { id: 'brief', label: 'Concept Brief', icon: 'FileText' },
    { id: 'mood', label: 'Moodboard', icon: 'Images' },
  ];
  const approvedDeliv = deliverables.filter(d => d.status === 'approved').length;

  return (
    <div className="fade-up" data-screen-label={tab === 'mood' ? 'Development · Moodboard' : 'Development · Concept Brief'}>
      {tab !== 'mood' && <SectionTitle kicker="Phase 01 · Development" icon="PencilRuler" title="Concept Brief"
        action={<Chip tone="proflow"><Icon name="Sparkles" size={12} /> ProFlow synced</Chip>} />}

      {/* tab bar */}
      <div className="mb-5 inline-flex rounded-xl border border-line bg-ink-850 p-1">
        {tabs.map(t => (
          <button key={t.id} onClick={() => setSubtab('develop', t.id)}
            className={cx('flex items-center gap-2 rounded-lg px-3.5 py-2 font-display text-[12.5px] transition-colors',
              tab === t.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
            <Icon name={t.icon} size={14} />{!mobile && t.label}
          </button>
        ))}
      </div>

      {tab === 'brief' && (
      <div className={cx('grid items-start gap-5', mobile ? 'grid-cols-1' : 'grid-cols-12')}>
        {/* ---- left: the brief itself ---- */}
        <div className={cx('space-y-5', mobile ? '' : 'col-span-7')}>
          <Panel>
            <div className="mb-5 flex items-center justify-between">
              <h3 className="font-display text-[15px] font-600 text-head">Concept Brief Builder</h3>
              <span className="font-mono text-[11px] text-zinc-500">{PROJECT.code}</span>
            </div>
            <div className="space-y-5">
              <BriefField label="Project Vision" icon="Eye">
                <textarea value={vision} onChange={e => setBrief({ vision: e.target.value })} rows={mobile ? 6 : 4}
                  className="w-full resize-none rounded-xl border border-line bg-ink-900 p-3 text-[13.5px] leading-relaxed text-zinc-200 outline-none transition-colors focus:border-electric/60" />
              </BriefField>

              <BriefField label="Key Message" icon="Quote">
                <div className="rounded-xl border border-line bg-ink-900 px-3.5 py-3">
                  <Edit value={message} onChange={v => setBrief({ message: v })} multiline
                    placeholder="The one line the audience must keep…" className="font-display text-[16px] leading-snug text-head" />
                </div>
              </BriefField>

              <BriefField label="Objectives" icon="Target">
                <div className="space-y-2">
                  {objectives.map((o, i) => (
                    <ObjectiveRow key={i} {...o} onToggle={() => toggleObj(i)} onEdit={v => editObj(i, v)} onDelete={() => delObj(i)} />
                  ))}
                  <div className="flex items-center gap-2.5 rounded-lg border border-dashed border-ink-600 px-3 py-2.5 transition-colors focus-within:border-electric/50">
                    <Icon name="Plus" size={14} className="shrink-0 text-zinc-500" />
                    <input value={newObj} onChange={e => setNewObj(e.target.value)} onKeyDown={e => e.key === 'Enter' && addObj()}
                      placeholder="Add an objective…" className="min-w-0 flex-1 bg-transparent text-[13px] text-zinc-200 outline-none placeholder:text-zinc-600" />
                    {newObj.trim() && <button onClick={addObj} className="shrink-0 text-[11.5px] font-700 text-electric-soft hover:text-electric">Add</button>}
                  </div>
                </div>
              </BriefField>
            </div>
          </Panel>

          <Panel>
            <div className="mb-4 flex items-center justify-between">
              <h3 className="font-display text-[15px] font-600 text-head">Deliverables</h3>
              <Chip tone={approvedDeliv === deliverables.length && deliverables.length > 0 ? 'mint' : 'neutral'}>{approvedDeliv}/{deliverables.length} approved</Chip>
            </div>
            <div className="space-y-2">
              {deliverables.map((d, i) => (
                <DeliverableRow key={i} d={d} mobile={mobile} onCycle={() => cycleDeliv(i)} onEdit={p => patchDeliv(i, p)} onDelete={() => delDeliv(i)} />
              ))}
              <button onClick={addDeliv} className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-ink-600 px-3 py-2.5 text-[12.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
                <Icon name="Plus" size={14} /> Add deliverable
              </button>
            </div>
            <p className="mt-2.5 text-[11px] text-zinc-600">Click a status to advance it — Drafting → With client → Approved.</p>
          </Panel>

          <Panel>
            <div className="mb-4 flex items-center gap-2">
              <h3 className="font-display text-[15px] font-600 text-head">Client materials</h3>
              <ProFlowPill label="Auto-filed" tip="ProFlow files drops into the project media database" />
            </div>
            <DropImport compact label="Drop what the client sent" hint="Brand book, scripts, legal supers, past campaigns — anything"
              sample={() => [{}, {}, {}]} onRows={() => window.toast && window.toast('Filed to project materials', 'FolderCheck')} />
          </Panel>
        </div>

        {/* ---- right: parameters & guardrails ---- */}
        <div className={cx('space-y-5', mobile ? '' : 'col-span-5')}>
          <Panel>
            <BriefField label="Timeline" icon="CalendarRange" auto>
              <div className="rounded-xl border border-line bg-ink-900 px-3 py-2.5">
                  <div className="flex items-center justify-between gap-2">
                    <div className="font-display text-[22px] font-600 leading-none text-head">{days} {days === 1 ? 'day' : 'days'}</div>
                    <div className="flex items-center gap-1">
                      <button onClick={() => setBrief({ days: Math.max(1, days - 1) })}
                        className="flex h-6 w-6 items-center justify-center rounded-md border border-line text-zinc-400 transition-colors hover:text-head"><Icon name="Minus" size={12} /></button>
                      <button onClick={() => setBrief({ days: Math.min(60, days + 1) })}
                        className="flex h-6 w-6 items-center justify-center rounded-md border border-line text-zinc-400 transition-colors hover:text-head"><Icon name="Plus" size={12} /></button>
                    </div>
                  </div>
                <div className="mt-1 font-mono text-[11px] text-zinc-500">{dateRange}</div>
                <div className="mt-2 flex flex-wrap gap-1">
                  {Array.from({ length: shootDayCount(sbLive) }, (_, i) => i + 1).map(n => {
                    const c = sbLive.filter(s => sceneDay(s) === n).length;
                    return <span key={n} title={`Day ${n} · ${c} scene${c === 1 ? '' : 's'}`} className={cx('inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-[10px] font-700', c ? 'bg-electric/15 text-electric-soft' : 'bg-ink-800 text-zinc-600')}>D{n}<span className={c ? 'text-zinc-400' : 'text-zinc-700'}>{c}</span></span>;
                  })}
                  {unschedCount > 0 && <span title="Unscheduled scenes" className="inline-flex items-center gap-1 rounded-md bg-amber/15 px-1.5 py-0.5 font-mono text-[10px] font-700 text-amber"><Icon name="CalendarOff" size={10} />{unschedCount}</span>}
                </div>
              </div>
            </BriefField>
          </Panel>

          <Panel>
            <BriefField label="Target Budget" icon="CircleDollarSign">
              <div className="rounded-xl border border-line bg-ink-900 px-3 py-2.5">
                  <div className="flex items-center justify-between gap-2">
                    <div className="flex items-baseline gap-1.5 font-display text-[22px] font-600 leading-none text-head">
                      {currency !== 'CHF' && <span className="text-zinc-400">{currency}</span>}
                      <input type="text" inputMode="numeric" value={budget ? budget.toLocaleString('en-US') : ''}
                        onChange={e => setBrief({ budget: Math.max(0, parseInt(e.target.value.replace(/[^\d]/g, '')) || 0) })}
                        placeholder="0"
                        style={{ width: `${Math.max(4, (budget ? budget.toLocaleString('en-US').length : 1))}ch` }}
                        className={cx('bg-transparent font-display text-[22px] font-600 leading-none outline-none', budget ? 'text-head' : 'text-zinc-500')} />
                      {currency === 'CHF' && <span className="text-[14px] text-zinc-400">CHF</span>}
                    </div>
                    <div className="flex items-center rounded-md border border-line p-0.5">
                      {['$', '€', '£', 'CHF'].map(c => (
                        <button key={c} onClick={() => setBrief({ currency: c })}
                          className={cx('rounded px-1.5 py-0.5 text-[11px] font-700 transition-colors', currency === c ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>{c}</button>
                      ))}
                    </div>
                  </div>
                <div className="mt-1.5 font-mono text-[11px] text-zinc-500">Type the full amount — e.g. 85,000</div>
              </div>
            </BriefField>
          </Panel>

          <Panel>
            <div className="space-y-5">
              <BriefField label="Audience" icon="Users" auto>
                <textarea value={audience} onChange={e => setBrief({ audience: e.target.value })} rows={3}
                  className="w-full resize-none rounded-xl border border-line bg-ink-900 p-3 text-[13px] leading-relaxed text-zinc-200 outline-none transition-colors focus:border-electric/60" />
              </BriefField>
              <BriefField label="Tone" icon="Palette">
                <KeywordChips items={tone} onChange={v => setBrief({ tone: v })} placeholder="Add tone…" />
              </BriefField>
            </div>
          </Panel>

          <Panel>
            <div className="mb-4 flex items-center justify-between">
              <h3 className="font-display text-[15px] font-600 text-head">Mandatories</h3>
              <Chip tone={mandatories.every(m => m.done) && mandatories.length > 0 ? 'mint' : 'neutral'}><Icon name="ShieldCheck" size={12} /> {mandatories.filter(m => m.done).length}/{mandatories.length}</Chip>
            </div>
            <div className="space-y-2">
              {mandatories.map((m, i) => (
                <ObjectiveRow key={i} {...m} onToggle={() => toggleMand(i)} onEdit={v => editMand(i, v)} onDelete={() => delMand(i)} />
              ))}
              <div className="flex items-center gap-2.5 rounded-lg border border-dashed border-ink-600 px-3 py-2.5 transition-colors focus-within:border-electric/50">
                <Icon name="Plus" size={14} className="shrink-0 text-zinc-500" />
                <input value={newMand} onChange={e => setNewMand(e.target.value)} onKeyDown={e => e.key === 'Enter' && addMand()}
                  placeholder="Add a mandatory…" className="min-w-0 flex-1 bg-transparent text-[13px] text-zinc-200 outline-none placeholder:text-zinc-600" />
                {newMand.trim() && <button onClick={addMand} className="shrink-0 text-[11.5px] font-700 text-electric-soft hover:text-electric">Add</button>}
              </div>
            </div>
          </Panel>

          <Panel>
            <h3 className="mb-4 font-display text-[15px] font-600 text-head">Usage &amp; rights</h3>
            <div className="grid grid-cols-2 gap-3">
              <div className="rounded-xl border border-line bg-ink-900 px-3 py-2.5">
                <div className="text-[10.5px] font-600 uppercase tracking-[0.12em] text-zinc-500">Territory</div>
                <div className="mt-1"><Edit value={usage.territory} onChange={v => setUsage({ territory: v })} className="text-[13px] font-600 text-zinc-200" /></div>
              </div>
              <div className="rounded-xl border border-line bg-ink-900 px-3 py-2.5">
                <div className="text-[10.5px] font-600 uppercase tracking-[0.12em] text-zinc-500">Term</div>
                <div className="mt-1"><Edit value={usage.term} onChange={v => setUsage({ term: v })} className="text-[13px] font-600 text-zinc-200" /></div>
              </div>
            </div>
            <div className="mt-3 flex flex-wrap gap-1.5">
              {USAGE_CHANNELS.map(c => {
                const on = usage.channels.includes(c);
                return (
                  <button key={c} onClick={() => toggleChannel(c)}
                    className={cx('inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11.5px] font-600 transition-colors',
                      on ? 'border-electric/40 bg-electric/12 text-electric-soft' : 'border-line bg-ink-800 text-zinc-500 hover:text-zinc-300')}>
                    {on && <Icon name="Check" size={10} sw={3} />}{c}
                  </button>
                );
              })}
            </div>
            <p className="mt-2.5 text-[11px] text-zinc-600">Talent &amp; music are licensed for the channels above — extend before re-airing.</p>
          </Panel>

          <Panel>
            <div className="mb-4 flex items-center justify-between">
              <h3 className="font-display text-[15px] font-600 text-head">Sign-off</h3>
              <Chip tone={approvals.every(a => a.status === 'signed') ? 'mint' : 'amber'}>{approvals.filter(a => a.status === 'signed').length}/{approvals.length} signed</Chip>
            </div>
            <div className="space-y-2">
              {approvals.map((a, i) => <ApprovalRow key={a.name} a={a} onCycle={() => cycleSign(i)} />)}
            </div>
          </Panel>
        </div>
      </div>
      )}

      {tab === 'mood' && <MoodLab mobile={mobile} wide={!mobile} />}
    </div>
  );
}

window.DevelopModule = DevelopModule;


/* ===== mod_storage ===== */
/* ============================================================
   MODULE 2 — Storage
   1) Send documents & media online (secure links)
   2) Safety backups — verified 1:1 mirrors to external drives
      + the Silverlines Server. No mistakes: checksum-verified.
   ============================================================ */

/* ---------- shared row for the send queue ---------- */
function UploadRow({ u, live }) {
  const done = u.pct >= 100;
  return (
    <div className="rounded-xl border border-line bg-ink-900 p-3.5">
      <div className="flex items-center gap-3">
        <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style={{ background:`${u.color}1f`, color:u.color }}>
          <Icon name={done ? 'CircleCheck' : 'Film'} size={17} />
        </span>
        <div className="min-w-0 flex-1">
          <div className="flex items-center justify-between gap-2">
            <span className="truncate text-[13.5px] font-600 text-zinc-100">{u.name}</span>
            <span className="shrink-0 whitespace-nowrap font-mono text-[11px] text-zinc-500">{u.size}</span>
          </div>
          <div className="flex items-center gap-2 overflow-hidden whitespace-nowrap text-[11px] text-zinc-500">
            <span className="font-mono">{u.kind}</span>
            <span className="text-zinc-700">·</span>
            <span className={done ? 'text-mint' : ''}>{done ? 'Verified' : `${u.speed}`}</span>
          </div>
        </div>
      </div>
      <div className="mt-2.5 flex items-center gap-3">
        <div className="relative h-1.5 flex-1 overflow-hidden rounded-full bg-ink-700">
          {!done && live < 100 && <div className="absolute inset-0 scan-anim rounded-full" />}
          <div className="relative h-full rounded-full transition-[width] duration-300"
            style={{ width:`${live}%`, background:done ? '#22D48A' : u.color }} />
        </div>
        <span className="w-10 text-right font-mono text-[11px] font-600" style={{ color:done ? '#22D48A' : u.color }}>{live}%</span>
      </div>
    </div>
  );
}

/* ============================================================
   TAB 1 — SEND & SHARE (documents & media online)
   ============================================================ */
function SendSharePanel({ mobile }) {
  const [drag, setDrag] = useState(false);
  const [progress, setProgress] = useState(UPLOADS.map(u => Math.max(0, u.pct - 10)));
  const targets = useRef(UPLOADS.map(u => u.pct));

  useEffect(() => {
    const id = setInterval(() => {
      setProgress(p => p.map((v, i) => {
        const tgt = targets.current[i];
        if (v >= tgt) return v;
        return Math.min(tgt, v + Math.random() * 3.2);
      }));
    }, 380);
    return () => clearInterval(id);
  }, []);

  const totalPct = Math.round(progress.reduce((a, b) => a + b, 0) / progress.length);
  const activeCount = progress.filter((v, i) => v < targets.current[i]).length;

  // link sharing
  const [allowDl, setAllowDl] = useState(true);
  const [pw, setPw] = useState(true);
  const [expiry, setExpiry] = useState(false);
  const [copied, setCopied] = useState(false);
  const [code, setCode] = useState('AX7-K2M-9QF');
  const buildLink = () => buildShareURL({ v: 1, type: 'files', exp: expiry ? expiryMs() : null, pass: pw ? code : null,
    payload: { kicker: 'Secure transfer', title: 'Crimson Skies — Day 1 Rushes', subtitle: 'Shared from Silverlines Storage', from: 'Noor Haddad',
      allowDl, totalText: '520.7 GB', files: (typeof UPLOADS !== 'undefined' ? UPLOADS : []).map(u => ({ name: u.name, sizeText: u.size, kind: u.kind })) } });
  const link = buildLink();
  const [showCode, setShowCode] = useState(false);
  const [sent, setSent] = useState(false);
  const regen = () => {
    const c = Array.from({ length: 3 }, () => Math.random().toString(36).slice(2, 5).toUpperCase()).join('-');
    setCode(c); setShowCode(true); setTimeout(() => setShowCode(false), 2200);
    window.toast('New passcode generated', 'KeyRound');
  };
  const sendLink = () => {
    if (sent) return;
    try { navigator.clipboard && navigator.clipboard.writeText(buildLink()); } catch (e) {}
    setSent(true); window.toast('Secure link copied — ready to send', 'Send');
    setTimeout(() => setSent(false), 2600);
  };
  const copy = () => { try { navigator.clipboard && navigator.clipboard.writeText(buildLink()); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1600); window.toast && window.toast('Share link copied', 'Link2'); };

  return (
    <div className={cx('fade-up grid gap-5', mobile ? 'grid-cols-1' : 'grid-cols-12')}>
      {/* Upload center */}
      <div className={cx('space-y-5', mobile ? '' : 'col-span-7')}>
        {/* dropzone + overview */}
        <Panel>
          <div className="mb-4 flex items-center justify-between">
            <h3 className="font-display text-[15px] font-600 text-head">Upload Overview</h3>
            <Chip tone={activeCount ? 'amber' : 'mint'}>
              <span className={cx('h-1.5 w-1.5 rounded-full', activeCount ? 'bg-amber animate-pulse' : 'bg-mint')} />
              {activeCount ? `${activeCount} transferring` : 'All synced'}
            </Chip>
          </div>

          {/* big aggregate bar */}
          <div className="mb-4 rounded-xl border border-line bg-gradient-to-br from-ink-900 to-ink-850 p-4">
            <div className="flex items-end justify-between">
              <div>
                <div className="whitespace-nowrap text-[11px] uppercase tracking-[0.14em] text-zinc-500">Total this session</div>
                <div className="font-display text-[32px] font-700 leading-none text-head">{totalPct}%</div>
              </div>
              <div className="text-right">
                <div className="font-mono text-[13px] text-zinc-300">520.7 GB</div>
                <div className="font-mono text-[11px] text-zinc-500">~ 4m 12s left</div>
              </div>
            </div>
            <div className="mt-3 h-2 overflow-hidden rounded-full bg-ink-700">
              <div className="h-full rounded-full bg-electric transition-[width] duration-500" style={{ width:`${totalPct}%` }} />
            </div>
          </div>

          {/* dropzone */}
          <div
            onDragOver={e => { e.preventDefault(); setDrag(true); }}
            onDragLeave={() => setDrag(false)}
            onDrop={e => { e.preventDefault(); setDrag(false); }}
            className={cx('flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed py-7 text-center transition-colors',
              drag ? 'border-electric bg-electric/8' : 'border-ink-600 hover:border-zinc-500')}>
            <span className="flex h-11 w-11 items-center justify-center rounded-xl bg-electric/12 text-electric-soft">
              <Icon name="UploadCloud" size={22} />
            </span>
            <div className="text-[13.5px] font-600 text-zinc-200">Drop documents, rushes & casting tapes</div>
            <div className="text-[12px] text-zinc-500">Accelerated multi-stream · up to 2 TB per asset</div>
          </div>
        </Panel>

        {/* queue */}
        <Panel>
          <h3 className="mb-3 font-display text-[15px] font-600 text-head">Active Queue</h3>
          <div className="space-y-2.5">
            {UPLOADS.map((u, i) => <UploadRow key={u.name} u={u} live={Math.round(progress[i])} />)}
          </div>
        </Panel>
      </div>

      {/* Secure link sharing */}
      <div className={cx(mobile ? '' : 'col-span-5')}>
        <Panel className={mobile ? '' : 'sticky top-5'}>
          <div className="mb-1 flex items-center gap-2">
            <Icon name="Link2" size={16} className="text-electric-soft" />
            <h3 className="font-display text-[15px] font-600 text-head">Secure Link Sharing</h3>
          </div>
          <p className="mb-4 text-[12.5px] text-zinc-500">Controlled access to <span className="text-zinc-300">Rushes — Day 1 A-Cam</span></p>

          {/* link box */}
          <div className="flex items-center gap-2 rounded-xl border border-line bg-ink-900 p-2 pl-3">
            <Icon name="Globe" size={14} className="shrink-0 text-zinc-500" />
            <span className="flex-1 truncate font-mono text-[12px] text-zinc-300">{link}</span>
            <button onClick={copy}
              className={cx('flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-600 transition-colors',
                copied ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
              <Icon name={copied ? 'Check' : 'Copy'} size={13} />{copied ? 'Copied' : 'Copy'}
            </button>
          </div>

          {/* toggles */}
          <div className="mt-4 space-y-1">
            {[
              { k:'dl', icon:'Download', label:'Allow downloads', sub:'Recipients can pull originals', on:allowDl, set:setAllowDl, acc:'electric' },
              { k:'pw', icon:'KeyRound', label:'Password protect', sub:'Require a passphrase to open', on:pw, set:setPw, acc:'electric' },
              { k:'ex', icon:'Clock', label:'Expiry date', sub:expiry ? 'Expires Jun 14, 2026' : 'Link never expires', on:expiry, set:setExpiry, acc:'electric' },
            ].map(r => (
              <div key={r.k} className="flex items-center gap-3 rounded-xl px-1 py-2.5">
                <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-zinc-400">
                  <Icon name={r.icon} size={16} />
                </span>
                <div className="flex-1">
                  <div className="text-[13.5px] font-600 text-zinc-100">{r.label}</div>
                  <div className="text-[11.5px] text-zinc-500">{r.sub}</div>
                </div>
                <Switch on={r.on} onChange={r.set} />
              </div>
            ))}
          </div>

          {pw && (
            <div className="fade-up mt-1 flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-3 py-2.5">
              <Icon name="Lock" size={13} className="text-zinc-500" />
              <span className={cx('flex-1 font-mono text-[14px]', showCode ? 'tracking-[0.1em] text-head' : 'tracking-[0.3em] text-zinc-300')}>{showCode ? code : '••••••••'}</span>
              <button onClick={regen} className="text-[11px] font-600 text-electric-soft hover:text-electric">Regenerate</button>
            </div>
          )}

          <button onClick={sendLink} className={cx('mt-4 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-[13.5px] font-700 transition-colors',
            sent ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
            <Icon name={sent ? 'Check' : 'Send'} size={15} /> {sent ? 'Link sent' : 'Send secure link'}
          </button>
        </Panel>
      </div>
    </div>
  );
}

/* ============================================================
   TAB 2 — 1:1 BACKUP (external drives + Silverlines Server)
   ============================================================ */
const BK_DRIVES = [
  { id:'graid',  name:'G-RAID Mirror 18TB',  bus:'Thunderbolt 4',     total:18000, free:11200, color:'#FFB020', cloud:false },
  { id:'lacie',  name:'LaCie Rugged 4TB',    bus:'USB-C · 10 Gb/s',   total:4000,  free:3120,  color:'#F57F45', cloud:false },
  { id:'t7',     name:'Samsung T7 2TB',      bus:'USB-C · 10 Gb/s',   total:2000,  free:240,   color:'#BD2555', cloud:false },
  { id:'server', name:'Silverlines Server',  bus:'Cloud · EU-West 1', total:8000,  free:5400,  color:'#22D48A', cloud:true },
];

const BK_SOURCES = [
  { id:'acam',  name:'A-Cam Card 0412',     size:248.5, files:412,  kind:'ProRes 4444 · 6K', icon:'Film',        color:'#F57F45' },
  { id:'bcam',  name:'B-Cam Card 0413',     size:191.2, files:388,  kind:'ProRes 4444 · 6K', icon:'Film',        color:'#F57F45' },
  { id:'sound', name:'Sound — Day 1',       size:4.2,   files:96,   kind:'WAV · 96kHz',      icon:'AudioLines',  color:'#FFB020' },
  { id:'docs',  name:'Production Documents',size:1.1,   files:214,  kind:'PDF · Sheets',     icon:'FileText',    color:'#22D48A' },
];

const bkSize = gb => gb >= 1000 ? (gb / 1000).toFixed(1) + ' TB' : (gb % 1 ? gb.toFixed(1) : gb) + ' GB';

const BK_HISTORY_KEY = 'silverlines.backups.v1';
const bkReadHistory = () => window.SLStore.get(BK_HISTORY_KEY, []);

/* ---- one destination drive row ---- */
function BkDriveRow({ d, inSet, onToggle, ejected, onEject, needGB, jobState }) {
  const used = d.total - d.free;
  const tooSmall = !ejected && inSet === false && d.free < needGB && needGB > 0;
  const cantFit = d.free < needGB && needGB > 0;
  return (
    <div className={cx('rounded-xl border p-3.5 transition-colors',
      ejected ? 'border-line bg-ink-900/40 opacity-50' : inSet ? 'border-electric/35 bg-electric/5' : 'border-line bg-ink-900')}>
      <div className="flex items-center gap-3">
        <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style={{ background:`${d.color}1f`, color:d.color }}>
          <Icon name={d.cloud ? 'Cloud' : 'HardDrive'} size={17} />
        </span>
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2">
            <span className="truncate text-[13.5px] font-600 text-zinc-100">{d.name}</span>
            {d.cloud
              ? <span className="flex items-center gap-1 rounded-full border border-mint/30 bg-mint/10 px-1.5 py-[2px] text-[9.5px] font-700 text-mint"><span className="h-1 w-1 rounded-full bg-mint proflow-pulse" />ONLINE</span>
              : ejected
                ? <span className="rounded-full border border-line bg-ink-800 px-1.5 py-[2px] text-[9.5px] font-700 text-zinc-500">EJECTED</span>
                : <span className="rounded-full border border-line bg-ink-800 px-1.5 py-[2px] text-[9.5px] font-700 text-zinc-400">MOUNTED</span>}
          </div>
          <div className="flex items-center gap-2 whitespace-nowrap text-[11px] text-zinc-500">
            <span className="font-mono">{d.bus}</span>
            <span className="text-zinc-700">·</span>
            <span className={cantFit && !ejected ? 'font-600 text-[#FF4D6D]' : ''}>{bkSize(d.free)} free of {bkSize(d.total)}</span>
          </div>
        </div>
        {!d.cloud && !ejected && (
          <button onClick={onEject} title="Eject safely" className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-line bg-ink-800 text-zinc-500 transition-colors hover:text-zinc-200">
            <Icon name="CircleArrowUp" size={15} />
          </button>
        )}
        {ejected
          ? <button onClick={onEject} className="shrink-0 rounded-lg border border-line bg-ink-800 px-2.5 py-1.5 text-[11px] font-600 text-zinc-400 hover:text-zinc-200">Remount</button>
          : <Switch on={inSet} onChange={onToggle} />}
      </div>
      {/* capacity bar */}
      <div className="mt-2.5 h-1.5 overflow-hidden rounded-full bg-ink-700">
        <div className="h-full rounded-full" style={{ width:`${(used / d.total) * 100}%`, background:d.color }} />
      </div>
      {cantFit && !ejected && inSet && (
        <div className="mt-2 flex items-center gap-1.5 text-[11px] font-600 text-[#FF4D6D]">
          <Icon name="TriangleAlert" size={12} /> Not enough space for this backup set — free {bkSize(needGB - d.free)} or deselect
        </div>
      )}
      {/* live job progress on this destination */}
      {jobState && (
        <div className="fade-up mt-2.5">
          <div className="flex items-center justify-between text-[11px]">
            <span className={cx('font-600', jobState.verify >= 100 ? 'text-mint' : jobState.copy >= 100 ? 'text-electric-soft' : 'text-zinc-400')}>
              {jobState.verify >= 100 ? '1:1 verified — checksums match'
                : jobState.copy >= 100 ? 'Verifying checksums (xxHash64)…' : 'Copying…'}
            </span>
            <span className="font-mono text-zinc-500">{jobState.copy >= 100 ? Math.round(jobState.verify) : Math.round(jobState.copy)}%</span>
          </div>
          <div className="relative mt-1.5 h-1.5 overflow-hidden rounded-full bg-ink-700">
            {jobState.verify < 100 && <div className="absolute inset-0 scan-anim rounded-full" />}
            <div className="relative h-full rounded-full transition-[width] duration-300"
              style={{ width:`${jobState.copy >= 100 ? jobState.verify : jobState.copy}%`, background: jobState.verify >= 100 ? '#22D48A' : jobState.copy >= 100 ? '#22D48A' : d.color, opacity: jobState.copy >= 100 && jobState.verify < 100 ? 0.7 : 1 }} />
          </div>
        </div>
      )}
    </div>
  );
}

/* ---- one source row (camera card / folder) ---- */
function BkSourceRow({ s, on, onToggle, disabled }) {
  return (
    <button onClick={onToggle} disabled={disabled}
      className={cx('flex w-full items-center gap-3 rounded-xl border p-3 text-left transition-colors',
        on ? 'border-electric/35 bg-electric/5' : 'border-line bg-ink-900 hover:border-ink-600', disabled && 'cursor-not-allowed opacity-60')}>
      <span className={cx('flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition-colors',
        on ? 'border-electric bg-electric text-white' : 'border-ink-600 bg-ink-800 text-transparent')}>
        <Icon name="Check" size={12} sw={3.5} />
      </span>
      <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg" style={{ background:`${s.color}1f`, color:s.color }}>
        <Icon name={s.icon} size={15} />
      </span>
      <div className="min-w-0 flex-1">
        <div className="truncate text-[13px] font-600 text-zinc-100">{s.name}</div>
        <div className="whitespace-nowrap font-mono text-[10.5px] text-zinc-500">{s.kind} · {s.files.toLocaleString()} files</div>
      </div>
      <span className="shrink-0 font-mono text-[11.5px] text-zinc-400">{bkSize(s.size)}</span>
    </button>
  );
}

function BackupVaultPanel({ mobile }) {
  /* drive set + eject state */
  const [inSet, setInSet] = useState({ graid:true, lacie:true, t7:false, server:true });
  const [ejected, setEjected] = useState({});
  const [scanning, setScanning] = useState(false);

  /* source selection */
  const [srcOn, setSrcOn] = useState({ acam:true, bcam:true, sound:true, docs:false });

  /* job + history */
  const [job, setJob] = useState(null);
  const [history, setHistory] = useState(bkReadHistory);
  const saveHistory = list => { setHistory(list); window.SLStore.set(BK_HISTORY_KEY, list.slice(0, 8)); };

  const sources = BK_SOURCES.filter(s => srcOn[s.id]);
  const needGB = sources.reduce((a, s) => a + s.size, 0);
  const needFiles = sources.reduce((a, s) => a + s.files, 0);
  const dests = BK_DRIVES.filter(d => inSet[d.id] && !ejected[d.id]);
  const blocked = dests.filter(d => d.free < needGB);
  const running = job && job.phase !== 'done';
  const canStart = !running && sources.length > 0 && dests.length > 0 && blocked.length === 0;

  /* safety meter */
  const copies = dests.length;
  const hasCloud = dests.some(d => d.cloud);
  const safety = copies === 0 ? { label:'No destinations', tone:'text-zinc-500', icon:'CircleOff' }
    : copies === 1 ? { label:'Single copy — no redundancy', tone:'text-amber', icon:'TriangleAlert' }
    : copies === 2 && !hasCloud ? { label:'2 copies — add cloud for off-site', tone:'text-amber', icon:'ShieldHalf' }
    : copies === 2 ? { label:'2 copies incl. off-site', tone:'text-mint', icon:'ShieldCheck' }
    : hasCloud ? { label:'3-2-1 · production safe', tone:'text-mint', icon:'ShieldCheck' }
    : { label:`${copies} local copies — add cloud for off-site`, tone:'text-mint', icon:'ShieldCheck' };

  /* job engine: copy phase → verify phase → done */
  useEffect(() => {
    if (!job || job.phase === 'done') return;
    const id = setInterval(() => {
      setJob(j => {
        if (!j || j.phase === 'done') return j;
        const d = { ...j.dests };
        let all = true;
        Object.keys(d).forEach(k => {
          const e = { ...d[k] };
          const sp = (e.cloud ? 1.1 + Math.random() * 1.5 : 2.4 + Math.random() * 2.8);
          if (j.phase === 'copy') { e.copy = Math.min(100, e.copy + sp); if (e.copy < 100) all = false; }
          else { e.verify = Math.min(100, e.verify + sp * 1.7); if (e.verify < 100) all = false; }
          d[k] = e;
        });
        if (all && j.phase === 'copy') return { ...j, dests:d, phase:'verify' };
        if (all && j.phase === 'verify') return { ...j, dests:d, phase:'done' };
        return { ...j, dests:d };
      });
    }, 320);
    return () => clearInterval(id);
  }, [job && job.phase]);

  /* record finished job once */
  useEffect(() => {
    if (job && job.phase === 'done' && !job.logged) {
      setJob(j => ({ ...j, logged:true }));
      saveHistory([{ id:Date.now(), ts:Date.now(), src:job.srcNames, size:job.size, files:job.files, n:job.n }, ...history]);
      window.toast(`1:1 backup verified on ${job.n} destination${job.n > 1 ? 's' : ''}`, 'ShieldCheck');
      window.notify && window.notify(`Backup verified — ${job.srcNames} → ${job.n} destinations`, 'DatabaseBackup', 'storage');
    }
  }, [job]);

  const start = () => {
    if (!canStart) return;
    const dmap = {};
    dests.forEach(d => { dmap[d.id] = { copy:0, verify:0, cloud:d.cloud }; });
    setJob({ phase:'copy', dests:dmap, n:dests.length, size:needGB, files:needFiles,
      srcNames: sources.map(s => s.name).join(', ') });
    window.toast(mobile ? 'Backup started on STAGE-MAC-01' : 'Backup started — copying to ' + dests.length + ' destinations', 'HardDriveDownload');
  };

  const rescan = () => {
    setScanning(true);
    setTimeout(() => { setScanning(false); setEjected({}); window.toast('4 destinations detected', 'HardDrive'); }, 1100);
  };

  const eject = id => {
    setEjected(o => {
      const next = { ...o, [id]: !o[id] };
      if (next[id]) window.toast('Safe to disconnect — drive ejected', 'CircleArrowUp');
      return next;
    });
    setInSet(o => ({ ...o, [id]: false }));
  };

  const ago = ts => {
    const m = Math.round((Date.now() - ts) / 60000);
    if (m < 1) return 'just now';
    if (m < 60) return m + ' min ago';
    const h = Math.round(m / 60);
    return h < 24 ? h + ' hr ago' : Math.round(h / 24) + ' d ago';
  };

  return (
    <div className={cx('fade-up grid gap-5', mobile ? 'grid-cols-1' : 'grid-cols-12')}>
      {/* LEFT — destinations + sources */}
      <div className={cx('space-y-5', mobile ? '' : 'col-span-7')}>
        {/* phone monitoring banner */}
        {mobile && (
          <div className="flex items-center gap-2.5 rounded-xl border border-electric/25 bg-electric/8 px-3 py-2.5">
            <Icon name="MonitorSmartphone" size={15} className="shrink-0 text-electric-soft" />
            <div className="text-[11.5px] text-zinc-400">Drives are connected to <span className="font-700 text-zinc-200">STAGE-MAC-01</span> — you're monitoring live and can start backups remotely.</div>
          </div>
        )}

        {/* destinations */}
        <Panel>
          <div className="mb-4 flex items-center justify-between gap-3">
            <div>
              <h3 className="font-display text-[15px] font-600 text-head">Backup Destinations</h3>
              <p className="mt-0.5 text-[11.5px] text-zinc-500">External drives on {mobile ? 'STAGE-MAC-01' : 'this machine'} + the Silverlines Server</p>
            </div>
            <button onClick={rescan}
              className="flex shrink-0 items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[12px] font-600 text-zinc-300 transition-colors hover:text-zinc-100">
              <Icon name="RefreshCw" size={13} className={scanning ? 'animate-spin text-electric-soft' : ''} />{scanning ? 'Scanning…' : 'Rescan'}
            </button>
          </div>
          <div className="space-y-2.5">
            {BK_DRIVES.map(d => (
              <BkDriveRow key={d.id} d={d}
                inSet={!!inSet[d.id]} ejected={!!ejected[d.id]} needGB={needGB}
                onToggle={v => !running && setInSet(o => ({ ...o, [d.id]: v }))}
                onEject={() => !running && eject(d.id)}
                jobState={job && job.dests[d.id]} />
            ))}
          </div>
          <div className="mt-3.5 flex items-center gap-2 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[11px] text-zinc-500">
            <Icon name="ShieldCheck" size={12} className="shrink-0 text-mint" />
            External drive access granted to the Silverlines {mobile ? 'desktop app on STAGE-MAC-01' : 'desktop app'} · every copy is checksum-verified before it counts
          </div>
        </Panel>

        {/* sources */}
        <Panel>
          <div className="mb-3.5 flex items-center justify-between">
            <h3 className="font-display text-[15px] font-600 text-head">Backup Set</h3>
            <span className="font-mono text-[11.5px] text-zinc-500">{sources.length ? `${bkSize(needGB)} · ${needFiles.toLocaleString()} files` : 'Nothing selected'}</span>
          </div>
          <div className="space-y-2">
            {BK_SOURCES.map(s => (
              <BkSourceRow key={s.id} s={s} on={!!srcOn[s.id]} disabled={running}
                onToggle={() => setSrcOn(o => ({ ...o, [s.id]: !o[s.id] }))} />
            ))}
          </div>
        </Panel>
      </div>

      {/* RIGHT — mirror job + history */}
      <div className={cx(mobile ? '' : 'col-span-5')}>
        <div className={cx('space-y-5', mobile ? '' : 'sticky top-5')}>
          <Panel>
            <div className="mb-1 flex items-center gap-2">
              <Icon name="DatabaseBackup" size={16} className="text-electric-soft" />
              <h3 className="font-display text-[15px] font-600 text-head">1:1 Mirror Job</h3>
            </div>
            <p className="mb-4 text-[12.5px] text-zinc-500">Identical, verified copies — every file, every byte.</p>

            {/* summary */}
            <div className="rounded-xl border border-line bg-gradient-to-br from-ink-900 to-ink-850 p-4">
              <div className="flex items-center justify-center gap-3 text-center">
                <div className="min-w-0">
                  <div className="font-display text-[24px] font-700 leading-none text-head">{sources.length}</div>
                  <div className="mt-1 text-[10px] uppercase tracking-[0.14em] text-zinc-500">source{sources.length === 1 ? '' : 's'}</div>
                </div>
                <Icon name="ArrowRight" size={16} className="shrink-0 text-zinc-600" />
                <div className="min-w-0">
                  <div className="font-display text-[24px] font-700 leading-none text-head">{dests.length}</div>
                  <div className="mt-1 text-[10px] uppercase tracking-[0.14em] text-zinc-500">destination{dests.length === 1 ? '' : 's'}</div>
                </div>
                <Icon name="ArrowRight" size={16} className="shrink-0 text-zinc-600" />
                <div className="min-w-0">
                  <div className="font-display text-[24px] font-700 leading-none text-head">{bkSize(needGB * Math.max(1, dests.length))}</div>
                  <div className="mt-1 text-[10px] uppercase tracking-[0.14em] text-zinc-500">written total</div>
                </div>
              </div>
              {/* safety meter */}
              <div className={cx('mt-3.5 flex items-center justify-center gap-1.5 border-t border-line pt-3 text-[12px] font-600', safety.tone)}>
                <Icon name={safety.icon} size={14} />{safety.label}
              </div>
            </div>

            {blocked.length > 0 && (
              <div className="fade-up mt-3 flex items-center gap-2 rounded-xl border border-[#FF4D6D]/30 bg-[#FF4D6D]/8 px-3 py-2.5 text-[12px] font-600 text-[#FF4D6D]">
                <Icon name="TriangleAlert" size={14} className="shrink-0" />
                {blocked.map(d => d.name).join(', ')} can't fit this set — backup is blocked to prevent a partial copy
              </div>
            )}

            {/* status / CTA */}
            {!job || job.phase === 'done' ? (
              <>
                {job && job.phase === 'done' && (
                  <div className="fade-up mt-3 rounded-xl border border-mint/30 bg-mint/8 p-3.5">
                    <div className="flex items-center gap-2 text-[13px] font-700 text-mint">
                      <Icon name="ShieldCheck" size={15} /> 1:1 verified — no mismatches
                    </div>
                    <div className="mt-1 text-[11.5px] text-zinc-400">{job.files.toLocaleString()} files · {bkSize(job.size)} · checksums match on all {job.n} destinations</div>
                    <div className="mt-2.5 flex items-center gap-2">
                      <button onClick={() => window.toast('MHL report saved to Production Documents', 'FileCheck')}
                        className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-1.5 text-[11.5px] font-600 text-zinc-300 hover:text-zinc-100">
                        <Icon name="FileCheck" size={12} /> MHL report
                      </button>
                      <button onClick={() => setJob(null)}
                        className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-1.5 text-[11.5px] font-600 text-zinc-300 hover:text-zinc-100">
                        <Icon name="Check" size={12} /> Clear
                      </button>
                    </div>
                  </div>
                )}
                <button onClick={start} disabled={!canStart}
                  className={cx('mt-4 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-[13.5px] font-700 transition-colors',
                    canStart ? 'bg-cta text-cta-ink hover:bg-cta-hover' : 'cursor-not-allowed bg-ink-700 text-zinc-500')}>
                  <Icon name="HardDriveDownload" size={15} />
                  {mobile ? 'Start backup on STAGE-MAC-01' : 'Start 1:1 backup'}
                </button>
              </>
            ) : (
              <div className="fade-up mt-4 rounded-xl border border-line bg-ink-900 p-3.5">
                <div className="flex items-center gap-2 text-[13px] font-700 text-zinc-100">
                  <span className="h-2 w-2 rounded-full bg-amber animate-pulse" />
                  {job.phase === 'copy' ? 'Copying to all destinations…' : 'Verifying checksums — nothing counts until this passes'}
                </div>
                <div className="mt-1 text-[11.5px] text-zinc-500">{job.files.toLocaleString()} files · {bkSize(job.size)} per destination · progress shown on each drive</div>
              </div>
            )}
          </Panel>

          {/* verified history */}
          <Panel>
            <div className="mb-3 flex items-center justify-between">
              <h3 className="font-display text-[15px] font-600 text-head">Verified Backups</h3>
              {history.length > 0 && <span className="font-mono text-[11px] text-zinc-500">{history.length} run{history.length > 1 ? 's' : ''}</span>}
            </div>
            {history.length === 0 ? (
              <div className="flex flex-col items-center gap-2 rounded-xl border border-dashed border-ink-600 py-6 text-center">
                <Icon name="DatabaseBackup" size={20} className="text-zinc-600" />
                <div className="text-[12px] text-zinc-500">No verified backups yet — your first run will be logged here</div>
              </div>
            ) : (
              <div className="space-y-2">
                {history.map(h => (
                  <div key={h.id} className="flex items-center gap-3 rounded-xl border border-line bg-ink-900 px-3 py-2.5">
                    <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-mint/12 text-mint"><Icon name="ShieldCheck" size={15} /></span>
                    <div className="min-w-0 flex-1">
                      <div className="truncate text-[12.5px] font-600 text-zinc-100">{h.src}</div>
                      <div className="whitespace-nowrap font-mono text-[10.5px] text-zinc-500">{bkSize(h.size)} · {h.files.toLocaleString()} files · {h.n}× mirror · {ago(h.ts)}</div>
                    </div>
                    <Chip tone="mint" className="shrink-0">Verified</Chip>
                  </div>
                ))}
              </div>
            )}
          </Panel>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   STORAGE MODULE — tab switcher
   ============================================================ */
function StorageModule({ mobile }) {
  const [tab, setTabState] = useState(() => { try { return localStorage.getItem('silverlines.storage.tab') || 'backup'; } catch (e) { return 'backup'; } });
  const setTab = t => { setTabState(t); try { localStorage.setItem('silverlines.storage.tab', t); } catch (e) {} };

  return (
    <div className="fade-up">
      <SectionTitle kicker="Tools · Storage" icon="HardDriveUpload"
        title={tab === 'send' ? 'Send & Secure Sharing' : tab === 'vault' ? 'Local Vault — Real Storage' : 'Backup Vault — 1:1 Mirrors'}
        action={
          <div className="flex items-center gap-2.5">
            {!mobile && <Chip tone="electric"><Icon name="HardDrive" size={12} /> Stored on your devices</Chip>}
            <div className="flex items-center rounded-lg border border-line bg-ink-900 p-0.5">
              {[['backup','DatabaseBackup','1:1 Backup'],['vault','HardDrive','Local Vault'],['send','Send','Send & Share']].map(([v, ic, lbl]) => (
                <button key={v} onClick={() => setTab(v)}
                  className={cx('flex items-center gap-1.5 whitespace-nowrap rounded-[7px] px-2.5 py-1.5 text-[12px] font-600 transition-colors',
                    tab === v ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>
                  <Icon name={ic} size={13} />{lbl}
                </button>
              ))}
            </div>
          </div>
        } />
      {tab === 'send' ? <SendSharePanel mobile={mobile} /> : tab === 'vault' ? <window.RealVaultPanel mobile={mobile} /> : <BackupVaultPanel mobile={mobile} />}
    </div>
  );
}

window.StorageModule = StorageModule;


/* ===== mod_preprod ===== */
/* ============================================================
   MODULE 3 — Pre-Production & ProFlow Automation
   Storyboard Multiplayer · Automated Shot List · Cast & Crew
   ============================================================ */
/* ---- pinned edits: persisted per scene/shot, anchored by its number ---- */
function usePinnedEdits(storeKey, defaults, idOf) {
  const [over, setOver] = useState(() => window.SLStore.get(storeKey, {}));
  useEffect(() => { window.SLStore.set(storeKey, over); }, [over]);
  const patch = (id, p) => setOver(o => ({ ...o, [id]: { ...(o[id] || {}), ...p } }));
  const rows = defaults.map(d => ({ ...d, ...(over[idOf(d)] || {}), _edited: !!over[idOf(d)] }));
  return [rows, patch];
}

/* card tone palette — dark scene tints from the brand scene colors */
const SCENE_TONES = ['#232040', '#3A1238', '#401A2C', '#1F2A3D', '#16323A', '#233625', '#3A2E1C', '#262B38'];

function StoryboardCard({ s, mobile, patch }) {
  const [tones, setTones] = useState(false);
  return (
    <div className="group fade-up overflow-hidden rounded-xl border border-line bg-ink-900 transition-colors hover:border-ink-600">
      <div className="relative border-b border-line" style={{ height: mobile ? 120 : 150, background: s.color }}>
        <image-slot id={`sb-${s.no}`} placeholder={`SC ${s.no} — drop a frame`} shape="rect"
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', color: 'rgba(255,255,255,.6)' }}></image-slot>
        {/* pages badge (editable) */}
        <span className="absolute right-2 top-2 z-10 rounded-md bg-black/60 px-1.5 py-1 font-mono text-[10px] font-600 text-white backdrop-blur-sm">
          <Edit value={s.pages} onChange={v => patch(s.no, { pages: v })} className="font-mono text-[10px]" />
        </span>
        {/* scene tag + time of day (editable) */}
        <span className="absolute bottom-2 left-2 z-10 flex items-center gap-1 whitespace-nowrap rounded-md bg-black/55 px-1.5 py-1 font-mono text-[10px] tracking-wide text-zinc-200 backdrop-blur-sm">
          SC {s.no} · <Edit value={s.tod} onChange={v => patch(s.no, { tod: v })} className="font-mono text-[10px]" />
        </span>
        {/* card tone (adjustable, pinned to this scene) */}
        <div className="absolute left-2 top-2 z-10 flex items-center gap-1">
          <button onClick={() => setTones(t => !t)} title="Card tone"
            className={cx('flex h-6 w-6 items-center justify-center rounded-md bg-black/55 text-zinc-300 backdrop-blur-sm transition-opacity hover:text-white',
              tones ? 'opacity-100' : 'opacity-0 group-hover:opacity-100')}>
            <Icon name="Palette" size={13} />
          </button>
          {tones && SCENE_TONES.map(c => (
            <button key={c} onClick={() => { patch(s.no, { color: c }); setTones(false); }}
              className={cx('h-5 w-5 rounded-md border transition-transform hover:scale-110',
                s.color === c ? 'border-white' : 'border-white/25')} style={{ background: c }}></button>
          ))}
        </div>
      </div>
      <div className="p-3">
        <div className="flex items-center gap-2">
          <span className="shrink-0 font-mono text-[11px] font-600 text-electric-soft">{s.no}</span>
          <Edit value={s.title} onChange={v => patch(s.no, { title: v })} className="min-w-0 flex-1 truncate text-[13px] font-600 text-zinc-100" />
        </div>
        <div className="mt-1 flex items-center gap-1.5 text-[11px] text-zinc-500">
          <Icon name="MapPin" size={11} className="shrink-0" />
          <Edit value={s.loc} onChange={v => patch(s.no, { loc: v })} className="min-w-0 flex-1 truncate text-[11px]" />
        </div>
        <div className="mt-2.5 flex items-center justify-between">
          <div className="flex -space-x-1.5">
            {s.cast.slice(0, 3).map((c, i) => <Avatar key={i} name={c} color="#2B303D" size={20} ring />)}
          </div>
          <Chip tone="neutral"><Icon name="Clock" size={10} /><Edit value={s.dur} onChange={v => patch(s.no, { dur: v })} className="text-[11px]" /></Chip>
        </div>
      </div>
    </div>
  );
}

/* ---- shared storyboard store -------------------------------------------
   The board (StoryboardModule) and the Automated Shot List read & write the
   SAME scenes from here, so adding or editing a shot on the board instantly
   shows up in the list, and per-shot frames dropped on either side carry
   across (they share an <image-slot> id). Persisted to localStorage. ---- */
const SB_KEY = 'silverlines.storyboard.v1';
let sbScenes = null;
const sbSubs = new Set();
/* seed asks (client/photographer requests) keyed by scene number, so boards
   saved before asks existed still surface the demo requests on migration. */
const SEED_ASKS = SCENES.reduce((m, s) => { if (s.asks) m[s.no] = s.asks; return m; }, {});
function sbMigrate(sc) {
  /* older saved scenes have no per-shot `auto` flag — infer it from the
     original ProFlow shot list so the Source column stays meaningful */
  /* normalize the shoot-day to a number (0 = unscheduled); seed a default
     only when a scene has never been scheduled */
  let sched = sc.sched || { day: '', call: '', est: '', status: 'Planned' };
  let day = sched.day;
  if (day === undefined || day === null || day === '') day = DEFAULT_DAYS[sc.no] || 0;
  else if (typeof day !== 'number') { const m = /(\d+)/.exec(day); day = m ? +m[1] : 0; }
  sched = { ...sched, day };
  /* don't bake a literal 'hero' onto unspotted scenes in a real production —
     leave it unset so sceneSpot() resolves it to the production's first spot */
  const spot = sc.spot || (!window.PROJ_BLANK ? DEFAULT_SPOT[sc.no] : undefined);
  const kind = sc.kind || (spot === 'stills' ? 'stills' : 'film');
  const asks = Array.isArray(sc.asks) ? sc.asks : (SEED_ASKS[sc.no] || []);
  return { ...sc, spot, kind, asks, sched, shots: (sc.shots || []).map((sh, i) => {
    let m = sh;
    if (m.auto === undefined) m = { ...m, auto: SHOTS.some(x => x.scene === sc.no && x.shot === m.shot && x.auto) };
    if (m.req === undefined) m = { ...m, req: seedReq(sc, i) };
    return m;
  }) };
}
function sbLoad() {
  if (sbScenes) return sbScenes;
  const s = window.SLStore.get(SB_KEY); if (Array.isArray(s) && s.length) { sbScenes = sbReconcile(s.map(sbMigrate)); return sbScenes; }
  sbScenes = seedStoryboard();
  return sbScenes;
}
/* append spots that didn't exist when this board was last saved (e.g. the
   stills shoot added later) so a returning project still gains new spots. */
function sbReconcile(list) {
  const have = new Set(list.map(s => s.id));
  const add = seedStoryboard().filter(s => !have.has(s.id) && s.spot === 'stills');
  return add.length ? [...list, ...add] : list;
}
function sbSet(next) {
  sbScenes = typeof next === 'function' ? next(sbLoad()) : next;
  window.SLStore.set(SB_KEY, sbScenes);
  sbSubs.forEach(fn => fn());
}
function useStoryboard() {
  const [, force] = useState(0);
  useEffect(() => {
    const fn = () => force(n => n + 1);
    sbSubs.add(fn);
    return () => sbSubs.delete(fn);
  }, []);
  return [sbLoad(), sbSet];
}
/* expose the shared scene store + an asks helper so the Spots module
   (loaded as its own file) reads/writes the same board. */
window.useStoryboard = useStoryboard;
window.sbLoad = sbLoad;
window.sbSetScenes = sbSet;

function ShotCard({ sc, sh, onPatch, onReq, onRemove }) {
  const Field = ({ label, value, onChange, mono }) => (
    <div className="min-w-0">
      <div className="font-mono text-[9px] uppercase tracking-[0.1em] text-zinc-600">{label}</div>
      <Edit value={value} onChange={onChange} placeholder="—" className={cx('truncate text-[12px] text-zinc-200', mono && 'font-mono')} />
    </div>
  );
  return (
    <div className="group/sc break-inside-avoid overflow-hidden rounded-xl border border-line bg-ink-900">
      {/* frame */}
      <div className="relative h-[120px] w-full" style={{ background: sc.color }}>
        <image-slot id={`sb-${sc.id}-${sh.id}`} placeholder={sh.shot} shape="rect"
          style={{ position:'absolute', inset:0, width:'100%', height:'100%', color:'rgba(255,255,255,.5)' }}></image-slot>
        <span className="pointer-events-none absolute left-2 top-2 flex items-center gap-1 rounded-md bg-black/65 px-1.5 py-0.5 font-mono text-[10px] font-700 text-white backdrop-blur-sm">
          <span className="text-electric-soft">{sc.no}</span><span>{sh.shot}</span>
        </span>
        <span className="pointer-events-none absolute right-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 font-mono text-[9px] font-600 uppercase tracking-wide backdrop-blur-sm">
          {sh.auto ? <span className="text-proflow-soft">ProFlow</span> : <span className="text-zinc-300">Manual</span>}
        </span>
        <button onClick={onRemove} title="Remove shot"
          className="absolute bottom-2 right-2 flex h-6 w-6 items-center justify-center rounded-md bg-black/60 text-zinc-300 opacity-0 backdrop-blur-sm transition-opacity hover:text-[#FF6B6B] group-hover/sc:opacity-100"><Icon name="Trash2" size={12} /></button>
      </div>
      {/* specs */}
      <div className="space-y-2 p-2.5">
        <div className="grid grid-cols-2 gap-x-3 gap-y-1.5">
          <Field label="Type" value={sh.type} onChange={v => onPatch({ type: v })} />
          <Field label="Angle" value={sh.angle} onChange={v => onPatch({ angle: v })} />
          <Field label="Lens" value={sh.lens} onChange={v => onPatch({ lens: v })} mono />
          <Field label="Movement" value={sh.move} onChange={v => onPatch({ move: v })} />
        </div>
        <div className="border-t border-line/70 pt-1.5">
          <div className="font-mono text-[9px] uppercase tracking-[0.1em] text-zinc-600">Notes</div>
          <Edit value={sh.notes} onChange={v => onPatch({ notes: v })} placeholder="Add a note…" className="text-[12px] text-zinc-300" />
        </div>
        <ReqEditor req={sh.req} onChange={onReq} />
      </div>
    </div>
  );
}

function ShotList({ mobile }) {
  /* cards are derived live from the storyboard, grouped by scene and
     organized by shoot day — editing a field writes back to the board. */
  const [scenes, setScenes] = useStoryboard();
  const days = shootDayCount(scenes);
  const upd = (scId, shId, p) => setScenes(ss => ss.map(s => s.id === scId
    ? { ...s, shots: s.shots.map(x => x.id === shId ? { ...x, ...p, auto: false } : x) } : s));
  const updReq = (scId, shId, req) => setScenes(ss => ss.map(s => s.id === scId
    ? { ...s, shots: s.shots.map(x => x.id === shId ? { ...x, req } : x) } : s));
  const setDay = (scId, n) => setScenes(ss => ss.map(s => s.id === scId ? { ...s, sched: { ...s.sched, day: n } } : s));
  const addShot = (scId) => setScenes(ss => ss.map(s => s.id === scId
    ? { ...s, shots: [...s.shots, { id:'sh-' + Date.now(), shot:String.fromCharCode(65 + s.shots.length), type:'WS', angle:'Eye-level', lens:'35mm', move:'Static', notes:'', auto:false, req:[] }] } : s));
  const removeShot = (scId, shId) => {
    const prev = scenes; const scene = scenes.find(s => s.id === scId);
    const shot = scene && scene.shots.find(x => x.id === shId);
    setScenes(ss => ss.map(s => s.id === scId ? { ...s, shots: s.shots.filter(x => x.id !== shId) } : s));
    window.pushUndo && window.pushUndo(`Removed shot ${(shot && shot.shot) || ''}`, () => setScenes(prev));
  };

  const totalShots = scenes.reduce((a, s) => a + (s.shots || []).length, 0);
  const manual = scenes.reduce((a, s) => a + (s.shots || []).filter(sh => !sh.auto).length, 0);

  /* bucket scenes by day */
  const byDay = {};
  scenes.forEach(s => { const d = sceneDay(s); (byDay[d] = byDay[d] || []).push(s); });
  const dayKeys = [];
  for (let d = 1; d <= days; d++) if (byDay[d]) dayKeys.push(d);
  if (byDay[0]) dayKeys.push(0);

  const colCls = mobile ? 'grid-cols-1' : 'grid-cols-2 xl:grid-cols-3';

  return (
    <div>
      <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
        <div className="flex items-center gap-2">
          <h3 className="font-display text-[15px] font-600 text-head">Shot List</h3>
          <ProFlowPill label="Derived from storyboard" />
        </div>
        <span className="font-mono text-[11px] text-zinc-500">{scenes.length} scenes · {totalShots} shots · {manual} manual</span>
      </div>

      {totalShots === 0 && scenes.length === 0 ? (
        <Panel><div className="px-4 py-12 text-center text-[12.5px] text-zinc-500">No shots yet — add scenes and shots in the <span className="text-zinc-300">Storyboard</span> tab.</div></Panel>
      ) : (
        <div className="space-y-7">
          {dayKeys.map(d => (
            <div key={d}>
              {/* day band */}
              <div className="mb-3 flex items-center gap-3 border-b border-line pb-2">
                {d === 0
                  ? <span className="flex items-center gap-1.5 font-display text-[15px] font-700 text-zinc-400"><Icon name="CalendarOff" size={15} />Unscheduled</span>
                  : <span className="flex items-center gap-2 font-display text-[15px] font-700 text-head"><span className="flex h-6 items-center rounded-md bg-electric/15 px-2 font-mono text-[12px] font-700 text-electric-soft">Day {d}</span>{fmtDayDate(d)}</span>}
                <span className="font-mono text-[11px] text-zinc-500">{byDay[d].length} sc · {byDay[d].reduce((a, s) => a + (s.shots || []).length, 0)} shots</span>
              </div>

              <div className="space-y-5">
                {byDay[d].map(sc => (
                  <div key={sc.id}>
                    {/* scene header */}
                    <div className="mb-2.5 flex flex-wrap items-center gap-x-3 gap-y-2">
                      <span className="flex h-6 items-center rounded-md bg-ink-700 px-2 font-mono text-[12px] font-700 text-electric-soft">{sc.no}</span>
                      <span className="text-[13.5px] font-700 text-head">{sc.title}</span>
                      <span className="flex items-center gap-1 text-[11.5px] text-zinc-500"><Icon name="MapPin" size={11} />{sc.loc}</span>
                      <span className="font-mono text-[11px] text-zinc-600">{(sc.shots || []).length} shot{(sc.shots || []).length === 1 ? '' : 's'}</span>
                      <div className="ml-auto flex items-center gap-2">
                        <span className="font-mono text-[9.5px] uppercase tracking-[0.12em] text-zinc-600">Day</span>
                        <DayPicker value={sceneDay(sc)} days={days} onChange={n => setDay(sc.id, n)} />
                      </div>
                    </div>
                    {/* shot cards */}
                    <div className={cx('grid gap-3', colCls)}>
                      {(sc.shots || []).map(sh => (
                        <ShotCard key={sh.id} sc={sc} sh={sh}
                          onPatch={p => upd(sc.id, sh.id, p)} onReq={r => updReq(sc.id, sh.id, r)} onRemove={() => removeShot(sc.id, sh.id)} />
                      ))}
                      <button onClick={() => addShot(sc.id)}
                        className="flex min-h-[120px] items-center justify-center gap-2 rounded-xl border border-dashed border-ink-600 text-[12.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
                        <Icon name="Plus" size={16} /> Add shot
                      </button>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ============================================================
   Storyboard — fully editable scene board: add, drag-reorder,
   delete, edit every field, plus a per-scene Shot List and a
   Shooting Schedule. Persisted to localStorage.
   ============================================================ */
const SCHED_STATUSES = ['Planned', 'Scheduled', 'Shot', 'Hold'];
const SCHED_TONE = { Planned:'amber', Scheduled:'electric', Shot:'mint', Hold:'neutral' };

/* ============================================================
   SHOOT-DAY ENGINE — one source of truth: each scene's day.
   It drives the shot list, the per-day call sheets, the auto
   crew/cast call, and the Day-Out-of-Days. Assign a scene to a
   day once; everything downstream re-derives.
   ============================================================ */
function getShootDays() {
  const b = window.SLStore.get('silverlines.brief.v1'); if (b && +b.days) return Math.max(1, Math.min(60, +b.days));
  return 4;
}
/* effective day count — never less than the highest day a scene is assigned to,
   so a scene can never fall through if the planned day count is lowered */
function shootDayCount(list) { return Math.max(getShootDays(), ...(list || []).map(sceneDay), 1); }
/* first shoot day — read per-project from the brief (brief.startDate, ISO yyyy-mm-dd)
   so a real show prints its REAL dates instead of a hardcoded June 2026. The demo
   keeps its date; an undated real production starts from today until one is set. */
function shootStart() {
  try {
    const b = window.SLStore.get('silverlines.brief.v1');
    if (b && b.startDate) { const d = new Date(b.startDate + 'T00:00:00'); if (!isNaN(d.getTime())) return d; }
  } catch (e) {}
  if (!window.PROJ_BLANK) return new Date(2026, 5, 22);
  const t = new Date(); t.setHours(0, 0, 0, 0); return t;
}
/* default day assignment for the seed scenes so the chain demos out of the box */
const DEFAULT_DAYS = { '12A':1, '12B':1, '14':2, '18':2, '21':3, '23':4, 'S1':3, 'S2':3, 'S3':3 };
function dayDate(day) { const d = shootStart(); d.setDate(d.getDate() + Math.max(0, day - 1)); return d; }
/* BCP-47 locale from the active UI language — Swiss French/German so the call
   sheet and schedules read "lun. 23 juin" for FR/DE crew, not "Mon, Jun 23" */
function slLocale() { const l = (window.getLang && window.getLang()) || 'en'; return l === 'fr' ? 'fr-CH' : l === 'de' ? 'de-CH' : 'en-US'; }
function fmtDayDate(day) { return dayDate(day).toLocaleDateString(slLocale(), { weekday: 'short', month: 'short', day: 'numeric' }); }
/* sunrise/sunset for a date + lat/lng (SunCalc core) → local HH:MM, or null.
   Times render in the viewer's timezone; golden/magic hour ≈ ±45 min of the horizon. */
function sunTimes(date, lat, lng) {
  if (lat == null || lng == null || isNaN(+lat) || isNaN(+lng)) return null;
  const rad = Math.PI / 180, PI = Math.PI, dayMs = 864e5, J1970 = 2440588, J2000 = 2451545;
  const toDays = d => (d.valueOf() / dayMs - 0.5 + J1970) - J2000;
  const fromJ = j => new Date((j + 0.5 - J1970) * dayMs);
  const e = rad * 23.4397;
  const meanAnomaly = d => rad * (357.5291 + 0.98560028 * d);
  const eclLng = M => M + rad * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M)) + rad * 102.9372 + PI;
  const lw = rad * -(+lng), phi = rad * (+lat), d = toDays(date), J0 = 0.0009;
  const n = Math.round(d - J0 - lw / (2 * PI));
  const ds = J0 + lw / (2 * PI) + n;
  const M = meanAnomaly(ds), L = eclLng(M), dec = Math.asin(Math.sin(e) * Math.sin(L));
  const Jnoon = J2000 + ds + 0.0053 * Math.sin(M) - 0.0069 * Math.sin(2 * L);
  const cosH = (Math.sin(-0.833 * rad) - Math.sin(phi) * Math.sin(dec)) / (Math.cos(phi) * Math.cos(dec));
  if (cosH > 1 || cosH < -1) return null; // polar day/night
  const w = Math.acos(cosH);
  const Jset = J2000 + (J0 + (w + lw) / (2 * PI) + n) + 0.0053 * Math.sin(M) - 0.0069 * Math.sin(2 * L);
  const Jrise = Jnoon - (Jset - Jnoon);
  const hm = ms => new Date(ms).toLocaleTimeString(slLocale(), { hour: '2-digit', minute: '2-digit', hour12: false });
  return { sunrise: hm(fromJ(Jrise).getTime()), sunset: hm(fromJ(Jset).getTime()), goldenEnd: hm(fromJ(Jrise).getTime() + 45 * 6e4), magicStart: hm(fromJ(Jset).getTime() - 45 * 6e4) };
}
function sceneDay(s) {
  const d = s && s.sched ? s.sched.day : '';
  if (typeof d === 'number') return d;
  const m = /(\d+)/.exec(d || ''); return m ? +m[1] : 0; // 0 = unscheduled
}
const CORE_CREW_DEPTS = ['Camera', 'Lighting', 'Grip', 'Prod'];
/* breakdown element id -> on-set crew department (matches CREW_CALL.dept) */
/* element dept id -> crew department that gets pulled onto the call sheet.
   Covers every ELEMENT_DEPTS id now (sfx/vehicle/vfx/bg/location were missing, so
   tagging those pulled no crew); 'camera' was a dead key (the element id is 'equip'). */
const BD_TO_CREWDEPT = { props:'Art', setdec:'Art', art:'Art', wardrobe:'Wardrobe', makeup:'MUA', sound:'Sound', equip:'Camera', bg:'Prod', extras:'Prod', location:'Prod', sfx:'SFX', vehicle:'Transport', vehicles:'Transport', vfx:'VFX' };
function crewDeptsForScenes(list) {
  const reasons = {};
  const addNeed = (dep, why) => { (reasons[dep] = reasons[dep] || new Set()).add(why); };
  CORE_CREW_DEPTS.forEach(d => addNeed(d, 'Core unit'));
  list.forEach(s => {
    if ((s.cast || []).length) { addNeed('MUA', 'Cast working'); addNeed('Wardrobe', 'Cast working'); addNeed('Sound', 'Performance'); }
    (s.shots || []).forEach(sh => (sh.req || []).forEach(r => {
      const cd = BD_TO_CREWDEPT[r.dept]; if (cd) addNeed(cd, ((typeof ED_BY_ID !== 'undefined' && ED_BY_ID[r.dept] && ED_BY_ID[r.dept].label) || r.dept) + ' tagged');
    }));
    /* breakdown tags from a linked script scene pull crew too (A1 linkage) */
    const st = window.scriptTagsFor && window.scriptTagsFor(s.no);
    if (st) Object.keys(st).forEach(cat => {
      if (!(st[cat] || []).length) return;
      if (cat === 'cast') { addNeed('MUA', 'Cast in script'); addNeed('Wardrobe', 'Cast in script'); addNeed('Sound', 'Performance'); return; }
      const cd = BD_TO_CREWDEPT[cat]; if (cd) addNeed(cd, cat + ' in script');
    });
  });
  return reasons; // { dept: Set(reasons) }
}
function autoCrewForScenes(list) {
  const reasons = crewDeptsForScenes(list);
  return CREW_CALL.filter(c => reasons[c.dept]).map(c => ({ ...c, why: [...reasons[c.dept]].join(' · ') }));
}
/* background/unit tokens ("Talent x4", "Crew", "BG") aren't named cast — keep them
   out of the Cast Call and the Day-Out-of-Days so they don't get fabricated numbers */
const isBgToken = nm => /^\s*(crew|extras?|background|bg|stand-?ins?|units?|talent|atmosphere|photo\s?double|dbl)\b/i.test(nm || '') || /\bx\s*\d+\s*$/i.test(nm || '');
function castForScenes(list) {
  const names = [...new Set(list.flatMap(s => s.cast || []))].filter(nm => nm && !isBgToken(nm));
  return names.map((nm, i) => {
    const c = CAST_CALL.find(x => x.actor === nm);
    return c ? { ...c } : { no: i + 1, actor: nm, character: '—', pickup: '', mu: '', set: '', status: 'W' };
  });
}
/* Day-Out-of-Days: which cast works/holds across the whole shoot */
function castDOOD(scenes, days) {
  const names = [...new Set(scenes.flatMap(s => s.cast || []))].filter(nm => nm && !isBgToken(nm));
  return names.map(nm => {
    const work = {};
    for (let d = 1; d <= days; d++) work[d] = scenes.some(s => sceneDay(s) === d && (s.cast || []).includes(nm));
    const wd = Object.keys(work).filter(d => work[d]).map(Number);
    const start = wd.length ? Math.min(...wd) : 0, finish = wd.length ? Math.max(...wd) : 0;
    const cells = [];
    for (let d = 1; d <= days; d++) {
      if (work[d]) cells.push(d === start && d === finish ? 'SWF' : d === start ? 'S' : d === finish ? 'F' : 'W');
      else cells.push(d > start && d < finish ? 'H' : '');
    }
    return { name: nm, cells, total: wd.length };
  });
}
const DOOD_TONE = { S:'#22D48A', W:'#F57F45', F:'#BD2555', SWF:'#8C9BFF', H:'#5B6478' };

/* per-day call-sheet overrides (call times, excused/added crew, conditions) */
const CD_KEY = 'silverlines.callday.' + (window.PROJ_ID || 'crimson') + '.v1';
function cdAll() { return window.SLStore.get(CD_KEY, {}); }
function cdSetAll(o) { window.SLStore.set(CD_KEY, o); }

/* day chip / picker shared UI */
function DayBadge({ day, className = '' }) {
  if (!day) return <span className={cx('inline-flex items-center gap-1 rounded-md bg-ink-800 px-1.5 py-0.5 font-mono text-[10px] font-700 text-zinc-500', className)}><Icon name="CalendarOff" size={10} />Unscheduled</span>;
  return <span className={cx('inline-flex items-center gap-1 rounded-md bg-electric/15 px-1.5 py-0.5 font-mono text-[10px] font-700 text-electric-soft', className)}><Icon name="CalendarDays" size={10} />Day {day}</span>;
}
function DayPicker({ value, days, onChange }) {
  const max = Math.max(days || 1, value || 0);
  const opts = [...Array.from({ length: max }, (_, i) => i + 1), 0];
  return (
    <div className="flex flex-wrap items-center gap-1">
      {opts.map(n => (
        <button key={n} onClick={() => onChange(n)}
          className={cx('rounded-md px-2 py-1 font-mono text-[11px] font-700 transition-colors',
            value === n
              ? (n === 0 ? 'bg-zinc-700 text-zinc-100' : 'bg-electric/20 text-electric-soft ring-1 ring-electric/40')
              : 'bg-ink-850 text-zinc-500 hover:text-zinc-300')}>
          {n === 0 ? 'None' : 'D' + n}
        </button>
      ))}
    </div>
  );
}

/* ============================================================
   BREAKDOWN LAYER — per-shot requirements
   Every shot can be tagged with the real-world elements it needs.
   Each element is a category (owned by a crew department) and the
   SAME records feed: the shot list, department pull lists and the
   1st AD breakdown. Colours are read consistently everywhere.
   ============================================================ */
const ELEMENT_DEPTS = [
  { id:'cast',     label:'Cast',          color:'#FF5D92', icon:'User',     dept:'casting'  },
  { id:'bg',       label:'Background',     color:'#FF8FB0', icon:'Users',    dept:'casting'  },
  { id:'props',    label:'Props',         color:'#A98BFF', icon:'Package',   dept:'art'      },
  { id:'setdec',   label:'Set dressing',  color:'#22D48A', icon:'Sofa',      dept:'art'      },
  { id:'wardrobe', label:'Wardrobe',      color:'#F7A8CF', icon:'Shirt',     dept:'styling'  },
  { id:'makeup',   label:'Hair / Makeup', color:'#FFB020', icon:'Brush',     dept:'mua'      },
  { id:'location', label:'Location',      color:'#F57F45', icon:'MapPin',    dept:'location' },
  { id:'sfx',      label:'Stunts / SFX',  color:'#FF4D6D', icon:'Flame',     dept:'stunts'   },
  { id:'vehicle',  label:'Vehicles',      color:'#5FB0E8', icon:'Truck',     dept:'transport'},
  { id:'vfx',      label:'VFX',           color:'#C26BFF', icon:'Sparkles',  dept:'vfx'      },
  { id:'equip',    label:'Special equip', color:'#36D0C4', icon:'Camera',    dept:'camera'   },
  { id:'sound',    label:'Sound',         color:'#8C9BFF', icon:'Mic',       dept:'sound'    },
];
const ED_BY_ID = Object.fromEntries(ELEMENT_DEPTS.map(d => [d.id, d]));
function ownDeptName(catId) { const c = ED_BY_ID[catId]; if (!c) return ''; const d = (typeof DEPARTMENTS !== 'undefined' ? DEPARTMENTS : []).find(x => x.id === c.dept); return d ? d.name : ''; }

/* derive sensible starter requirements from a scene so the board is useful
   out of the box without being messy — just the location + (on the first
   setup) the scene's cast. Everything else the user tags by hand. */
function seedReq(sc, i) {
  const r = [];
  if (sc.loc && sc.loc !== 'Location TBD') r.push({ id:'rq-loc-' + (sc.no||'x'), dept:'location', label:sc.loc, qty:1, note:'' });
  if (i === 0) (sc.cast || []).forEach((c, j) => r.push({ id:'rq-c' + j + '-' + (sc.no||'x'), dept:'cast', label:c, qty:1, note:'' }));
  return r;
}

/* ---- managed element / props library: the reusable catalogue you pick
   from when tagging shots. Persisted to localStorage. ---- */
const ELIB_KEY = 'silverlines.elements.v1';
let elibItems = null; const elibSubs = new Set();
function elibSeed() {
  return [
    { dept:'props', label:'Vintage altimeter (hero)' }, { dept:'props', label:'Brushed-steel switch panels' },
    { dept:'props', label:'Leather flight gloves' }, { dept:'props', label:'Vintage tool chest' },
    { dept:'props', label:'Runway marker cones' }, { dept:'props', label:'Aviator sunglasses' },
    { dept:'props', label:'Logbook & charts' }, { dept:'props', label:'Brass compass' },
    { dept:'setdec', label:'Instrument bezel LEDs' }, { dept:'setdec', label:'Industrial floor fans' },
    { dept:'setdec', label:'Hazard tape & decals' }, { dept:'setdec', label:'Fuel cart (non-prac)' },
    { dept:'wardrobe', label:'Flight suit — hero' }, { dept:'wardrobe', label:'Ground crew coveralls' },
    { dept:'wardrobe', label:'Leather flight jacket' },
    { dept:'makeup', label:'Sweat / grime kit' }, { dept:'makeup', label:'Continuity scar' },
    { dept:'cast', label:'The Aviator' }, { dept:'cast', label:'Ground Chief' },
    { dept:'bg', label:'Ground crew ×6' }, { dept:'bg', label:'Tarmac extras ×12' },
    { dept:'vehicle', label:'Period fuel truck' }, { dept:'vehicle', label:'Tow tractor' },
    { dept:'sfx', label:'Atmos smoke' }, { dept:'sfx', label:'Practical flare' }, { dept:'sfx', label:'Wind machine' },
    { dept:'vfx', label:'Bluescreen plate' }, { dept:'vfx', label:'Tracking markers' },
    { dept:'equip', label:'Crane / techno arm' }, { dept:'equip', label:'Steadicam' }, { dept:'equip', label:'Dolly & track' },
    { dept:'location', label:'Stage 4 — Cockpit' }, { dept:'location', label:'Runway B — Tarmac' }, { dept:'location', label:'Hangar 7 — Reveal' },
    { dept:'sound', label:'Radio mic ×2' }, { dept:'sound', label:'Playback for tarmac' },
  ].map((x, k) => ({ id:'el-' + k, ...x }));
}
function elibLoad() {
  if (elibItems) return elibItems;
  const s = window.SLStore.get(ELIB_KEY); if (Array.isArray(s)) { elibItems = s; return s; }
  elibItems = elibSeed(); return elibItems;
}
function elibSet(next) {
  elibItems = typeof next === 'function' ? next(elibLoad()) : next;
  window.SLStore.set(ELIB_KEY, elibItems);
  elibSubs.forEach(fn => fn());
}
function useElementLib() {
  const [, force] = useState(0);
  useEffect(() => { const fn = () => force(n => n + 1); elibSubs.add(fn); return () => elibSubs.delete(fn); }, []);
  return [elibLoad(), elibSet];
}

/* aggregate every tagged requirement across the board, grouped by
   category + label, with the shots that call for it. */
function collectReqs(scenes) {
  const map = new Map();
  scenes.forEach(sc => (sc.shots || []).forEach(sh => (sh.req || []).forEach(r => {
    const key = r.dept + '|' + (r.label || '').trim().toLowerCase();
    if (!map.has(key)) map.set(key, { dept:r.dept, label:r.label, qty:0, refs:[] });
    const e = map.get(key); e.qty += (r.qty || 1); e.refs.push({ sc:sc.no, shot:sh.shot });
  })));
  return [...map.values()];
}
/* breakdown tags from linked script scenes, mapped to element-dept ids (A1) */
const SCRIPT2ED = { extras:'bg', vehicles:'vehicle', art:'setdec' }; // rest identity; cast excluded
function collectScriptReqs(scenes) {
  const out = [];
  scenes.forEach(sc => {
    const st = window.scriptTagsFor && window.scriptTagsFor(sc.no);
    if (!st) return;
    Object.keys(st).forEach(cat => {
      if (cat === 'cast') return;
      const dept = SCRIPT2ED[cat] || cat;
      if (!ED_BY_ID[dept]) return;
      (st[cat] || []).forEach(label => out.push({ dept, label, qty:1, refs:[{ sc:sc.no, shot:'·s' }], _script:true }));
    });
  });
  return out;
}
/* merge two req lists, dedupe by dept+label; _script survives only if all-script */
function mergeReqs(a, b) {
  const map = new Map();
  [...a, ...b].forEach(r => {
    const key = r.dept + '|' + (r.label || '').trim().toLowerCase();
    if (!map.has(key)) map.set(key, { ...r, refs: [...(r.refs || [])] });
    else { const e = map.get(key); e.qty = (e.qty || 1) + (r.qty || 1); e.refs.push(...(r.refs || [])); if (!r._script) e._script = false; }
  });
  return [...map.values()];
}

/* ---- read-only requirement chip (shot list, AD breakdown, summaries) ---- */
function ReqChip({ r }) {
  const d = ED_BY_ID[r.dept] || { color:'#7A8090', label:r.dept };
  return (
    <span className="reqchip" style={{ '--c': d.color }} title={d.label + (r.note ? ' · ' + r.note : '')}>
      <span className="reqdot" style={{ '--c': d.color }}></span>
      <span className="reqlabel">{r.label}</span>
      {(r.qty || 1) > 1 && <span className="reqqty">×{r.qty}</span>}
    </span>
  );
}

/* ---- one editable requirement row (inside the per-shot editor) ---- */
function ReqEditRow({ r, onPatch, onRemove }) {
  const d = ED_BY_ID[r.dept] || { color:'#7A8090', label:r.dept, icon:'Tag' };
  return (
    <div className="flex items-center gap-2 rounded-lg border border-line bg-ink-900 px-2 py-1.5">
      <span className="reqdot shrink-0" style={{ '--c': d.color }}></span>
      <span className="hidden w-[68px] shrink-0 truncate font-mono text-[9px] font-700 uppercase tracking-wide sm:block" style={{ color:d.color }}>{d.label}</span>
      <Edit value={r.label} onChange={v => onPatch({ label:v })} className="min-w-0 flex-1 text-[11.5px] font-600 text-zinc-100" />
      <span className="flex shrink-0 items-center gap-0.5 font-mono text-[10.5px] text-zinc-400">×<Edit value={String(r.qty || 1)} onChange={v => onPatch({ qty:Math.max(1, parseInt(v) || 1) })} align="center" className="w-5 font-mono text-[10.5px]" /></span>
      <Edit value={r.note || ''} onChange={v => onPatch({ note:v })} placeholder="note" className="w-[64px] shrink-0 text-[10.5px] text-zinc-500" />
      <button onClick={onRemove} title="Remove" className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-zinc-600 transition-colors hover:bg-proflow/15 hover:text-proflow-soft"><Icon name="X" size={11} /></button>
    </div>
  );
}

/* ---- picker: search/add from the library, or create a new element ---- */
function ReqPickerInline({ onAdd }) {
  const [lib, setLib] = useElementLib();
  const [q, setQ] = useState('');
  const [cat, setCat] = useState('props');
  const ql = q.trim().toLowerCase();
  const matches = lib.filter(it => (!cat || it.dept === cat) && (!ql || it.label.toLowerCase().includes(ql))).slice(0, 24);
  const canCreate = ql && !lib.some(it => it.dept === cat && it.label.toLowerCase() === ql);
  const createAndAdd = () => {
    const item = { id:'el-' + Date.now(), dept:cat, label:q.trim() };
    setLib(items => [...items, item]); onAdd({ dept:item.dept, label:item.label }); setQ('');
  };
  return (
    <div className="rounded-lg border border-line bg-ink-950/50 p-2">
      <div className="mb-1.5 flex flex-wrap gap-1">
        {ELEMENT_DEPTS.map(d => (
          <button key={d.id} onClick={() => setCat(d.id)}
            className={cx('inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[9.5px] font-700 transition-colors', cat === d.id ? '' : 'opacity-45 hover:opacity-80')}
            style={cat === d.id ? { color:d.color, background:'color-mix(in oklab,' + d.color + ' 16%, transparent)', border:'1px solid color-mix(in oklab,' + d.color + ' 45%, transparent)' } : { color:d.color, border:'1px solid transparent' }}>
            <span className="reqdot" style={{ '--c': d.color }}></span>{d.label}
          </button>
        ))}
      </div>
      <input value={q} onChange={e => setQ(e.target.value)} placeholder={'Search or add a ' + (ED_BY_ID[cat]?.label || '').toLowerCase() + '…'}
        className="mb-1.5 w-full rounded-md border border-line bg-ink-850 px-2 py-1.5 text-[11.5px] text-zinc-100 outline-none placeholder:text-zinc-600 focus:border-electric/40" />
      <div className="max-h-[132px] space-y-0.5 overflow-y-auto">
        {canCreate && (
          <button onClick={createAndAdd} className="flex w-full items-center gap-1.5 rounded-md border border-dashed border-electric/40 px-2 py-1 text-left text-[11px] font-600 text-electric-soft hover:bg-electric/10">
            <Icon name="Plus" size={12} />Create “{q.trim()}” in {ED_BY_ID[cat]?.label} · add to library
          </button>
        )}
        {matches.map(it => (
          <button key={it.id} onClick={() => onAdd({ dept:it.dept, label:it.label })}
            className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-[11.5px] text-zinc-200 transition-colors hover:bg-ink-800">
            <span className="reqdot shrink-0" style={{ '--c': (ED_BY_ID[it.dept] || {}).color }}></span>
            <span className="min-w-0 flex-1 truncate">{it.label}</span>
            <Icon name="Plus" size={11} className="shrink-0 text-zinc-600" />
          </button>
        ))}
        {matches.length === 0 && !canCreate && <div className="px-2 py-2 text-center text-[10.5px] text-zinc-600">Type to add an element</div>}
      </div>
    </div>
  );
}

/* ---- the per-shot requirements tray: chips always visible (clean),
   editing tucked behind a toggle so the board stays calm ---- */
function ReqEditor({ req, onChange }) {
  const [open, setOpen] = useState(false);
  const list = req || [];
  const add = it => onChange([...list, { id:'rq-' + Date.now() + Math.random().toString(36).slice(2, 5), dept:it.dept, label:it.label, qty:1, note:'' }]);
  const patch = (id, p) => onChange(list.map(r => r.id === id ? { ...r, ...p } : r));
  const rm = id => onChange(list.filter(r => r.id !== id));
  return (
    <div className="mt-2 border-t border-line/60 pt-2">
      <div className="flex flex-wrap items-center gap-1.5">
        <span className="font-mono text-[9px] font-700 uppercase tracking-[0.12em] text-zinc-500">Needs</span>
        {list.length === 0 && !open && <span className="text-[10.5px] italic text-zinc-600">nothing tagged</span>}
        {!open && list.map(r => <ReqChip key={r.id} r={r} />)}
        <button onClick={() => setOpen(o => !o)}
          className={cx('reqadd', open && 'border-solid border-electric/50 text-electric-soft')}>
          {open ? <><Icon name="Check" size={10} />done</> : <><Icon name="Plus" size={10} />tag</>}
        </button>
      </div>
      {open && (
        <div className="mt-2 space-y-1.5">
          {list.map(r => <ReqEditRow key={r.id} r={r} onPatch={p => patch(r.id, p)} onRemove={() => rm(r.id)} />)}
          <ReqPickerInline onAdd={add} />
        </div>
      )}
    </div>
  );
}

/* ============================================================
   Department pull lists + the managed element library
   (Pre-Production → Breakdown)
   ============================================================ */
function PullLists({ scenes }) {
  const agg = mergeReqs(collectReqs(scenes), collectScriptReqs(scenes));
  const cats = ELEMENT_DEPTS.filter(d => agg.some(a => a.dept === d.id));
  const totalEls = agg.length;
  return (
    <div>
      <div className="mb-4 flex items-center gap-2 text-[12px] text-zinc-500">
        <Icon name="Boxes" size={13} className="text-electric-soft" />
        <span><span className="text-zinc-300">{totalEls}</span> distinct elements pulled across <span className="text-zinc-300">{scenes.reduce((a, s) => a + (s.shots || []).length, 0)}</span> shots — auto-grouped by department.</span>
      </div>
      {cats.length === 0 ? (
        <div className="rounded-xl border border-dashed border-line bg-ink-900/50 px-4 py-12 text-center text-[12.5px] text-zinc-500">
          No elements tagged yet — open <span className="text-zinc-300">Storyboard</span>, expand a shot’s <span className="text-zinc-300">Needs</span> tray and start tagging.
        </div>
      ) : (
        <div className="grid gap-3.5 md:grid-cols-2 xl:grid-cols-3">
          {cats.map(d => {
            const items = agg.filter(a => a.dept === d.id).sort((a, b) => b.refs.length - a.refs.length);
            return (
              <div key={d.id} className="overflow-hidden rounded-xl border border-line bg-ink-900">
                <div className="flex items-center justify-between border-b border-line px-3.5 py-2.5" style={{ background:'color-mix(in oklab,' + d.color + ' 9%, transparent)' }}>
                  <div className="flex items-center gap-2">
                    <span className="flex h-6 w-6 items-center justify-center rounded-md" style={{ background:'color-mix(in oklab,' + d.color + ' 18%, transparent)', color:d.color }}><Icon name={d.icon} size={13} /></span>
                    <div>
                      <div className="font-display text-[13px] font-700 text-head">{d.label}</div>
                      <div className="font-mono text-[9.5px] uppercase tracking-wide text-zinc-500">{ownDeptName(d.id) || '—'}</div>
                    </div>
                  </div>
                  <span className="font-mono text-[11px]" style={{ color:d.color }}>{items.length}</span>
                </div>
                <div className="divide-y divide-line/70">
                  {items.map((it, k) => (
                    <div key={k} className="flex items-start justify-between gap-2 px-3.5 py-2">
                      <div className="min-w-0">
                        <div className="flex items-center gap-1.5">
                          <span className="truncate text-[12.5px] font-600 text-zinc-100">{it.label}</span>
                          {it._script && <span title="From the linked script breakdown" className="shrink-0 rounded bg-electric/15 px-1 font-mono text-[8.5px] font-700 uppercase text-electric-soft">script</span>}
                        </div>
                        <div className="mt-0.5 flex flex-wrap gap-1">
                          {it.refs.map((rf, ri) => <span key={ri} className="rounded bg-ink-800 px-1 font-mono text-[9.5px] text-zinc-500">{rf.sc}{rf.shot}</span>)}
                        </div>
                      </div>
                      {it.qty > 1 && <span className="shrink-0 rounded-md bg-ink-800 px-1.5 py-0.5 font-mono text-[10.5px] text-zinc-300">×{it.qty}</span>}
                    </div>
                  ))}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function ElementLibrary() {
  const [lib, setLib] = useElementLib();
  const [q, setQ] = useState('');
  const [cat, setCat] = useState('props');
  const [draft, setDraft] = useState('');
  const ql = q.trim().toLowerCase();
  const groups = ELEMENT_DEPTS.map(d => ({ d, items: lib.filter(it => it.dept === d.id && (!ql || it.label.toLowerCase().includes(ql))) })).filter(g => g.items.length);
  const addItem = () => { if (!draft.trim()) return; setLib(items => [...items, { id:'el-' + Date.now(), dept:cat, label:draft.trim() }]); setDraft(''); };
  const rm = id => setLib(items => items.filter(it => it.id !== id));
  return (
    <div>
      <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-center gap-2 text-[12px] text-zinc-500">
          <Icon name="Library" size={13} className="text-electric-soft" />
          <span>Reusable elements & props — <span className="text-zinc-300">{lib.length}</span> in the catalogue. Pick these when tagging shots.</span>
        </div>
        <div className="relative w-full sm:w-[220px]">
          <Icon name="Search" size={13} className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-500" />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search library…"
            className="w-full rounded-lg border border-line bg-ink-850 py-2 pl-8 pr-2 text-[12px] text-zinc-100 outline-none placeholder:text-zinc-600 focus:border-electric/40" />
        </div>
      </div>

      {/* add row */}
      <div className="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-line bg-ink-900 p-2.5">
        <span className="font-mono text-[9.5px] uppercase tracking-[0.12em] text-zinc-500">Add</span>
        <select value={cat} onChange={e => setCat(e.target.value)}
          className="rounded-lg border border-line bg-ink-850 px-2 py-1.5 text-[12px] font-600 text-zinc-200 outline-none">
          {ELEMENT_DEPTS.map(d => <option key={d.id} value={d.id}>{d.label}</option>)}
        </select>
        <input value={draft} onChange={e => setDraft(e.target.value)} onKeyDown={e => e.key === 'Enter' && addItem()} placeholder="New element name…"
          className="min-w-[160px] flex-1 rounded-lg border border-line bg-ink-850 px-2.5 py-1.5 text-[12px] text-zinc-100 outline-none placeholder:text-zinc-600 focus:border-electric/40" />
        <button onClick={addItem} className="flex items-center gap-1.5 rounded-lg bg-cta px-3 py-1.5 text-[12px] font-700 text-cta-ink hover:bg-cta-hover"><Icon name="Plus" size={13} />Add to library</button>
      </div>

      <div className="grid gap-3.5 md:grid-cols-2 xl:grid-cols-3">
        {groups.map(({ d, items }) => (
          <div key={d.id} className="overflow-hidden rounded-xl border border-line bg-ink-900">
            <div className="flex items-center justify-between border-b border-line px-3.5 py-2.5" style={{ background:'color-mix(in oklab,' + d.color + ' 9%, transparent)' }}>
              <div className="flex items-center gap-2">
                <span className="reqdot" style={{ '--c': d.color }}></span>
                <span className="font-display text-[12.5px] font-700 text-head">{d.label}</span>
              </div>
              <span className="font-mono text-[10.5px] text-zinc-500">{items.length}</span>
            </div>
            <div className="divide-y divide-line/70">
              {items.map(it => (
                <div key={it.id} className="group/li flex items-center justify-between gap-2 px-3.5 py-2">
                  <span className="min-w-0 truncate text-[12.5px] text-zinc-200">{it.label}</span>
                  <button onClick={() => rm(it.id)} title="Remove from library"
                    className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-zinc-700 opacity-0 transition-all hover:bg-proflow/15 hover:text-proflow-soft group-hover/li:opacity-100"><Icon name="Trash2" size={12} /></button>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function BreakdownModule({ mobile }) {
  const [scenes] = useStoryboard();
  const [view, setView] = useState('pull');
  const views = [{ id:'pull', label:'Pull Lists', icon:'Boxes' }, { id:'lib', label:'Element Library', icon:'Library' }];
  return (
    <div>
      <div className="mb-4 flex items-center gap-2">
        <div className="inline-flex rounded-lg border border-line bg-ink-850 p-0.5">
          {views.map(v => (
            <button key={v.id} onClick={() => setView(v.id)}
              className={cx('flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-600 transition-colors', view === v.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
              <Icon name={v.icon} size={13} />{v.label}
            </button>
          ))}
        </div>
        <ProFlowPill label="Lined from storyboard" />
      </div>
      {view === 'pull' ? <PullLists scenes={scenes} /> : <ElementLibrary />}
    </div>
  );
}

/* ============================================================
   1st AD Breakdown — full per-scene element breakdown for the
   assistant director. Lives on the Production page, NOT on the
   crew call sheet. Printable.
   ============================================================ */
function ADBreakdown({ mobile }) {
  const [scenes, setScenes] = useStoryboard();
  const patchShot = (sid, shid, p) => setScenes(ss => ss.map(s => s.id === sid ? { ...s, shots: s.shots.map(x => x.id === shid ? { ...x, ...p } : x) } : s));
  const print = () => { document.body.classList.add('bd-print'); window.print(); setTimeout(() => document.body.classList.remove('bd-print'), 400); };
  const grandEls = mergeReqs(collectReqs(scenes), collectScriptReqs(scenes)).length;
  const days = shootDayCount(scenes);
  const dood = castDOOD(scenes, days);
  return (
    <div className="fade-up">
      <div className="no-print mb-4 flex flex-wrap items-center justify-between gap-3">
        <div className="flex items-center gap-2 rounded-xl border border-amber/25 bg-amber/[0.07] px-3 py-2 text-[12px] text-amber">
          <Icon name="Eye" size={14} /><span className="font-600">1st AD eyes only — the full breakdown. Not published on the crew call sheet.</span>
        </div>
        <button onClick={print} className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[12.5px] font-700 text-zinc-200 hover:border-ink-600"><Icon name="FileDown" size={14} />Print breakdown</button>
      </div>

      <div id="print-breakdown" className="mx-auto max-w-[1000px] overflow-hidden rounded-2xl border border-line bg-ink-900">
        <div className="flex items-center justify-between border-b border-line bg-ink-850 px-5 py-4">
          <div>
            <div className="font-display text-[18px] font-800 italic text-head">Breakdown Sheet</div>
            <div className="font-mono text-[11px] text-zinc-500">{(typeof PROJECT !== 'undefined' ? PROJECT.code + ' · ' : '')}{scenes.length} scenes · {grandEls} elements</div>
          </div>
          <div className="flex flex-wrap justify-end gap-1.5">
            {ELEMENT_DEPTS.map(d => (
              <span key={d.id} className="inline-flex items-center gap-1 text-[9.5px] font-600 text-zinc-500"><span className="reqdot" style={{ '--c': d.color }}></span>{d.label}</span>
            ))}
          </div>
        </div>

        {/* Day Out of Days — cast across the whole shoot */}
        {dood.length > 0 && (
          <div className="border-b border-line px-5 py-4">
            <div className="mb-2.5 flex flex-wrap items-center gap-2">
              <Icon name="CalendarRange" size={14} className="text-electric-soft" />
              <h4 className="font-display text-[13px] font-700 uppercase tracking-[0.08em] text-head">Day Out of Days · Cast</h4>
              <span className="font-mono text-[10px] text-zinc-500">S start · W work · H hold · F finish</span>
            </div>
            <div className="overflow-x-auto no-scrollbar">
              <table className="w-full min-w-[420px] text-left">
                <thead><tr className="text-[9.5px] uppercase tracking-wide text-zinc-500">
                  <th className="py-1 pr-2 font-600">Cast</th>
                  {Array.from({ length: days }, (_, i) => i + 1).map(n => <th key={n} className="px-1 py-1 text-center font-600">D{n}</th>)}
                  <th className="px-1 py-1 text-center font-600">Days</th>
                </tr></thead>
                <tbody>
                  {dood.map(r => (
                    <tr key={r.name} className="border-t border-line/60">
                      <td className="whitespace-nowrap py-1.5 pr-2 text-[12px] font-600 text-zinc-200">{r.name}</td>
                      {r.cells.map((c, i) => (
                        <td key={i} className="px-1 py-1.5 text-center">
                          {c ? <span className="inline-flex h-5 w-6 items-center justify-center rounded font-mono text-[10px] font-700" style={{ background:`color-mix(in oklab, ${DOOD_TONE[c]} 18%, transparent)`, color:DOOD_TONE[c] }}>{c}</span> : <span className="text-zinc-700">·</span>}
                        </td>
                      ))}
                      <td className="px-1 py-1.5 text-center font-mono text-[11px] text-zinc-400">{r.total}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        )}

        {scenes.length === 0 && <div className="px-5 py-12 text-center text-[12.5px] text-zinc-500">No scenes yet.</div>}

        <div className="divide-y divide-line">
          {scenes.map(s => {
            const shots = s.shots || [];
            const cats = ELEMENT_DEPTS.filter(d => shots.some(sh => (sh.req || []).some(r => r.dept === d.id)));
            return (
              <div key={s.id} className="px-5 py-4">
                {/* scene header */}
                <div className="flex flex-wrap items-center justify-between gap-2">
                  <div className="flex min-w-0 items-center gap-2">
                    <span className="flex h-7 items-center rounded-md bg-ink-700 px-2 font-mono text-[12px] font-700 text-electric-soft">SC {s.no}</span>
                    <span className="truncate font-display text-[15px] font-700 text-head">{s.title}</span>
                  </div>
                  <div className="flex items-center gap-2 text-[11px] text-zinc-500">
                    <span className="inline-flex items-center gap-1"><Icon name="MapPin" size={11} />{s.loc}</span>
                    <span className="inline-flex items-center gap-1"><Icon name="Sun" size={11} />{s.tod}</span>
                    <DayBadge day={sceneDay(s)} />
                    <Chip tone={SCHED_TONE[s.sched?.status] || 'neutral'}>{s.sched?.status || 'Planned'}</Chip>
                  </div>
                </div>
                {/* departments due strip */}
                {cats.length > 0 && (
                  <div className="mt-2 flex flex-wrap gap-1.5">
                    {cats.map(d => {
                      const n = shots.reduce((a, sh) => a + (sh.req || []).filter(r => r.dept === d.id).length, 0);
                      return <span key={d.id} className="reqchip" style={{ '--c': d.color }}><span className="reqdot" style={{ '--c': d.color }}></span><span className="reqlabel">{d.label}</span><span className="reqqty">{n}</span></span>;
                    })}
                  </div>
                )}
                {/* per-shot rows */}
                <div className="mt-3 space-y-2">
                  {shots.map(sh => (
                    <div key={sh.id} className="grid grid-cols-[64px_1fr] gap-3 rounded-lg border border-line bg-ink-850/60 p-2.5">
                      <div className="border-r border-line pr-3">
                        <div className="font-mono text-[13px] font-700 text-electric-soft"><Edit value={sh.shot} onChange={v => patchShot(s.id, sh.id, { shot: v })} className="font-mono text-[13px] font-700" /></div>
                        <div className="mt-0.5 font-mono text-[10px] text-zinc-500"><Edit value={sh.type} onChange={v => patchShot(s.id, sh.id, { type: v })} className="font-mono text-[10px]" /></div>
                        <div className="font-mono text-[10px] text-zinc-600"><Edit value={sh.lens} onChange={v => patchShot(s.id, sh.id, { lens: v })} className="font-mono text-[10px]" /></div>
                      </div>
                      <div className="min-w-0">
                        <ReqEditor req={sh.req} onChange={r => patchShot(s.id, sh.id, { req: r })} />
                        <div className="mt-1.5 flex items-start gap-1 text-[10.5px] text-zinc-500"><Icon name="StickyNote" size={10} className="mt-0.5 shrink-0" /><Edit value={sh.notes} onChange={v => patchShot(s.id, sh.id, { notes: v })} placeholder="Add a note…" multiline className="text-[10.5px]" /></div>
                      </div>
                    </div>
                  ))}
                  {shots.length === 0 && <div className="text-[11px] italic text-zinc-600">No shots in this scene yet.</div>}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function seedStoryboard() {
  if (window.PROJ_BLANK) return []; // a real/blank production starts with no scenes, not the demo's
  return SCENES.map(s => ({
    ...s,
    id: 'sc-' + s.no,
    spot: s.spot || DEFAULT_SPOT[s.no] || 'hero',
    kind: s.kind || (DEFAULT_SPOT[s.no] === 'stills' ? 'stills' : 'film'),
    asks: s.asks ? s.asks.map(a => ({ ...a })) : [],
    shots: SHOTS.filter(x => x.scene === s.no).map((x, i) => ({
      id: 'sh-' + s.no + '-' + i, shot: x.shot, type: x.type, angle: x.angle, lens: x.lens, move: x.move, notes: x.notes, auto: x.auto !== false, req: seedReq(s, i),
    })),
    sched: { day: DEFAULT_DAYS[s.no] || 0, call: '', est: s.dur, status: 'Planned' },
  }));
}

function ShotRow({ sh, onPatch, onRemove }) {
  return (
    <div className="rounded-lg border border-line bg-ink-850 p-2">
      <div className="flex items-center gap-2">
        <span className="flex h-6 min-w-[24px] items-center justify-center rounded-md bg-ink-700 px-1 font-mono text-[11px] font-700 text-electric-soft">
          <Edit value={sh.shot} onChange={v => onPatch({ shot: v })} align="center" className="font-mono text-[11px]" />
        </span>
        <span className="rounded-full border border-line bg-ink-800 px-2 py-0.5 text-[10.5px] font-600 text-zinc-300">
          <Edit value={sh.type} onChange={v => onPatch({ type: v })} className="text-[10.5px]" />
        </span>
        <span className="font-mono text-[11px] text-zinc-300"><Edit value={sh.lens} onChange={v => onPatch({ lens: v })} className="font-mono text-[11px]" /></span>
        <button onClick={onRemove} title="Delete shot"
          className="ml-auto flex h-6 w-6 items-center justify-center rounded-md text-zinc-600 transition-colors hover:bg-proflow/15 hover:text-proflow-soft"><Icon name="Trash2" size={12} /></button>
      </div>
      <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-zinc-400">
        <span className="inline-flex items-center gap-1"><Icon name="Move" size={11} className="text-zinc-600" /><Edit value={sh.move} onChange={v => onPatch({ move: v })} className="text-[11px]" /></span>
        <span className="inline-flex items-center gap-1"><Icon name="Compass" size={11} className="text-zinc-600" /><Edit value={sh.angle} onChange={v => onPatch({ angle: v })} className="text-[11px]" /></span>
      </div>
      <div className="mt-1 text-[11px] text-zinc-500">
        <Edit value={sh.notes} onChange={v => onPatch({ notes: v })} placeholder="Add a shot note…" multiline className="text-[11px]" />
      </div>
      <ReqEditor req={sh.req} onChange={r => onPatch({ req: r })} />
    </div>
  );
}

function SchedField({ icon, label, value, onChange, placeholder }) {
  return (
    <div className="rounded-lg border border-line bg-ink-850 p-2">
      <div className="mb-1 flex items-center gap-1 text-[10px] font-600 uppercase tracking-[0.1em] text-zinc-500"><Icon name={icon} size={10} />{label}</div>
      <Edit value={value} onChange={onChange} placeholder={placeholder} className="text-[12px] font-600 text-zinc-100" />
    </div>
  );
}

function SceneCard({ s, index, mobile, dragging, dragOver, patch, onRemove, addShot, patchShot, removeShot, patchSched, drag }) {
  const [panel, setPanel] = useState(null);
  const [tones, setTones] = useState(false);
  const [grab, setGrab] = useState(false);
  const n = s.shots.length;
  const gcols = n <= 1 ? 1 : n === 2 ? 2 : n === 4 ? 2 : 3;
  const cellH = n <= 1 ? (mobile ? 120 : 144) : (mobile ? 80 : 92);
  const cycleStatus = () => {
    const i = SCHED_STATUSES.indexOf(s.sched.status || 'Planned');
    patchSched({ status: SCHED_STATUSES[(i + 1) % SCHED_STATUSES.length] });
  };
  return (
    <div
      draggable={grab}
      onDragStart={e => drag.start(e, index)}
      onDragEnter={() => drag.enter(index)}
      onDragOver={e => e.preventDefault()}
      onDrop={e => { e.preventDefault(); drag.drop(index); }}
      onDragEnd={() => { setGrab(false); drag.end(); }}
      className={cx('group fade-up overflow-hidden rounded-xl border bg-ink-900 transition-all',
        dragging ? 'opacity-40' : '',
        dragOver ? 'border-electric ring-2 ring-electric/40' : 'border-line hover:border-ink-600')}>
      {/* scene chrome strip */}
      <div className="relative flex items-center justify-between gap-2 border-b border-line bg-ink-850 px-2.5 py-1.5">
        <div className="flex min-w-0 items-center gap-1.5">
          <span onMouseDown={() => setGrab(true)} onMouseUp={() => setGrab(false)} onMouseLeave={() => setGrab(false)} title="Drag to reorder"
            className="flex h-6 w-5 shrink-0 cursor-grab items-center justify-center rounded text-zinc-500 transition-colors hover:text-zinc-200 active:cursor-grabbing">
            <Icon name="GripVertical" size={14} />
          </span>
          <span className="flex items-center gap-1 whitespace-nowrap font-mono text-[10.5px] tracking-wide text-zinc-300">
            SC <Edit value={s.no} onChange={v => patch({ no: v })} className="font-mono text-[10.5px]" /> · <Edit value={s.tod} onChange={v => patch({ tod: v })} className="font-mono text-[10.5px]" />
          </span>
        </div>
        <div className="flex shrink-0 items-center gap-1.5">
          <span className="rounded-md bg-ink-700 px-1.5 py-0.5 font-mono text-[10px] font-600 text-zinc-200">
            <Edit value={s.pages} onChange={v => patch({ pages: v })} className="font-mono text-[10px]" />
          </span>
          <button onClick={() => setTones(t => !t)} title="Card tone"
            className={cx('flex h-6 w-6 items-center justify-center rounded-md transition-colors', tones ? 'bg-ink-700 text-white' : 'text-zinc-500 hover:bg-ink-800 hover:text-zinc-200')}>
            <Icon name="Palette" size={13} />
          </button>
          {tones && (
            <div className="absolute right-2 top-full z-30 mt-1 flex gap-1 rounded-lg border border-line bg-ink-800 p-1.5 shadow-xl">
              {SCENE_TONES.map(c => (
                <button key={c} onClick={() => { patch({ color: c }); setTones(false); }}
                  className={cx('h-5 w-5 rounded-md border transition-transform hover:scale-110', s.color === c ? 'border-white' : 'border-white/25')} style={{ background: c }}></button>
              ))}
            </div>
          )}
        </div>
      </div>
      {/* per-shot frames — one image slot per shot */}
      <div className="border-b border-line" style={{ background: s.color }}>
        {n === 0 ? (
          <button onClick={addShot} style={{ height: mobile ? 110 : 132 }}
            className="flex w-full flex-col items-center justify-center gap-1.5 text-white/75 transition-colors hover:text-white">
            <Icon name="ImagePlus" size={20} />
            <span className="text-[11.5px] font-600">Add a shot to start framing</span>
          </button>
        ) : (
          <div className={cx('grid gap-px', gcols === 1 ? 'grid-cols-1' : gcols === 2 ? 'grid-cols-2' : 'grid-cols-3')}>
            {s.shots.map(sh => (
              <div key={sh.id} className="relative bg-ink-950" style={{ height: cellH }}>
                <image-slot id={`sb-${s.id}-${sh.id}`} placeholder={sh.shot} shape="rect"
                  style={{ position:'absolute', inset:0, width:'100%', height:'100%', color:'rgba(255,255,255,.55)' }}></image-slot>
                <span className="pointer-events-none absolute bottom-1 left-1 z-10 flex items-center gap-1 rounded bg-black/65 px-1.5 py-0.5 font-mono text-[9.5px] font-700 text-white backdrop-blur-sm">
                  {sh.shot}<span className="font-500 text-zinc-300">{sh.type}</span>
                </span>
              </div>
            ))}
          </div>
        )}
      </div>
      {/* meta */}
      <div className="p-3">
        <div className="flex items-center gap-2">
          <span className="shrink-0 font-mono text-[11px] font-600 text-electric-soft">{s.no}</span>
          <Edit value={s.title} onChange={v => patch({ title: v })} className="min-w-0 flex-1 truncate text-[13px] font-600 text-zinc-100" />
        </div>
        <div className="mt-1 flex items-center gap-1.5 text-[11px] text-zinc-500">
          <Icon name="MapPin" size={11} className="shrink-0" />
          <Edit value={s.loc} onChange={v => patch({ loc: v })} className="min-w-0 flex-1 truncate text-[11px]" />
        </div>
        <div className="mt-2 flex items-center gap-1.5">
          <button onClick={() => setPanel('sched')} title="Set shoot day"><DayBadge day={sceneDay(s)} /></button>
        </div>
        <div className="mt-2.5 flex items-center justify-between">
          <div className="flex items-center -space-x-1.5">
            {(s.cast || []).slice(0, 3).map((c, i) => <Avatar key={i} name={c} color="#2B303D" size={20} ring />)}
            {!(s.cast || []).length && <span className="text-[10.5px] text-zinc-600">No cast tagged</span>}
          </div>
          <Chip tone="neutral"><Icon name="Clock" size={10} /><Edit value={s.dur} onChange={v => patch({ dur: v })} className="text-[11px]" /></Chip>
        </div>
      </div>
      {/* options bar */}
      <div className="flex items-center gap-1.5 border-t border-line px-2.5 py-2">
        <button onClick={() => setPanel(p => p === 'shots' ? null : 'shots')}
          className={cx('inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[11.5px] font-600 transition-colors',
            panel === 'shots' ? 'bg-electric/15 text-electric-soft' : 'text-zinc-400 hover:bg-ink-800 hover:text-zinc-200')}>
          <Icon name="ListChecks" size={13} />Shots · {s.shots.length}
        </button>
        <button onClick={() => setPanel(p => p === 'sched' ? null : 'sched')}
          className={cx('inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[11.5px] font-600 transition-colors',
            panel === 'sched' ? 'bg-amber/15 text-amber' : 'text-zinc-400 hover:bg-ink-800 hover:text-zinc-200')}>
          <Icon name="CalendarClock" size={13} />Schedule
        </button>
        <button onClick={onRemove} title="Delete scene"
          className="ml-auto flex h-7 w-7 items-center justify-center rounded-md text-zinc-600 transition-colors hover:bg-proflow/15 hover:text-proflow-soft"><Icon name="Trash2" size={13} /></button>
      </div>
      {/* shots panel */}
      {panel === 'shots' && (
        <div className="fade-up space-y-1.5 border-t border-line bg-ink-950/40 p-2.5">
          {s.shots.length === 0 && <div className="px-1 py-1 text-[11px] text-zinc-600">No shots yet — add the first setup.</div>}
          {s.shots.map(sh => <ShotRow key={sh.id} sh={sh} onPatch={p => patchShot(sh.id, p)} onRemove={() => removeShot(sh.id)} />)}
          <button onClick={addShot}
            className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-line py-1.5 text-[11.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
            <Icon name="Plus" size={13} />Add shot
          </button>
        </div>
      )}
      {/* schedule panel */}
      {panel === 'sched' && (
        <div className="fade-up border-t border-line bg-ink-950/40 p-2.5">
          <div className="mb-2 rounded-lg border border-line bg-ink-850 p-2">
            <div className="mb-1.5 flex items-center gap-1 text-[10px] font-600 uppercase tracking-[0.1em] text-zinc-500"><Icon name="CalendarDays" size={10} />Shoot day<span className="ml-auto font-mono normal-case tracking-normal text-[9.5px] text-zinc-600">shots inherit this</span></div>
            <DayPicker value={sceneDay(s)} days={getShootDays()} onChange={n => patchSched({ day: n })} />
          </div>
          <div className="grid grid-cols-2 gap-2">
            <SchedField icon="Clock" label="Call time" value={s.sched.call} onChange={v => patchSched({ call: v })} placeholder="06:00" />
            <SchedField icon="Hourglass" label="Est. length" value={s.sched.est} onChange={v => patchSched({ est: v })} placeholder="1h 30m" />
            <div className="col-span-2 rounded-lg border border-line bg-ink-850 p-2">
              <div className="mb-1 flex items-center gap-1 text-[10px] font-600 uppercase tracking-[0.1em] text-zinc-500"><Icon name="Flag" size={10} />Status</div>
              <button onClick={cycleStatus} title="Click to change" className="flex w-full items-center justify-between">
                <Chip tone={SCHED_TONE[s.sched.status] || 'neutral'}>{s.sched.status}</Chip>
                <Icon name="RefreshCw" size={11} className="text-zinc-600" />
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function StoryboardModule({ mobile }) {
  const [scenes, setScenes] = useStoryboard();
  const [activeSpot] = useActiveSpot();
  const [dragIdx, setDragIdx] = useState(null);
  const [overIdx, setOverIdx] = useState(null);

  const patch = (id, p) => setScenes(ss => ss.map(s => s.id === id ? { ...s, ...p } : s));
  const remove = (id) => {
    const prev = scenes; const scene = scenes.find(s => s.id === id) || {};
    setScenes(ss => ss.filter(s => s.id !== id));
    window.pushUndo && window.pushUndo(`Removed scene "${scene.title || scene.no || ''}"`, () => setScenes(prev));
  };
  const add = () => setScenes(ss => {
    /* file the new scene under the spot you're currently filtered to (or the first
       spot when viewing "all") so it never lands in an unreachable spot */
    const spotId = (activeSpot && activeSpot !== 'all') ? activeSpot : (spotsLoad()[0] && spotsLoad()[0].id) || undefined;
    return [...ss, { id:'sc-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6), no:String(ss.reduce((m, s) => Math.max(m, +s.no || 0), 0) + 1), title:'New Scene', loc:'Location TBD', tod:'Day', pages:'1/8', cast:[], dur:'0h 30m', color: SCENE_TONES[ss.length % SCENE_TONES.length], shots:[], spot: spotId, sched:{ day:0, call:'', est:'', status:'Planned' } }];
  });
  const addShot = (sid) => setScenes(ss => ss.map(s => s.id === sid ? { ...s, shots:[...s.shots, { id:'sh-' + Date.now(), shot:String.fromCharCode(65 + s.shots.length), type:'WS', angle:'Eye-level', lens:'35mm', move:'Static', notes:'', auto:false, req:[] }] } : s));
  const patchShot = (sid, shid, p) => setScenes(ss => ss.map(s => s.id === sid ? { ...s, shots:s.shots.map(sh => sh.id === shid ? { ...sh, ...p, auto:false } : sh) } : s));
  const removeShot = (sid, shid) => {
    const prev = scenes; const scene = scenes.find(s => s.id === sid);
    const shot = scene && scene.shots.find(sh => sh.id === shid);
    setScenes(ss => ss.map(s => s.id === sid ? { ...s, shots:s.shots.filter(sh => sh.id !== shid) } : s));
    window.pushUndo && window.pushUndo(`Removed shot ${(shot && shot.shot) || ''}`, () => setScenes(prev));
  };
  const patchSched = (sid, p) => setScenes(ss => ss.map(s => s.id === sid ? { ...s, sched:{ ...s.sched, ...p } } : s));

  const drag = {
    start: (e, i) => { setDragIdx(i); if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; },
    enter: (i) => setOverIdx(i),
    drop: (to) => { const from = dragIdx; setDragIdx(null); setOverIdx(null); if (from === null || from === to) return; const a = [...scenes]; const [m] = a.splice(from, 1); a.splice(to, 0, m); setScenes(a); },
    end: () => { setDragIdx(null); setOverIdx(null); },
  };

  const cols = mobile ? [scenes] : [[], [], []];
  if (!mobile) scenes.forEach((s, i) => cols[i % 3].push(s));

  const totalShots = scenes.reduce((a, s) => a + s.shots.length, 0);
  const scheduled = scenes.filter(s => s.sched.status === 'Scheduled' || s.sched.status === 'Shot').length;

  return (
    <div>
      <div className="mb-4 grid grid-cols-3 gap-3">
        {[
          { ic:'LayoutGrid', l:'Scenes', v:scenes.length, tone:'#F57F45' },
          { ic:'ListChecks', l:'Shots', v:totalShots, tone:'#86C94B' },
          { ic:'CalendarClock', l:'Scheduled', v:`${scheduled}/${scenes.length}`, tone:'#FFC533' },
        ].map(x => (
          <div key={x.l} className="rounded-xl border border-line bg-ink-900 p-3">
            <div className="flex items-center gap-1.5 text-[10.5px] uppercase tracking-[0.1em] text-zinc-500"><Icon name={x.ic} size={12} />{x.l}</div>
            <div className="mt-1 font-display text-[26px] font-700 leading-none" style={{ color:x.tone }}>{x.v}</div>
          </div>
        ))}
      </div>

      <div className="mb-3 flex items-center gap-2 text-[12px] text-zinc-500">
        <Icon name="MousePointer2" size={13} className="text-proflow-soft" />
        <span>Drag a frame to reorder · click any text to edit · open <span className="text-zinc-300">Shots</span> or <span className="text-zinc-300">Schedule</span> on each scene</span>
      </div>

      <div className={cx('flex gap-3.5', mobile ? 'flex-col' : 'items-start')}>
        {cols.map((col, ci) => (
          <div key={ci} className="flex min-w-0 flex-1 flex-col gap-3.5">
            {col.map(s => {
              const i = scenes.indexOf(s);
              return (
                <SceneCard key={s.id} s={s} index={i} mobile={mobile}
                  dragging={dragIdx === i} dragOver={overIdx === i && dragIdx !== null && dragIdx !== i}
                  patch={p => patch(s.id, p)} onRemove={() => remove(s.id)}
                  addShot={() => addShot(s.id)} patchShot={(shid, p) => patchShot(s.id, shid, p)} removeShot={shid => removeShot(s.id, shid)}
                  patchSched={p => patchSched(s.id, p)} drag={drag} />
              );
            })}
          </div>
        ))}
      </div>

      <button onClick={add}
        className="mt-3.5 flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-line py-3.5 text-[12.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
        <Icon name="Plus" size={15} />Add scene
      </button>
    </div>
  );
}

/* ---- Departments: full crew breakdown ---- */
const DEPARTMENTS = [
  { id:'direction', name:'Direction', icon:'Megaphone', head:'Dahlia Mercer', crew:4, tone:'electric',
    note:'Director, 1st AD, 2nd AD, Script Supervisor',
    items:[['Shot list locked',true],['Blocking rehearsal booked',true],['Continuity bible prepped',false]] },
  { id:'production', name:'Production Office', icon:'ClipboardList', head:'Noor Haddad', crew:6, tone:'electric',
    note:'PM, Coordinator, Accountant, PAs',
    items:[['Insurance & permits filed',true],['Crew deal memos signed',true],['Petty cash floats issued',false]] },
  { id:'camera', name:'Camera', icon:'Camera', head:'Idris Vance', crew:7, tone:'proflow',
    note:'DP, 1st & 2nd AC, DIT, Operators',
    items:[['Camera package booked (Alexa 35)',true],['Lens test — anamorphic set',true],['Media & DIT cart confirmed',true],['Drone unit on standby',false]] },
  { id:'lighting', name:'Lighting / Electrical', icon:'Lightbulb', head:'Theo Nkemelu', crew:5, tone:'amber',
    note:'Gaffer, Best Boy, Sparks',
    items:[['Lighting plot approved',true],['Genny + distro ordered',false],['HMI & tungsten package',false]] },
  { id:'grip', name:'Grip', icon:'Wrench', head:'TBD — bidding', crew:4, tone:'neutral',
    note:'Key Grip, Dolly Grip, Riggers',
    items:[['Dolly & track ordered',true],['Crane / techno arm quote',false],['Rigging plan for Hangar 7',false]] },
  { id:'sound', name:'Sound', icon:'Mic', head:'TBD — bidding', crew:2, tone:'neutral',
    note:'Production Mixer, Boom Op',
    items:[['Radio mic channels planned',false],['Reference playback for tarmac',false]] },
  { id:'art', name:'Art / Production Design', icon:'Palette', head:'Ravi Anand', crew:8, tone:'proflow',
    note:'Designer, Art Director, Set Dec, Props, Construction',
    items:[['Cockpit build — Stage 4',true],['Altimeter & instrument props',true],['Hangar dressing plan',false],['Set construction sign-off',false]] },
  { id:'mua', name:'Hair & Makeup (MUA)', icon:'Brush', head:'Coraline Bisset', crew:3, tone:'mint',
    note:'Key MUA, Hair Stylist, Assistants',
    items:[['Hero looks tested w/ Director',true],['Sweat / grime kit for cockpit',true],['Continuity stills logged',false]] },
  { id:'styling', name:'Wardrobe / Styling', icon:'Shirt', head:'Juno Park', crew:3, tone:'mint',
    note:'Costume Designer, Stylist, Standby',
    items:[['Flight suit fittings done',true],['Doubles & backups sourced',true],['Steamer + standby kit packed',false]] },
  { id:'location', name:'Locations', icon:'MapPin', head:'Gabriel Soto', crew:3, tone:'amber',
    note:'Location Manager, Scouts, Unit',
    items:[['Backlot + runway locked',true],['Hangar 7 permit pending',false],['Parking & basecamp map',true],['Weather contingency day held',false]] },
  { id:'catering', name:'Catering / Craft', icon:'UtensilsCrossed', head:'Mesa & Co.', crew:4, tone:'mint',
    note:'Caterer, Craft Service, Dietary',
    items:[['Headcount + dietary tally',true],['Dawn breakfast 05:30 confirmed',true],['Craft table restock schedule',true]] },
  { id:'transport', name:'Transport / Logistics', icon:'Truck', head:'TBD — bidding', crew:5, tone:'neutral',
    note:'Drivers, Picture Cars, Trucks',
    items:[['Shuttle from Lot C',false],['Camera & grip trucks booked',false]] },
  { id:'casting', name:'Casting / Talent', icon:'Users', head:'Lena Frost (lead)', crew:6, tone:'electric',
    note:'Lead, Featured, Background, Wrangler',
    items:[['Lead talent contracted',true],['4× featured pilots cast',true],['Background release forms',false]] },
  { id:'vfx', name:'VFX', icon:'Sparkles', head:'Orbit FX', crew:4, tone:'proflow',
    note:'Supervisor, Plate Sup, Trackers',
    items:[['Bluescreen plan — SC18',true],['Tracking markers spec',false],['Plate shoot brief to camera',false]] },
  { id:'stunts', name:'Stunts & SFX', icon:'Flame', head:'TBD — bidding', crew:3, tone:'amber',
    note:'Stunt Coord, SFX, Safety',
    items:[['Risk assessment — tarmac',false],['Practical flare / atmos plan',false]] },
  { id:'safety', name:'Health & Safety', icon:'HeartPulse', head:'Unit Medic', crew:2, tone:'amber',
    note:'Set Medic, H&S Officer',
    items:[['Nearest hospital logged',true],['First-aid + fire on set',true],['Safety briefing scheduled',false]] },
];

if (window.PROJ_BLANK) DEPARTMENTS.length = 0;

/* Logo library available to every department board — pick one when
   creating or editing an entry (names map to Lucide icons). */
const DEPT_ICONS = ['Boxes','Megaphone','ClipboardList','Camera','Lightbulb','Wrench','Mic','Palette','Brush','Shirt','MapPin','UtensilsCrossed','Truck','Users','Sparkles','Flame','HeartPulse','Clapperboard','Film','Video','Music','Headphones','Plane','Car','Hammer','Scissors','Star','Zap','Sun','Globe','Briefcase','PenTool','Radio','Anchor','Gem','Aperture','Wand2','Drama'];

/* Status model — seven readiness bands, each with its own colour shared by
   the badge, the count and the load bar so progress reads at a glance. */
function statusFor(items) {
  const total = items.length;
  const done = items.filter(i => i[1]).length;
  if (total === 0)      return { label:'Empty',        key:'empty',  color:'#6B7280' };
  if (done === total)   return { label:'Locked',       key:'locked', color:'#22D48A' };
  if (done === 0)       return { label:'Not started',  key:'todo',   color:'#FF4D6D' };
  const pct = done / total;
  if (pct < 0.34)      return { label:'Just started', key:'early',  color:'#FF8A3D' };
  if (pct < 0.6)       return { label:'In Prep',      key:'prep',   color:'#FFC533' };
  if (pct < 0.85)      return { label:'Underway',     key:'mid',    color:'#C3D43A' };
  return { label:'Almost there', key:'near', color:'#86C94B' };
}

/* readiness rank used by the two sort modes */
const READY_RANK = { locked:0, near:1, mid:2, prep:3, early:4, todo:5, empty:6 };
const WORK_RANK  = { todo:0, early:1, prep:2, mid:3, near:4, empty:5, locked:6 };

function StatusBadge({ st }) {
  return (
    <span className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[11px] font-semibold"
      style={{ color:st.color, background:st.color + '1f', borderColor:st.color + '59' }}>
      <span className="h-1.5 w-1.5 rounded-full" style={{ background:st.color }} />{st.label}
    </span>
  );
}

/* Per-task assignee — pick someone from the crew database or type a name to
   add a brand-new person to the to-do item. */
const ASSIGN_PALETTE = ['#F57F45','#BD2555','#22D48A','#FFB020','#F43F3E','#5B0952','#F7A8CF','#86C94B','#4D9DFF'];
function TaskAssignee({ assignee, people, onPick, label = 'Assign' }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState('');
  const list = people.filter(p => (p.name + ' ' + (p.role || '')).toLowerCase().includes(q.trim().toLowerCase()));
  const nm = q.trim();
  const exists = people.some(p => p.name.toLowerCase() === nm.toLowerCase());
  return (
    <div className="relative shrink-0">
      <button onClick={() => { setOpen(o => !o); setQ(''); }} title={assignee ? `Lead: ${assignee.name}` : 'Assign someone'}
        className={cx('flex items-center gap-1.5 rounded-full border py-0.5 pl-0.5 pr-2 transition-colors',
          assignee ? 'border-line bg-ink-800 hover:border-ink-600' : 'border-dashed border-zinc-700 px-2 text-zinc-500 hover:border-electric/50 hover:text-electric-soft')}>
        {assignee
          ? <><Avatar name={assignee.name} color={assignee.color} size={18} /><span className="max-w-[140px] truncate text-[11.5px] text-zinc-200">{assignee.name}</span></>
          : <span className="flex items-center gap-1 text-[10.5px] font-600"><Icon name="UserPlus" size={12} />{label}</span>}
      </button>
      {open && (<>
        <div className="fixed inset-0 z-20" onClick={() => setOpen(false)} />
        <div className="absolute right-0 top-full z-30 mt-1 w-56 rounded-xl border border-line bg-ink-850 p-1.5 shadow-xl">
          <div className="mb-1 flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-2 py-1.5">
            <Icon name="Search" size={12} className="text-zinc-500" />
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Search or add a person…"
              className="min-w-0 flex-1 bg-transparent text-[12px] text-zinc-200 placeholder:text-zinc-600 focus:outline-none" />
          </div>
          <div className="max-h-48 space-y-0.5 overflow-y-auto no-scrollbar">
            {assignee && (
              <button onClick={() => { onPick(null); setOpen(false); }}
                className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] text-zinc-400 hover:bg-ink-800">
                <span className="flex h-[22px] w-[22px] items-center justify-center rounded-full border border-zinc-700"><Icon name="UserX" size={12} /></span>Unassign
              </button>
            )}
            {list.map(p => (
              <button key={p.id || p.name} onClick={() => { onPick({ name:p.name, color:p.color }); setOpen(false); }}
                className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-800">
                <Avatar name={p.name} color={p.color} size={22} />
                <span className="min-w-0 flex-1"><span className="block truncate text-[12px] text-zinc-200">{p.name}</span><span className="block truncate text-[10.5px] text-zinc-500">{p.role}</span></span>
              </button>
            ))}
            {nm && !exists && (
              <button onClick={() => { onPick({ name:nm, color: ASSIGN_PALETTE[nm.length % ASSIGN_PALETTE.length] }); setOpen(false); }}
                className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-800">
                <span className="flex h-[22px] w-[22px] items-center justify-center rounded-full bg-electric/20 text-electric-soft"><Icon name="Plus" size={13} /></span>
                <span className="text-[12px] text-zinc-200">Add “{nm}”</span>
              </button>
            )}
            {!list.length && !nm && <div className="px-2 py-2 text-center text-[11px] text-zinc-600">No one yet — type a name to add</div>}
          </div>
        </div>
      </>)}
    </div>
  );
}

/* Multi-select leads for a whole board (“tab”). Pick several people from the
   crew database, add brand-new names, and remove with the chip ×. */
function LeadPicker({ leads, people, onToggle }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState('');
  const [pos, setPos] = useState({ left:0, top:0 });
  const addRef = useRef(null);
  const menuRef = useRef(null);
  useEffect(() => {
    if (!open) return;
    const close = (e) => { if (menuRef.current && e && menuRef.current.contains(e.target)) return; setOpen(false); };
    window.addEventListener('scroll', close, true);
    return () => window.removeEventListener('scroll', close, true);
  }, [open]);
  const openMenu = () => {
    if (!open && addRef.current) {
      const r = addRef.current.getBoundingClientRect();
      const W = 240;
      const left = Math.max(8, Math.min(r.left, window.innerWidth - W - 8));
      const top = Math.max(8, Math.min(r.bottom + 6, window.innerHeight - 320));
      setPos({ left, top });
    }
    setQ(''); setOpen(o => !o);
  };
  const has = (name) => leads.some(l => l.name.toLowerCase() === name.toLowerCase());
  const list = people.filter(p => (p.name + ' ' + (p.role || '')).toLowerCase().includes(q.trim().toLowerCase()));
  const nm = q.trim();
  const exists = people.some(p => p.name.toLowerCase() === nm.toLowerCase());
  return (
    <div className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
      {leads.map(l => (
        <span key={l.name} className="inline-flex items-center gap-1 rounded-full border border-line bg-ink-800 py-0.5 pl-0.5 pr-1">
          <Avatar name={l.name} color={l.color || '#3a3a46'} size={16} />
          <span className="max-w-[110px] truncate text-[11px] text-zinc-200">{l.name}</span>
          <button onClick={() => onToggle({ name:l.name, color:l.color })} title="Remove lead"
            className="flex h-4 w-4 items-center justify-center rounded-full text-zinc-500 transition-colors hover:bg-ink-700 hover:text-zinc-200"><Icon name="X" size={10} /></button>
        </span>
      ))}
      <button ref={addRef} onClick={openMenu}
        className="inline-flex items-center gap-1 rounded-full border border-dashed border-zinc-700 px-2 py-0.5 text-[10.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
        <Icon name="UserPlus" size={12} />{leads.length ? 'Add' : 'Assign leads'}
      </button>
      {open && ReactDOM.createPortal(<>
        <div className="fixed inset-0 z-[55]" onClick={() => setOpen(false)} />
        <div ref={menuRef} style={{ position:'fixed', left:pos.left, top:pos.top, width:240, zIndex:60 }}
          className="rounded-xl border border-line bg-ink-850 p-1.5 shadow-2xl">
          <div className="mb-1 flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-2 py-1.5">
            <Icon name="Search" size={12} className="text-zinc-500" />
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Search or add a person…"
              className="min-w-0 flex-1 bg-transparent text-[12px] text-zinc-200 placeholder:text-zinc-600 focus:outline-none" />
          </div>
          <div className="max-h-48 space-y-0.5 overflow-y-auto no-scrollbar">
            {list.map(p => {
              const on = has(p.name);
              return (
                <button key={p.id || p.name} onClick={() => onToggle({ name:p.name, color:p.color })}
                  className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-800">
                  <Avatar name={p.name} color={p.color} size={22} />
                  <span className="min-w-0 flex-1"><span className="block truncate text-[12px] text-zinc-200">{p.name}</span><span className="block truncate text-[10.5px] text-zinc-500">{p.role}</span></span>
                  <span className={cx('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border transition-colors',
                    on ? 'border-electric bg-electric text-white' : 'border-zinc-600 text-transparent')}><Icon name="Check" size={11} sw={3} /></span>
                </button>
              );
            })}
            {nm && !exists && (
              <button onClick={() => { onToggle({ name:nm, color: ASSIGN_PALETTE[nm.length % ASSIGN_PALETTE.length] }); setQ(''); }}
                className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-ink-800">
                <span className="flex h-[22px] w-[22px] items-center justify-center rounded-full bg-electric/20 text-electric-soft"><Icon name="Plus" size={13} /></span>
                <span className="text-[12px] text-zinc-200">Add “{nm}”</span>
              </button>
            )}
            {!list.length && !nm && <div className="px-2 py-2 text-center text-[11px] text-zinc-600">No one yet — type a name to add</div>}
          </div>
        </div>
      </>, document.body)}
    </div>
  );
}

/* Controlled card — items, pending queue & callbacks come from the module
   so a single Sync button can deliver queued tasks to every department.
   Drag the grip to reorder; the trash button deletes the whole board. */
function DeptCard({ d, index, dragging, dragOver, onToggle, onQueue, onUnqueue, onRemove, onRemoveItem, onRename, onIcon, onLead, onCrew, people, drag }) {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState('');
  const [grab, setGrab] = useState(false);
  const [editing, setEditing] = useState(false);
  const [name, setName] = useState(d.name);
  const [pick, setPick] = useState(false);
  const startEdit  = () => { setName(d.name); setEditing(true); };
  const saveEdit   = () => { const v = name.trim(); if (v) onRename(d.id, v); setEditing(false); setPick(false); };
  const cancelEdit = () => { setEditing(false); setPick(false); setName(d.name); };
  const items = d.items, pending = d.pending;
  const done = items.filter(i => i[1]).length;
  const pct = items.length ? Math.round((done / items.length) * 100) : 0;
  const st = statusFor(items);

  const submit = () => { const v = draft.trim(); if (!v) return; onQueue(d.id, v); setDraft(''); setOpen(true); };

  return (
    <div
      draggable={grab}
      onDragStart={e => drag.start(e, index)}
      onDragEnter={() => drag.enter(index)}
      onDragOver={e => e.preventDefault()}
      onDrop={e => { e.preventDefault(); drag.drop(index); }}
      onDragEnd={() => { setGrab(false); drag.end(); }}
      className={cx('fade-up rounded-xl border bg-ink-900 transition-all',
        dragging ? 'opacity-40' : '',
        dragOver ? 'border-electric ring-2 ring-electric/40'
          : pending.length ? 'border-proflow/40' : 'border-line hover:border-ink-600')}
      style={{ borderLeftWidth:'3px', borderLeftColor:st.color }}>
      <div className="flex items-center gap-1 p-3.5 pl-2">
        <span onMouseDown={() => setGrab(true)} onMouseUp={() => setGrab(false)} onMouseLeave={() => setGrab(false)}
          title="Drag to reorder"
          className="flex h-9 w-5 shrink-0 cursor-grab items-center justify-center text-zinc-600 transition-colors hover:text-zinc-300 active:cursor-grabbing">
          <Icon name="GripVertical" size={15} />
        </span>
        {editing ? (
          <div className="flex min-w-0 flex-1 flex-col gap-2">
            <div className="flex items-center gap-2">
              <button onClick={() => setPick(p => !p)} title="Choose a logo"
                className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-electric-soft ring-1 ring-electric/40 transition-colors hover:bg-ink-600">
                <Icon name={d.icon} size={18} />
              </button>
              <input autoFocus value={name} onChange={e => setName(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter') saveEdit(); if (e.key === 'Escape') cancelEdit(); }}
                className="min-w-0 flex-1 rounded-md border border-line bg-ink-850 px-2 py-1.5 text-[13.5px] font-600 text-zinc-100 focus:border-electric/60 focus:outline-none" />
              <button onClick={saveEdit} title="Save name"
                className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-mint/15 text-mint transition-colors hover:bg-mint/25"><Icon name="Check" size={15} /></button>
              <button onClick={cancelEdit} title="Cancel"
                className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-ink-700 hover:text-zinc-300"><Icon name="X" size={15} /></button>
            </div>
            {pick && (
              <div className="flex flex-wrap gap-1.5 rounded-lg border border-line bg-ink-850 p-2">
                {DEPT_ICONS.map(ic => (
                  <button key={ic} onClick={() => { onIcon(d.id, ic); setPick(false); }} title={ic}
                    className={cx('flex h-8 w-8 items-center justify-center rounded-md transition-colors',
                      d.icon === ic ? 'bg-electric text-white' : 'bg-ink-700 text-zinc-400 hover:bg-ink-600 hover:text-zinc-200')}>
                    <Icon name={ic} size={15} />
                  </button>
                ))}
              </div>
            )}
            <div className="flex items-center gap-2 pt-0.5">
              <span className="text-[11px] font-600 uppercase tracking-[0.1em] text-zinc-500">Crew</span>
              <div className="inline-flex items-center gap-1 rounded-lg border border-line bg-ink-850 p-0.5">
                <button onClick={() => onCrew(d.id, (d.crew || 0) - 1)} className="flex h-6 w-6 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-ink-700 hover:text-zinc-200"><Icon name="Minus" size={13} /></button>
                <input value={d.crew} onChange={e => onCrew(d.id, parseInt(e.target.value, 10) || 0)}
                  className="w-9 bg-transparent text-center text-[12.5px] font-700 text-zinc-100 focus:outline-none" />
                <button onClick={() => onCrew(d.id, (d.crew || 0) + 1)} className="flex h-6 w-6 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-ink-700 hover:text-zinc-200"><Icon name="Plus" size={13} /></button>
              </div>
              <span className="text-[11px] text-zinc-600">total headcount</span>
            </div>
          </div>
        ) : (
          <button onClick={() => setOpen(o => !o)} className="flex min-w-0 flex-1 items-center gap-3 text-left">
            <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-zinc-300">
              <Icon name={d.icon} size={18} />
            </span>
            <div className="min-w-0 flex-1">
              <div className="flex flex-wrap items-center gap-2">
                <span className="truncate text-[13.5px] font-600 text-zinc-100">{d.name}</span>
                <StatusBadge st={st} />
                {pending.length > 0 && (
                  <span className="inline-flex items-center gap-1 rounded-full border border-proflow/40 bg-proflow/12 px-2 py-[3px] text-[10px] font-700 uppercase tracking-[0.1em] text-proflow-soft">
                    <span className="proflow-pulse h-1 w-1 rounded-full bg-proflow" />{pending.length} queued
                  </span>
                )}
              </div>
              <div className="mt-0.5 flex items-center gap-1.5 text-[11.5px] text-zinc-500">
                <Icon name="Users" size={11} /><span className="whitespace-nowrap">{d.crew} crew</span>
              </div>
            </div>
          </button>
        )}
        <div className="flex shrink-0 items-center gap-1">
          <span className="mr-1 font-mono text-[11px] font-600" style={{ color:st.color }}>{done}/{items.length}</span>
          <button onClick={() => editing ? saveEdit() : startEdit()} title="Rename / change logo"
            className={cx('flex h-7 w-7 items-center justify-center rounded-md transition-colors',
              editing ? 'bg-electric/15 text-electric-soft' : 'text-zinc-600 hover:bg-electric/15 hover:text-electric-soft')}>
            <Icon name={editing ? 'Check' : 'Pencil'} size={13} />
          </button>
          <button onClick={() => onRemove(d.id)} title="Delete board"
            className="flex h-7 w-7 items-center justify-center rounded-md text-zinc-600 transition-colors hover:bg-proflow/15 hover:text-proflow-soft">
            <Icon name="Trash2" size={13} />
          </button>
          <button onClick={() => setOpen(o => !o)} className="flex h-7 w-7 items-center justify-center text-zinc-500 transition-colors hover:text-zinc-300">
            <Icon name={open ? 'ChevronUp' : 'ChevronDown'} size={15} />
          </button>
        </div>
      </div>
      <div className="flex items-start gap-2 px-3.5 pb-1.5 pl-9">
        <span className="mt-1 shrink-0 text-[10px] font-700 uppercase tracking-[0.12em] text-zinc-600">{(d.leads || []).length > 1 ? 'Leads' : 'Lead'}</span>
        <LeadPicker leads={d.leads || []} people={people} onToggle={p => onLead(d.id, p)} />
      </div>
      <div className="px-3.5 pb-2.5">
        <div className="h-2 overflow-hidden rounded-full" style={{ background:st.color + '24' }}>
          <div className="h-full rounded-full transition-all duration-500"
            style={{ width:`${pct}%`, background:st.color, boxShadow:`0 0 10px ${st.color}80` }} />
        </div>
      </div>
      {open && (
        <div className="fade-up space-y-1 p-3 pt-2.5">
          <div className="mb-1 text-[11px] text-zinc-500">{d.note}</div>
          {items.map((it, i) => (
            <div key={i} className="group flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-ink-800">
              <button onClick={() => onToggle(d.id, i)} className="flex min-w-0 flex-1 items-center gap-2.5 text-left">
                <span className={cx('flex h-[17px] w-[17px] shrink-0 items-center justify-center rounded-[5px] border transition-colors',
                  it[1] ? 'border-mint bg-mint text-ink-950' : 'border-zinc-600 text-transparent group-hover:border-zinc-400')}>
                  <Icon name="Check" size={11} sw={3} />
                </span>
                <span className={cx('truncate text-[12.5px]', it[1] ? 'text-zinc-500 line-through' : 'text-zinc-300')}>{it[0]}</span>
              </button>
              {it[2] === 'synced' && (
                <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-proflow/12 px-1.5 py-px text-[9px] font-700 uppercase tracking-[0.08em] text-proflow-soft">
                  <Icon name="Sparkles" size={9} />received
                </span>
              )}
              <button onClick={() => onRemoveItem(d.id, i)} title="Delete entry"
                className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-zinc-600 opacity-0 transition-all hover:bg-proflow/15 hover:text-proflow-soft group-hover:opacity-100">
                <Icon name="Trash2" size={12} />
              </button>
            </div>
          ))}

          {/* queued (not yet synced) tasks */}
          {pending.map((p, i) => (
            <div key={'p' + i} className="flex items-center gap-2.5 rounded-lg border border-dashed border-proflow/40 bg-proflow/[0.06] px-2 py-1.5">
              <span className="flex h-[17px] w-[17px] shrink-0 items-center justify-center rounded-[5px] border border-proflow/50 text-proflow-soft">
                <Icon name="Clock" size={10} />
              </span>
              <span className="text-[12.5px] text-zinc-200">{p}</span>
              <span className="ml-auto text-[9px] font-700 uppercase tracking-[0.1em] text-proflow-soft">pending sync</span>
              <button onClick={() => onUnqueue(d.id, i)} className="text-zinc-500 transition-colors hover:text-zinc-200"><Icon name="X" size={12} /></button>
            </div>
          ))}

          {/* add-task row */}
          <div className="mt-1.5 flex items-center gap-2 rounded-lg border border-line bg-ink-850 px-2 py-1.5 focus-within:border-proflow/50">
            <Icon name="Plus" size={13} className="text-zinc-500" />
            <input value={draft} onChange={e => setDraft(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter') submit(); }}
              placeholder={`Assign a task to ${d.name}…`}
              className="min-w-0 flex-1 bg-transparent text-[12.5px] text-zinc-200 placeholder:text-zinc-600 focus:outline-none" />
            <button onClick={submit} disabled={!draft.trim()}
              className={cx('rounded-md px-2 py-1 text-[11px] font-600 transition-colors',
                draft.trim() ? 'bg-proflow/15 text-proflow-soft hover:bg-proflow/25' : 'text-zinc-600')}>Queue</button>
          </div>
        </div>
      )}
    </div>
  );
}

const DEPTS_KEY = 'silverlines.depts.' + (window.PROJ_ID || 'crimson') + '.v1';
function deptsSeed(blank) {
  const db = readPeopleDB();
  const colorOf = nm => (db.find(x => x.name === nm) || {}).color;
  /* real productions get the department shells (structure, empty checklists);
     the demo carries the illustrative Crimson items + leads. */
  return DEPARTMENTS.map(d => blank
    ? { ...d, head:'Unassigned', crew:0, note:'', items:[], pending:[], leads:[] }
    : { ...d, items: d.items.map(x => [x[0], x[1]]), pending: [], leads: d.head && !/^(TBD|Unassigned)/i.test(d.head) ? [{ name: d.head, color: colorOf(d.head) }] : [] });
}
function deptsLoad() {
  const s = window.SLStore.get(DEPTS_KEY);
  if (Array.isArray(s) && s.length) return s;
  return deptsSeed(!!window.PROJ_BLANK);
}

function DepartmentsModule({ mobile }) {
  /* whole department list lives in state (persisted per project) so boards can be
     reordered, sorted, added and deleted — each carries items + pending queue */
  const [depts, setDeptsState] = useState(deptsLoad);
  const setDepts = next => setDeptsState(prev => {
    const v = typeof next === 'function' ? next(prev) : next;
    window.SLStore.set(DEPTS_KEY, v);
    return v;
  });
  const [syncing, setSyncing] = useState(false);
  const [toast, setToast] = useState(null);
  const [sortMode, setSortMode] = useState('custom');   // custom | ready | work
  const [adding, setAdding] = useState(false);
  const [newName, setNewName] = useState('');
  const [newIcon, setNewIcon] = useState('Boxes');
  const [newPick, setNewPick] = useState(false);
  const [dragIdx, setDragIdx] = useState(null);
  const [overIdx, setOverIdx] = useState(null);

  const toggle  = (id, i)     => setDepts(ds => ds.map(d => d.id === id ? { ...d, items: d.items.map((x, k) => k === i ? [x[0], !x[1], x[2], x[3]] : x) } : d));
  const queue   = (id, label) => setDepts(ds => ds.map(d => d.id === id ? { ...d, pending: [...d.pending, label] } : d));
  const unqueue = (id, i)     => setDepts(ds => ds.map(d => d.id === id ? { ...d, pending: d.pending.filter((_, k) => k !== i) } : d));
  const removeItem = (id, i)  => {
    const prev = depts; const d0 = depts.find(d => d.id === id);
    const it = d0 && d0.items[i];
    setDepts(ds => ds.map(d => d.id === id ? { ...d, items: d.items.filter((_, k) => k !== i) } : d));
    window.pushUndo && window.pushUndo(`Removed "${(it && it[0]) || 'item'}"`, () => setDepts(prev));
  };
  const removeDept = (id)     => {
    const prev = depts; const d0 = depts.find(d => d.id === id) || {};
    setDepts(ds => ds.filter(d => d.id !== id));
    window.pushUndo && window.pushUndo(`Removed board "${d0.name || ''}"`, () => setDepts(prev));
  };
  const assign     = (id, p) => setDepts(ds => ds.map(d => {
    if (d.id !== id) return d;
    const leads = d.leads || [];
    const at = leads.findIndex(l => l.name.toLowerCase() === p.name.toLowerCase());
    return { ...d, leads: at >= 0 ? leads.filter((_, k) => k !== at) : [...leads, { name: p.name, color: p.color }] };
  }));
  const peopleDB   = readPeopleDB();
  const rename     = (id, nm) => setDepts(ds => ds.map(d => d.id === id ? { ...d, name: nm } : d));
  const setIcon    = (id, ic) => setDepts(ds => ds.map(d => d.id === id ? { ...d, icon: ic } : d));
  const setCrew    = (id, n)  => setDepts(ds => ds.map(d => d.id === id ? { ...d, crew: Math.max(0, n) } : d));

  const addDept = () => {
    const v = newName.trim(); if (!v) return;
    setDepts(ds => [...ds, { id:'d' + Date.now(), name:v, icon:newIcon, head:'Unassigned', crew:0, tone:'neutral', note:'New department board', items:[], pending:[], leads:[] }]);
    setNewName(''); setNewIcon('Boxes'); setNewPick(false); setAdding(false); setSortMode('custom');
  };

  /* displayed order — sorted view or manual custom order */
  const ordered = sortMode === 'custom' ? depts : [...depts].sort((a, b) => {
    const rank = sortMode === 'ready' ? READY_RANK : WORK_RANK;
    return rank[statusFor(a.items).key] - rank[statusFor(b.items).key];
  });

  /* drag-to-reorder — always rewrites the displayed order, locking it to custom */
  const drag = {
    start: (e, i) => { setDragIdx(i); setSortMode('custom'); if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; },
    enter: (i) => setOverIdx(i),
    drop: (to) => {
      const from = dragIdx;
      setDragIdx(null); setOverIdx(null);
      if (from === null || from === to) return;
      const a = [...ordered]; const [m] = a.splice(from, 1); a.splice(to, 0, m);
      setDepts(a);
    },
    end: () => { setDragIdx(null); setOverIdx(null); },
  };

  const pendingTotal = depts.reduce((a, d) => a + d.pending.length, 0);
  const pendingDepts = depts.filter(d => d.pending.length).length;

  const sync = () => {
    if (!pendingTotal || syncing) return;
    setSyncing(true);
    const n = pendingTotal, nd = pendingDepts;
    setTimeout(() => {
      setDepts(ds => ds.map(d => d.pending.length
        ? { ...d, items: [...d.items, ...d.pending.map(l => [l, false, 'synced'])], pending: [] }
        : d));
      setSyncing(false);
      setToast(`${n} task${n > 1 ? 's' : ''} delivered to ${nd} department${nd > 1 ? 's' : ''}`);
      setTimeout(() => setToast(null), 3200);
    }, 850);
  };

  const locked = depts.filter(d => statusFor(d.items).key === 'locked').length;
  const totalCrew = depts.reduce((a, d) => a + d.crew, 0);

  const sorts = [
    { id:'custom', label:'Manual',      icon:'GripVertical' },
    { id:'ready',  label:'Ready first', icon:'ArrowUpNarrowWide' },
    { id:'work',   label:'Needs work',  icon:'ArrowDownWideNarrow' },
  ];

  return (
    <div>
      <div className="mb-4 grid grid-cols-3 gap-3">
        {[
          { ic:'LayoutGrid', l:'Departments', v:depts.length, tone:'#F57F45' },
          { ic:'CircleCheck', l:'Locked', v:`${locked}/${depts.length}`, tone:'#22D48A' },
          { ic:'Users', l:'Total crew', v:totalCrew, tone:'#BD2555' },
        ].map(s => (
          <div key={s.l} className="rounded-xl border border-line bg-ink-900 p-3">
            <div className="flex items-center gap-1.5 text-[10.5px] uppercase tracking-[0.1em] text-zinc-500"><Icon name={s.ic} size={12} />{s.l}</div>
            <div className="mt-1 font-display text-[26px] font-700 leading-none" style={{ color:s.tone }}>{s.v}</div>
          </div>
        ))}
      </div>

      {/* sort toolbar */}
      <div className="mb-3 flex flex-wrap items-center justify-between gap-3">
        <div className="flex items-center gap-2 text-[12px] text-zinc-500">
          <Icon name="GripVertical" size={13} className="text-zinc-600" />
          <span>Drag a board to reorder, or sort by readiness</span>
        </div>
        <div className="inline-flex rounded-lg border border-line bg-ink-850 p-1">
          {sorts.map(s => (
            <button key={s.id} onClick={() => setSortMode(s.id)}
              className={cx('flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[11.5px] font-600 transition-colors',
                sortMode === s.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
              <Icon name={s.icon} size={13} />{!mobile && s.label}
            </button>
          ))}
        </div>
      </div>

      {/* sync toolbar */}
      <div className="mb-3 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-line bg-ink-900 px-3.5 py-2.5">
        <div className="flex items-center gap-2 text-[12px] text-zinc-500">
          <Icon name="MousePointerClick" size={13} className="text-electric-soft" />
          <span>Expand a department to tick prep items or <span className="text-zinc-300">assign a new task</span> — then sync to deliver</span>
        </div>
        <div className="flex items-center gap-2.5">
          {pendingTotal > 0 && (
            <span className="inline-flex items-center gap-1.5 rounded-full border border-proflow/40 bg-proflow/12 px-2.5 py-1 text-[11px] font-700 text-proflow-soft">
              <span className="proflow-pulse h-1.5 w-1.5 rounded-full bg-proflow" />{pendingTotal} queued · {pendingDepts} dept{pendingDepts > 1 ? 's' : ''}
            </span>
          )}
          <button onClick={sync} disabled={!pendingTotal || syncing}
            className={cx('inline-flex items-center gap-2 rounded-lg px-3.5 py-2 text-[12.5px] font-700 transition-colors',
              pendingTotal && !syncing ? 'bg-proflow text-white hover:bg-proflow-deep' : 'cursor-not-allowed bg-ink-700 text-zinc-500')}>
            <Icon name={syncing ? 'Loader' : 'RefreshCw'} size={14} className={syncing ? 'animate-spin' : ''} />
            {syncing ? 'Syncing…' : `Sync${pendingTotal ? ` · ${pendingTotal}` : ''}`}
          </button>
        </div>
      </div>

      {toast && (
        <div className="fade-up mb-3 flex items-center gap-2 rounded-xl border border-mint/40 bg-mint/10 px-3.5 py-2.5 text-[12.5px] font-600 text-mint">
          <Icon name="CheckCircle2" size={15} />{toast}
        </div>
      )}

      {/* masonry-style columns: opening a card only pushes the cards below it
          in its OWN column — the neighbouring column packs to the top as if
          everything were closed */}
      <div className={cx('flex gap-3', mobile ? 'flex-col' : 'items-start')}>
        {(mobile
          ? [ordered]
          : [ordered.filter((_, i) => i % 2 === 0), ordered.filter((_, i) => i % 2 === 1)]
        ).map((col, ci) => (
          <div key={ci} className="flex min-w-0 flex-1 flex-col gap-3">
            {col.map(d => {
              const i = ordered.indexOf(d);
              return (
                <DeptCard key={d.id} d={d} index={i}
                  dragging={dragIdx === i} dragOver={overIdx === i && dragIdx !== null && dragIdx !== i}
                  onToggle={toggle} onQueue={queue} onUnqueue={unqueue} onRemove={removeDept} onRemoveItem={removeItem}
                  onRename={rename} onIcon={setIcon} onLead={assign} onCrew={setCrew} people={peopleDB} drag={drag} />
              );
            })}
          </div>
        ))}
      </div>

      {/* add-board tile — pick a logo from the library, then name it */}
      <div className="mt-3">
        {adding ? (
          <div className="rounded-xl border border-dashed border-electric/50 bg-ink-900 p-3.5">
            <div className="flex items-center gap-2">
              <button onClick={() => setNewPick(p => !p)} title="Choose a logo"
                className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-electric-soft ring-1 ring-electric/40 transition-colors hover:bg-ink-600">
                <Icon name={newIcon} size={18} />
              </button>
              <input autoFocus value={newName} onChange={e => setNewName(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter') addDept(); if (e.key === 'Escape') { setAdding(false); setNewName(''); setNewPick(false); } }}
                placeholder="New department name…"
                className="min-w-0 flex-1 bg-transparent text-[13px] text-zinc-100 placeholder:text-zinc-600 focus:outline-none" />
              <button onClick={addDept} disabled={!newName.trim()}
                className={cx('rounded-md px-2.5 py-1.5 text-[11.5px] font-700 transition-colors', newName.trim() ? 'bg-electric text-white hover:bg-electric-deep' : 'bg-ink-700 text-zinc-600')}>Add</button>
              <button onClick={() => { setAdding(false); setNewName(''); setNewPick(false); }} className="flex h-7 w-7 items-center justify-center text-zinc-500 hover:text-zinc-300"><Icon name="X" size={15} /></button>
            </div>
            {newPick && (
              <div className="mt-2.5 flex flex-wrap gap-1.5 rounded-lg border border-line bg-ink-850 p-2">
                {DEPT_ICONS.map(ic => (
                  <button key={ic} onClick={() => { setNewIcon(ic); setNewPick(false); }} title={ic}
                    className={cx('flex h-8 w-8 items-center justify-center rounded-md transition-colors',
                      newIcon === ic ? 'bg-electric text-white' : 'bg-ink-700 text-zinc-400 hover:bg-ink-600 hover:text-zinc-200')}>
                    <Icon name={ic} size={15} />
                  </button>
                ))}
              </div>
            )}
          </div>
        ) : (
          <button onClick={() => setAdding(true)}
            className="flex w-full items-center justify-center gap-2 rounded-xl border border-dashed border-line py-3.5 text-[12.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
            <Icon name="Plus" size={15} /> Add department board
          </button>
        )}
      </div>
    </div>
  );
}

function CastModule({ mobile, onOpen }) {
  const palette = ['#F57F45','#BD2555','#22D48A','#FFB020','#F43F3E','#F9A06E','#F7A8CF','#5B0952'];
  const [people, setPeople] = useState(PEOPLE.map(p => ({ ...p, kind:'crew' })));
  const { roster, remove: rosterRemove } = useRoster();
  const { flags, setFlag } = useFlags();
  /* people pulled in from the workspace Crew Directory */
  const db = readPeopleDB();
  const fromDirectory = roster
    .map(id => db.find(x => x.id === id)).filter(Boolean)
    .filter(x => !people.some(y => y.name === x.name))
    .map(x => ({ name: x.name, role: x.role, dept: x.dept, color: x.color, phone: x.phone, email: x.email, online: false, kind: 'crew', fromDb: true, _id: x.id }));

  const importPeople = (kind) => (rows) => {
    const added = rows.map((r, i) => ({
      name: pick(r, ['name','actor','crew','member']) || 'Unnamed',
      role: pick(r, ['role','character','position','title']) || (kind === 'cast' ? 'Cast' : 'Crew'),
      dept: pick(r, ['dept','department','category']) || (kind === 'cast' ? 'Cast' : 'Crew'),
      phone: pick(r, ['phone','mobile','cell','tel']) || '—',
      email: pick(r, ['email','mail','e-mail']) || '—',
      color: palette[(people.length + i) % palette.length], online:false, kind, imported:true,
    })).filter(p => p.name !== 'Unnamed');
    if (added.length) setPeople(ps => [...ps, ...added]);
  };
  const sampleCrew = () => ([
    { name:'Russ Calderon', role:'Key Grip', dept:'Grip', phone:'+1 818 555 0131', email:'russ@gripco.co' },
    { name:'Pia Lindqvist', role:'Production Mixer', dept:'Sound', phone:'+1 310 555 0155', email:'pia@boomroom.co' },
    { name:'Mia Sorenson', role:'1st AC', dept:'Camera', phone:'+1 323 555 0188', email:'mia@lensworks.co' },
  ]);
  const sampleCast = () => ([
    { name:'Dorian Wells', role:'Ground Chief', dept:'Cast', phone:'+1 213 555 0166', email:'dorian@talenthaus.co' },
    { name:'Aria Bloom', role:'Co-Pilot', dept:'Cast', phone:'+1 213 555 0172', email:'aria@talenthaus.co' },
  ]);

  const crew = [...people.filter(p => p.kind !== 'cast'), ...fromDirectory];
  const cast = people.filter(p => p.kind === 'cast');

  const Card = ({ p }) => {
    const flag = flags[p.name];
    return (
      <div onClick={() => onOpen(p)} role="button" tabIndex={0}
        className="group flex cursor-pointer items-center gap-3 rounded-xl border border-line bg-ink-900 p-3 text-left transition-colors hover:border-ink-600">
        <div className="relative shrink-0">
          <Avatar name={p.name} color={p.color} size={42} />
          <span className={cx('absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-ink-900', p.online ? 'bg-mint' : 'bg-zinc-600')} />
        </div>
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-1.5"><span className="truncate text-[13.5px] font-600 text-zinc-100">{p.name}</span>
            {p.imported && <span className="rounded bg-electric/15 px-1.5 py-px text-[9px] font-700 text-electric-soft">NEW</span>}
            {p.fromDb && <span className="rounded bg-proflow/15 px-1.5 py-px text-[9px] font-700 text-proflow-soft">DIRECTORY</span>}</div>
          <div className="truncate text-[12px] text-zinc-500">{p.role}</div>
          <div className="mt-1.5" onClick={e => e.stopPropagation()}>
            <FlagPicker name={p.name} flag={flag} setFlag={setFlag} />
          </div>
        </div>
        <div className="flex shrink-0 gap-1.5 transition-opacity" onClick={e => e.stopPropagation()}>
          <a href={`tel:${(p.phone || '').replace(/\s/g, '')}`} className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-400 transition-colors hover:bg-cta hover:text-cta-ink"><Icon name="Phone" size={14} /></a>
          {p.fromDb
            ? <button title="Remove from project" onClick={() => { rosterRemove(p._id, `Removed ${p.name} from the project`); window.toast(`${p.name} removed from the project`, 'UserMinus'); window.notify && window.notify(`${p.name} removed from the project`, 'UserMinus', 'crewdb'); }}
                className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-500 transition-colors hover:text-[#FF4D6D]"><Icon name="UserMinus" size={14} /></button>
            : <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-400 transition-colors hover:bg-cta hover:text-cta-ink"><Icon name="MessageSquare" size={14} /></span>}
        </div>
      </div>
    );
  };
  return (
    <div className="space-y-5">
      {/* import zones */}
      <div className={cx('grid gap-3', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
        <DropImport icon="UserPlus" label="Import crew list" hint="Drag a contact sheet — no manual typing"
          onRows={importPeople('crew')} sample={sampleCrew} compact />
        <DropImport icon="Users" label="Import cast list" hint="Drag a talent roster — CSV maps instantly"
          onRows={importPeople('cast')} sample={sampleCast} compact />
      </div>

      <div>
        <div className="mb-2.5 flex items-center gap-2 text-[11px] font-700 uppercase tracking-[0.12em] text-zinc-500"><Icon name="HardHat" size={13} /> Crew · {crew.length}</div>
        <div className={cx('grid gap-3', mobile ? 'grid-cols-1' : 'grid-cols-2 lg:grid-cols-3')}>
          {crew.map((p, i) => <Card key={p.name + i} p={p} />)}
        </div>
      </div>

      {cast.length > 0 && (
        <div>
          <div className="mb-2.5 flex items-center gap-2 text-[11px] font-700 uppercase tracking-[0.12em] text-zinc-500"><Icon name="Star" size={13} /> Cast · {cast.length}</div>
          <div className={cx('grid gap-3', mobile ? 'grid-cols-1' : 'grid-cols-2 lg:grid-cols-3')}>
            {cast.map((p, i) => <Card key={p.name + i} p={p} />)}
          </div>
        </div>
      )}
    </div>
  );
}

function ContactModal({ person, onClose }) {
  if (!person) return null;
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={onClose}>
      <div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
      <div onClick={e => e.stopPropagation()}
        className="fade-up relative w-full max-w-sm overflow-hidden rounded-2xl border border-line bg-ink-850">
        <div className="h-20" style={{ background:`linear-gradient(120deg, ${person.color}40, transparent)` }} />
        <button onClick={onClose} className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-lg bg-black/40 text-zinc-300 hover:bg-black/60"><Icon name="X" size={16} /></button>
        <div className="-mt-9 px-5 pb-5">
          <Avatar name={person.name} color={person.color} size={64} ring />
          <div className="mt-3 flex items-center gap-2">
            <h3 className="font-display text-[24px] font-600 leading-none text-head">{person.name}</h3>
            <span className={cx('h-2 w-2 rounded-full', person.online ? 'bg-mint' : 'bg-zinc-600')} />
          </div>
          <div className="text-[13px] text-zinc-400">{person.role} · {person.dept}</div>

          <div className="mt-4 space-y-2">
            <a href={`tel:${person.phone.replace(/\s/g, '')}`} className="flex items-center gap-3 rounded-xl border border-line bg-ink-900 px-3 py-2.5 text-[13px] text-zinc-200 hover:border-ink-600">
              <Icon name="Phone" size={15} className="text-zinc-500" /><span className="font-mono">{person.phone}</span></a>
            <a href={`mailto:${person.email}`} className="flex items-center gap-3 rounded-xl border border-line bg-ink-900 px-3 py-2.5 text-[13px] text-zinc-200 hover:border-ink-600">
              <Icon name="Mail" size={15} className="text-zinc-500" /><span className="truncate">{person.email}</span></a>
          </div>
          <div className="mt-4 grid grid-cols-3 gap-2">
            {[['Phone','Call'],['MessageSquare','Message'],['Video','Meet']].map(([ic, l]) => (
              <button key={l}
                onClick={() => window.toast(l === 'Call' ? `Calling ${person.name.split(' ')[0]}…` : l === 'Message' ? `Message thread with ${person.name.split(' ')[0]} opened` : `Meet link sent to ${person.name.split(' ')[0]}`, ic)}
                className="flex flex-col items-center gap-1.5 rounded-xl bg-ink-700 py-3 text-[11.5px] font-600 text-zinc-200 transition-colors hover:bg-cta hover:text-cta-ink">
                <Icon name={ic} size={17} />{l}
              </button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function PreProdModule({ mobile }) {
  const { subtabs, setSubtab } = useContext(AccessContext);
  const tab = (subtabs && subtabs.preprod) || 'story';
  const setTab = id => setSubtab('preprod', id);
  const [contact, setContact] = useState(null);
  const tabs = [
    { id:'story', label:'Storyboard', icon:'LayoutGrid' },
    { id:'depts', label:'Departments', icon:'Boxes' },
    { id:'shots', label:'Shot List', icon:'ListChecks' },
    { id:'breakdown', label:'Breakdown', icon:'PackageSearch' },
    { id:'cater', label:'Catering & Parking', icon:'UtensilsCrossed' },
    { id:'cast',  label:'Cast & Crew', icon:'Users' },
  ];
  return (
    <div className="fade-up">
      <SectionTitle kicker="Phase 02 · Pre-Production" icon="Clapperboard" title="ProFlow Pre-Production"
        action={<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-proflow/30 bg-proflow/10 px-3 py-1.5">
          <span className="h-1.5 w-1.5 rounded-full bg-proflow proflow-pulse" />
          <span className="text-[12px] font-600 text-proflow-soft">ProFlow engine live</span>
        </div>} />

      {/* tab bar */}
      <div className="mb-5 inline-flex rounded-xl border border-line bg-ink-850 p-1">
        {tabs.map(t => (
          <button key={t.id} onClick={() => setTab(t.id)}
            className={cx('flex items-center gap-2 rounded-lg px-3.5 py-2 font-display text-[12.5px] transition-colors',
              tab === t.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
            <Icon name={t.icon} size={14} />{!mobile && t.label}
          </button>
        ))}
      </div>

      {tab === 'story' && <StoryboardModule mobile={mobile} />}
      {tab === 'depts' && <DepartmentsModule mobile={mobile} />}
      {tab === 'shots' && <ShotList mobile={mobile} />}
      {tab === 'breakdown' && <BreakdownModule mobile={mobile} />}
      {tab === 'cater' && <CateringTab mobile={mobile} />}
      {tab === 'cast'  && <CastModule mobile={mobile} onOpen={setContact} />}

      <ContactModal person={contact} onClose={() => setContact(null)} />
    </div>
  );
}

window.PreProdModule = PreProdModule;


/* ===== mod_production ===== */
/* ============================================================
   MODULE 4 — Production Pipeline · Full Call Sheet
   Header + conditions · Locations 1-3 (links + maps) · Nearest
   hospital · Crew call · Cast call · Shooting schedule
   Exportable to PDF (print) or a shareable web link (no app needed)
   ============================================================ */
function StatBlock({ icon, label, value, sub, tone = 'neutral', onValue, onSub }) {
  const tones = { neutral:'text-zinc-300', electric:'text-electric-soft', amber:'text-amber', mint:'text-mint', danger:'text-[#FF4D6D]' };
  return (
    <div className="rounded-xl border border-line bg-ink-900 p-3.5">
      <div className="mb-2 flex items-center gap-2 text-[11px] font-600 uppercase tracking-[0.1em] text-zinc-500"><Icon name={icon} size={13} />{label}</div>
      <div className={cx('font-display text-[26px] font-700 leading-none', tones[tone])}><Edit value={value} onChange={onValue} className="font-display text-[26px] font-700" /></div>
      <div className="mt-1 text-[11.5px] text-zinc-500"><Edit value={sub} onChange={onSub} className="text-[11.5px]" placeholder="Add note…" /></div>
    </div>
  );
}

const SCHEDULE = [
  { time:'06:00', tag:'Call', label:'General crew call · Breakfast', dept:'All', tone:'electric' },
  { time:'07:00', tag:'Shoot', label:'SC 12A — Cockpit, Dawn Ignition', dept:'A-Cam · Lena Frost', scene:'12A', dur:'1h 30m' },
  { time:'08:30', tag:'Move', label:'Company move → Backlot Runway', dept:'Grip · Electric', tone:'amber' },
  { time:'09:15', tag:'Shoot', label:'SC 12B — Tarmac Walk, Hero Shot', dept:'A+B Cam · Talent x4', scene:'12B', dur:'2h 00m' },
  { time:'11:15', tag:'Lunch', label:'Lunch — 30 min', dept:'All', tone:'mint' },
  { time:'12:00', tag:'Shoot', label:'SC 14 — Hangar Reveal', dept:'Crane · Lena Frost', scene:'14', dur:'1h 10m' },
  { time:'13:15', tag:'Shoot', label:'SC 18 — Ascent VFX Plate', dept:'Bluescreen', scene:'18', dur:'0h 45m' },
  { time:'18:40', tag:'Shoot', label:'SC 21 — Sunset Glide (Magic Hour)', dept:'A-Cam · Steadicam', scene:'21', tone:'amber', dur:'0h 40m' },
];
const TAG_TONES = { Call:'electric', Shoot:'neutral', Move:'amber', Lunch:'mint', Wrap:'electric' };

/* ---- schedule clock helpers: derive call-sheet times from scene durations ---- */
function parseDur(s) { if (!s) return 0; const h = /(\d+)\s*h/i.exec(s); const m = /(\d+)\s*m/i.exec(s); return (h ? +h[1] : 0) * 60 + (m ? +m[1] : 0); }
function parseClock(t) { const m = /(\d{1,2}):(\d{2})/.exec(t || ''); return m ? (+m[1]) * 60 + (+m[2]) : null; }
function fmtClock(mins) { mins = ((Math.round(mins) % 1440) + 1440) % 1440; return String(Math.floor(mins / 60)).padStart(2, '0') + ':' + String(mins % 60).padStart(2, '0'); }

function SheetSection({ icon, title, n, right, children }) {
  return (
    <div className="border-t border-line">
      <div className="flex items-center justify-between px-5 py-3">
        <div className="flex items-center gap-2">
          {n && <span className="flex h-5 w-5 items-center justify-center rounded-md bg-electric/15 font-mono text-[11px] font-700 text-electric-soft">{n}</span>}
          <Icon name={icon} size={14} className="text-zinc-400" />
          <h4 className="font-display text-[13px] font-700 uppercase tracking-[0.08em] text-head">{title}</h4>
        </div>
        {right}
      </div>
      <div className="px-5 pb-5">{children}</div>
    </div>
  );
}

function LocationCard({ loc, onEdit }) {
  const [links, setLinks] = useState([]);
  const [adding, setAdding] = useState(false);
  const [lbl, setLbl] = useState('');
  return (
    <div className="rounded-xl border border-line bg-ink-900 p-3.5">
      <div className="flex items-start justify-between gap-2">
        <div className="min-w-0">
          <div className="flex items-center gap-2">
            <span className="flex h-5 w-5 items-center justify-center rounded-md bg-ink-700 font-mono text-[11px] font-700 text-zinc-300">{loc.no}</span>
            <span className="truncate text-[13.5px] font-700 text-head"><Edit value={loc.name} onChange={v => onEdit('name', v)} className="text-[13.5px] font-700 text-head" /></span>
          </div>
          <div className="mt-0.5 text-[11px] text-zinc-500">{loc.kind}</div>
        </div>
        <a href={loc.map} target="_blank" rel="noopener noreferrer"
          className="no-print flex shrink-0 items-center gap-1 rounded-lg border border-line bg-ink-850 px-2 py-1 text-[11px] font-600 text-electric-soft hover:border-electric/40">
          <Icon name="Map" size={12} /> Map<Icon name="ArrowUpRight" size={11} />
        </a>
      </div>
      <div className="mt-2 flex items-start gap-1.5 text-[12px] text-zinc-300">
        <Icon name="MapPin" size={13} className="mt-0.5 shrink-0 text-zinc-500" />
        <Edit value={loc.address} onChange={v => onEdit('address', v)} className="text-[12px] text-zinc-300" />
      </div>
      <div className="mt-1 flex items-center gap-1.5 text-[11.5px] text-zinc-500">
        <Icon name="Car" size={12} /><Edit value={loc.parking} onChange={v => onEdit('parking', v)} className="text-[11.5px] text-zinc-500" />
      </div>
      {/* links */}
      <div className="mt-2.5 flex flex-wrap items-center gap-1.5">
        {links.map((l, i) => (
          <a key={i} href="#" onClick={e => e.preventDefault()} className="flex items-center gap-1 rounded-full border border-line bg-ink-850 px-2 py-0.5 text-[10.5px] font-600 text-zinc-300">
            <Icon name="Link" size={10} />{l}
          </a>
        ))}
        {adding ? (
          <span className="no-print flex items-center gap-1">
            <input autoFocus value={lbl} onChange={e => setLbl(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && lbl.trim()) { setLinks(x => [...x, lbl.trim()]); setLbl(''); setAdding(false); } if (e.key === 'Escape') setAdding(false); }}
              placeholder="Label (e.g. Sides PDF)" className="w-32 rounded-md border border-line bg-ink-850 px-2 py-0.5 text-[11px] text-zinc-200 outline-none" />
          </span>
        ) : (
          <button onClick={() => setAdding(true)} className="no-print flex items-center gap-1 rounded-full border border-dashed border-ink-600 px-2 py-0.5 text-[10.5px] font-600 text-zinc-500 hover:border-electric/40 hover:text-electric-soft">
            <Icon name="Plus" size={10} /> Add link
          </button>
        )}
      </div>
    </div>
  );
}

function CallDoc({ mobile }) {
  const [scenes, setScenes] = useStoryboard();
  const [activeSpot] = useActiveSpot();
  const view = scenes.filter(s => inActiveSpot(s, activeSpot));
  const days = shootDayCount(scenes);
  const [day, setDay] = useState(1);
  const [ov, setOv] = useState(() => cdAll()[1] || {});
  useEffect(() => { setOv(cdAll()[day] || {}); }, [day]);
  const patchOv = p => { const all = cdAll(); all[day] = { ...(all[day] || {}), ...p }; cdSetAll(all); setOv({ ...all[day] }); };
  /* A5 — per-day approve / publish. A content signature (everything except the
     verdict) detects later edits, so an approved/published day flips back to
     draft without storing a snapshot. */
  const [pubShare, setPubShare] = useState(false);
  const slHashStr = str => { let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0; return h; };
  const sigOf = o => { try { const c = { ...o }; delete c.verdict; return slHashStr(JSON.stringify(c)); } catch (e) { return 0; } };
  const verdict = ov.verdict || { state: 'draft', rev: 0 };
  const stale = verdict.state !== 'draft' && verdict.sig != null && sigOf(ov) !== verdict.sig;
  const acctName = (window.Auth && window.Auth.current() && window.Auth.current().name) || 'Producer';
  const approveDay = () => { patchOv({ verdict: { state: 'approved', rev: verdict.rev || 0, by: acctName, at: new Date().toISOString(), sig: sigOf(ov) } }); window.notify && window.notify('Day ' + day + ' call sheet approved', 'CircleCheck', 'production'); window.toast && window.toast('Day ' + day + ' approved', 'CircleCheck'); };
  const publishDay = () => { patchOv({ verdict: { state: 'published', rev: (verdict.rev || 0) + 1, by: acctName, at: new Date().toISOString(), sig: sigOf(ov) } }); window.notify && window.notify('Day ' + day + ' call sheet published', 'Send', 'production'); setPubShare(true); };

  const dayScenes = view.filter(s => sceneDay(s) === day);
  const unscheduled = view.filter(s => sceneDay(s) === 0);
  const assignDay = (scId, n) => setScenes(ss => ss.map(s => s.id === scId ? { ...s, sched: { ...s.sched, day: n } } : s));

  /* per-day derived call data */
  const callTime = ov.callTime || '06:00';
  const callMer = ov.callMer || 'AM';
  /* sun times: computed from the shoot date + the day's location (when it's geocoded
     in the Location Library), else the static fallback */
  const _loclib = window.SLStore.get('silverlines.loclib.' + (window.PROJ_ID || 'crimson') + '.v1', []) || [];
  const _dayLoc = dayScenes.map(s => s.loc).filter(Boolean)
    .map(nm => _loclib.find(l => l && l.lat != null && l.name && (String(l.name).toLowerCase().includes(String(nm).toLowerCase()) || String(nm).toLowerCase().includes(String(l.name).toLowerCase()))))
    .find(Boolean);
  const _sun = _dayLoc ? sunTimes(dayDate(day), _dayLoc.lat, _dayLoc.lng) : null;
  const stats = ov.stats || {
    sunrise: _sun ? { v: _sun.sunrise, s: 'Golden ' + _sun.sunrise + '–' + _sun.goldenEnd } : { v:'06:14', s:'Golden 06:14–06:52' },
    weather: { v:'61°F', s:'Clear · wind 7mph NW' },
    sunset:  _sun ? { v: _sun.sunset, s: 'Magic hour ' + _sun.magicStart } : { v:'20:09', s:'Magic hour 19:40' },
    hospital:{ v:'4.2 mi', s:'11 min by road' },
  };
  const setStat = (k, f, val) => patchOv({ stats: { ...stats, [k]: { ...stats[k], [f]: val } } });

  /* locations actually used this day */
  const matchLoc = t => {
    const x = (t || '').toLowerCase();
    if (/hangar/.test(x)) return LOCATIONS.find(l => /hangar/i.test(l.name));
    if (/runway|tarmac|backlot/.test(x)) return LOCATIONS.find(l => /runway/i.test(l.name)) || LOCATIONS.find(l => /backlot/i.test(l.name));
    if (/stage|cockpit|bluescreen/.test(x)) return LOCATIONS.find(l => /stage/i.test(l.name));
    return null;
  };
  const locNames = [...new Set(dayScenes.map(s => s.loc).filter(Boolean))];
  const locs = locNames.map((nm, i) => { const m = matchLoc(nm); const base = m || { name:nm, kind:'', address:'', map:'#', parking:'' }; return { ...base, no:i+1, name:nm, ...((ov.locs || {})[nm] || {}) }; });
  const editLoc = (nm, k, v) => patchOv({ locs: { ...(ov.locs || {}), [nm]: { ...((ov.locs || {})[nm] || {}), [k]: v } } });

  /* auto cast & crew for this day, with manual overrides */
  const cast = castForScenes(dayScenes).map(c => ({ ...c, ...((ov.castCall || {})[c.actor] || {}) }));
  const setCastCall = (actor, k, v) => patchOv({ castCall: { ...(ov.castCall || {}), [actor]: { ...((ov.castCall || {})[actor] || {}), [k]: v } } });

  const autoCrew = autoCrewForScenes(dayScenes);
  const autoNames = new Set(autoCrew.map(c => c.name));
  const excused = ov.excused || {};
  const added = ov.added || [];
  const crew = CREW_CALL
    .filter(c => (autoNames.has(c.name) && !excused[c.name]) || added.includes(c.name))
    .map(c => ({ ...c, call: (ov.crewCall || {})[c.name] || c.call, why: (autoCrew.find(a => a.name === c.name) || {}).why || (added.includes(c.name) ? 'Added manually' : '') }));
  const bench = CREW_CALL.filter(c => !crew.some(x => x.name === c.name));
  const excuse = nm => patchOv({ excused: { ...excused, [nm]: true }, added: added.filter(n => n !== nm) });
  const recall = nm => patchOv({ excused: { ...excused, [nm]: false }, added: autoNames.has(nm) ? added : [...new Set([...added, nm])] });
  const setCrewCall = (nm, v) => patchOv({ crewCall: { ...(ov.crewCall || {}), [nm]: v } });

  /* schedule lined from this day's scenes */
  let startMin = parseClock(callTime) ?? 360;
  /* parseClock ignores AM/PM — fold the general call's meridiem into the clock so
     a 6:00 PM call lines the day from 18:00, not 06:00 */
  const _startH = Math.floor(startMin / 60);
  if (callMer === 'PM' && _startH < 12) startMin += 720;
  else if (callMer === 'AM' && _startH === 12) startMin -= 720;
  /* break rows (Setup / Coffee / Lunch / company Move) interleave into the same
     cascade: a hard time repositions the clock like a manual call, otherwise the
     break consumes its duration — so the wrap self-corrects. Stored per day. */
  const breaks = ov.breaks || [];
  const setBreaks = list => patchOv({ breaks: list });
  const addBreak = () => { const lastNo = dayScenes.length ? dayScenes[dayScenes.length - 1].no : null; setBreaks([...breaks, { id:'bk' + Date.now(), type:'Lunch', label:'Lunch', dur:45, afterNo:lastNo, t:null }]); };
  const patchBreak = (id, p) => setBreaks(breaks.map(b => b.id === id ? { ...b, ...p } : b));
  const rmBreak = id => setBreaks(breaks.filter(b => b.id !== id));
  let _clock = startMin + 60;
  const timeline = [];
  const emitBreak = brk => {
    const hard = parseClock(brk.t);
    if (hard != null) _clock = hard;
    const at = fmtClock(_clock);
    _clock += (+brk.dur || 0);
    timeline.push({ kind:'break', at, brk });
  };
  breaks.filter(b => !b.afterNo).forEach(emitBreak);   // pinned to the top, after general call
  dayScenes.forEach(s => {
    /* a manual per-scene call (e.g. a hard call after a company move) resets the
       running clock so the following times and the wrap stay correct */
    const manual = parseClock(s.sched?.call);
    if (manual != null) _clock = manual;
    const at = fmtClock(_clock);
    _clock += parseDur(s.sched?.est || s.dur) || 30;
    timeline.push({ kind:'scene', at, s });
    breaks.filter(b => String(b.afterNo) === String(s.no)).forEach(emitBreak);
  });
  const wrapAt = fmtClock(_clock);
  const dayShots = dayScenes.reduce((a, s) => a + (s.shots?.length || 0), 0);
  const estTotal = dayScenes.reduce((a, s) => a + (parseDur(s.sched?.est || s.dur) || 0), 0);
  const fmtMins = m => `${Math.floor(m / 60)}h ${m % 60}m`;

  return (
    <div>
      <SpotBar scenes={scenes} />
      {/* ===== day switcher ===== */}
      <div className="no-print mb-4 flex flex-wrap items-center gap-2">
        <span className="font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500">Shoot day</span>
        <div className="flex flex-wrap gap-1.5">
          {Array.from({ length: days }, (_, i) => i + 1).map(n => {
            const cnt = view.filter(s => sceneDay(s) === n).length;
            return (
              <button key={n} onClick={() => setDay(n)}
                className={cx('flex items-center gap-2 rounded-lg border px-3 py-2 text-left transition-colors',
                  day === n ? 'border-electric/50 bg-electric/10' : 'border-line bg-ink-900 hover:border-ink-600')}>
                <div>
                  <div className={cx('font-display text-[13px] font-700', day === n ? 'text-head' : 'text-zinc-300')}>Day {n}</div>
                  <div className="font-mono text-[9.5px] text-zinc-500">{fmtDayDate(n).split(',')[0]} · {cnt} sc</div>
                </div>
                {cnt === 0 && <span title="No scenes scheduled" className="h-1.5 w-1.5 rounded-full bg-amber" />}
              </button>
            );
          })}
        </div>
        {/* A5 — approve / publish this day */}
        <div className="ml-auto flex items-center gap-2">
          <Chip tone={verdict.state === 'published' ? 'mint' : verdict.state === 'approved' ? 'electric' : 'neutral'}>
            <Icon name={verdict.state === 'published' ? 'Send' : verdict.state === 'approved' ? 'CircleCheck' : 'FilePen'} size={11} />
            {verdict.state === 'published' ? ('Published v' + verdict.rev) : verdict.state === 'approved' ? 'Approved' : 'Draft'}
          </Chip>
          {stale && <span className="rounded-md bg-amber/15 px-2 py-1 text-[10.5px] font-700 text-amber">Edited since {verdict.state} v{verdict.rev}</span>}
          {(verdict.state === 'draft' || stale) && <button onClick={approveDay} className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[12px] font-700 text-zinc-200 transition-colors hover:border-mint/50 hover:text-mint"><Icon name="CircleCheck" size={13} /> Approve</button>}
          {verdict.state === 'approved' && !stale && <button onClick={publishDay} className="flex items-center gap-1.5 rounded-lg bg-cta px-3 py-2 text-[12px] font-700 text-cta-ink transition-colors hover:bg-cta-hover"><Icon name="Send" size={13} /> Publish</button>}
          {verdict.state === 'published' && !stale && <button onClick={publishDay} className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[12px] font-700 text-zinc-200 transition-colors hover:border-electric/50 hover:text-electric-soft"><Icon name="RefreshCw" size={13} /> Re-share</button>}
        </div>
        {unscheduled.length > 0 && <span className="ml-1 inline-flex items-center gap-1 rounded-md bg-amber/15 px-2 py-1 font-mono text-[10px] font-700 text-amber"><Icon name="CalendarOff" size={11} />{unscheduled.length} unscheduled</span>}
      </div>

      <Panel pad={false} className="overflow-hidden">
        {/* header */}
        <div className="relative grain bg-gradient-to-br from-ink-800 to-ink-900 p-5">
          <div className="relative z-10 flex flex-wrap items-start justify-between gap-3">
            <div className="min-w-0 flex-1">
              <div className="flex items-center gap-2 text-[11px] font-600 uppercase tracking-[0.16em] text-zinc-400">
                <Icon name="Clapperboard" size={13} className="text-electric-soft" />
                <span>Call Sheet · Day {day} of {days}</span>
                {activeSpot !== 'all' && <SpotBadge spotId={activeSpot} />}
              </div>
              <h3 className="mt-1.5 font-display text-[24px] font-700 text-head">{PROJECT.name}</h3>
              <div className="text-[12.5px] text-zinc-400">{fmtDayDate(day)}, {dayDate(day).getFullYear()} · {PROJECT.client}</div>
              {window.PLAN && window.PLAN.pdfWatermark && <div className="print-watermark">Made with Silverlines · Free plan</div>}
            </div>
            <div className="shrink-0 rounded-xl border border-electric/30 bg-electric/10 px-4 py-2 text-center">
              <div className="whitespace-nowrap text-[10px] uppercase tracking-[0.14em] text-electric-soft">General Call</div>
              <div className="font-display text-[30px] font-700 leading-none text-head">
                <Edit value={callTime} onChange={v => patchOv({ callTime: v })} className="font-display text-[30px] font-700 leading-none text-head" />
                <span className="text-[13px] font-500"> <Edit value={callMer} onChange={v => patchOv({ callMer: v })} className="text-[13px] font-500 text-head" /></span>
              </div>
            </div>
          </div>
          {/* per-day stats strip */}
          <div className="relative z-10 mt-4 flex flex-wrap gap-2">
            {[['Scenes', dayScenes.length, 'Clapperboard'], ['Shots', dayShots, 'ListChecks'], ['Cast', cast.length, 'Star'], ['Crew', crew.length, 'Users'], ['Est. shoot', fmtMins(estTotal), 'Hourglass'], ['Wrap', wrapAt, 'MoonStar']].map(([l, v, ic]) => (
              <div key={l} className="flex items-center gap-2 rounded-lg border border-line bg-ink-900/70 px-2.5 py-1.5">
                <Icon name={ic} size={13} className="text-zinc-500" />
                <span className="font-display text-[14px] font-700 text-head">{v}</span>
                <span className="font-mono text-[9.5px] uppercase tracking-wide text-zinc-500">{l}</span>
              </div>
            ))}
          </div>
        </div>

        {dayScenes.length === 0 ? (
          <div className="px-5 py-12 text-center">
            <Icon name="CalendarOff" size={26} className="mx-auto text-zinc-600" />
            <div className="mt-2 text-[13px] font-600 text-zinc-300">No scenes scheduled for Day {day}</div>
            <div className="mt-1 text-[12px] text-zinc-500">Assign scenes to this day from the Storyboard, the Shot List, or the bucket below.</div>
          </div>
        ) : (
        <>
          {/* conditions */}
          <SheetSection icon="CloudSun" title="Conditions">
            <div className={cx('grid gap-3', mobile ? 'grid-cols-2' : 'grid-cols-4')}>
              <StatBlock icon="Sunrise" label="Sunrise" tone="amber" value={stats.sunrise.v} sub={stats.sunrise.s} onValue={v => setStat('sunrise','v',v)} onSub={v => setStat('sunrise','s',v)} />
              <StatBlock icon="CloudSun" label="Weather" tone="electric" value={stats.weather.v} sub={stats.weather.s} onValue={v => setStat('weather','v',v)} onSub={v => setStat('weather','s',v)} />
              <StatBlock icon="Sunset" label="Sunset" tone="amber" value={stats.sunset.v} sub={stats.sunset.s} onValue={v => setStat('sunset','v',v)} onSub={v => setStat('sunset','s',v)} />
              <StatBlock icon="MapPinned" label="To Hospital" tone="danger" value={stats.hospital.v} sub={stats.hospital.s} onValue={v => setStat('hospital','v',v)} onSub={v => setStat('hospital','s',v)} />
            </div>
          </SheetSection>

          {/* locations (this day) */}
          <SheetSection icon="MapPin" title="Locations" right={<span className="text-[11px] text-zinc-500">{locs.length} this day</span>}>
            {locs.length === 0 ? <div className="text-[12px] text-zinc-500">No locations on the day's scenes yet.</div>
              : <div className={cx('grid gap-3', mobile ? 'grid-cols-1' : 'grid-cols-3')}>
                  {locs.map((l) => <LocationCard key={l.name} loc={l} onEdit={(k, v) => editLoc(l.name, k, v)} />)}
                </div>}
          </SheetSection>

          {/* hospital */}
          <SheetSection icon="Cross" title="Nearest Hospital">
            <div className="flex flex-wrap items-center gap-3 rounded-xl border border-[#FF4D6D]/25 bg-[#FF4D6D]/8 p-3.5">
              <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[#FF4D6D]/15 text-[#FF4D6D]"><Icon name="Cross" size={20} /></span>
              <div className="min-w-0 flex-1">
                <div className="text-[13.5px] font-700 text-head">{HOSPITAL.name}</div>
                <div className="flex items-center gap-1.5 text-[12px] text-zinc-400"><Icon name="MapPin" size={12} />{HOSPITAL.address}</div>
              </div>
              <div className="flex items-center gap-2">
                <span className="rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[12px] font-600 text-zinc-300">{HOSPITAL.distance}</span>
                <a href={`tel:${HOSPITAL.phone.replace(/\s/g,'')}`} className="flex items-center gap-1.5 rounded-lg bg-[#FF4D6D] px-3 py-1.5 text-[12px] font-700 text-white"><Icon name="Phone" size={13} />{HOSPITAL.phone}</a>
                <a href={HOSPITAL.map} target="_blank" rel="noopener noreferrer" className="no-print flex items-center gap-1 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[12px] font-600 text-electric-soft hover:border-electric/40"><Icon name="Map" size={13} /> Map</a>
              </div>
            </div>
          </SheetSection>

          <LogisticsSheet mobile={mobile} />

          {/* crew + cast call */}
          <div className={cx('grid', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
            <SheetSection icon="Users" title="Crew Call" right={<div className="flex items-center gap-2"><ProFlowPill label="Auto by day" /><span className="text-[11px] text-zinc-500">{crew.length}</span></div>}>
              <div className="overflow-hidden rounded-xl border border-line">
                <table className="w-full text-left">
                  <thead><tr className="bg-ink-900 text-[10px] uppercase tracking-[0.1em] text-zinc-500"><th className="px-3 py-2 font-600">Dept</th><th className="px-3 py-2 font-600">Name · why</th><th className="px-3 py-2 text-right font-600">Call</th><th className="no-print px-2 py-2"></th></tr></thead>
                  <tbody>
                    {crew.map((r) => (
                      <tr key={r.name} className="border-t border-line/70">
                        <td className="px-3 py-2"><Chip tone="neutral">{r.dept}</Chip></td>
                        <td className="px-3 py-2"><div className="text-[12.5px] font-600 text-zinc-200">{r.name}</div><div className="text-[10.5px] text-zinc-500">{r.role}{r.why ? ' · ' + r.why : ''}</div></td>
                        <td className="px-3 py-2 text-right font-mono text-[12.5px] font-700 text-head"><Edit value={r.call} onChange={v => setCrewCall(r.name, v)} className="font-mono text-[12.5px] font-700 text-head" /></td>
                        <td className="no-print px-2 py-2 text-right"><button onClick={() => excuse(r.name)} title="Excuse from this day" className="text-zinc-600 hover:text-[#FF6B6B]"><Icon name="UserMinus" size={13} /></button></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              {bench.length > 0 && (
                <div className="no-print mt-2 flex flex-wrap items-center gap-1.5">
                  <span className="font-mono text-[10px] uppercase tracking-wide text-zinc-600">Not called:</span>
                  {bench.map(c => (
                    <button key={c.name} onClick={() => recall(c.name)} title="Call to this day"
                      className="flex items-center gap-1 rounded-full border border-dashed border-ink-600 px-2 py-0.5 text-[10.5px] font-600 text-zinc-500 hover:border-electric/40 hover:text-electric-soft">
                      <Icon name="Plus" size={10} />{c.name} <span className="text-zinc-600">· {c.dept}</span>
                    </button>
                  ))}
                </div>
              )}
            </SheetSection>

            <SheetSection icon="Star" title="Cast Call" right={<div className="flex items-center gap-2"><ProFlowPill label="From day's scenes" /><span className="text-[11px] text-zinc-500">{cast.length}</span></div>}>
              {cast.length === 0 ? <div className="text-[12px] text-zinc-500">No cast in this day's scenes.</div>
              : <div className="overflow-hidden rounded-xl border border-line">
                <table className="w-full text-left">
                  <thead><tr className="bg-ink-900 text-[10px] uppercase tracking-[0.1em] text-zinc-500"><th className="px-3 py-2 font-600">#</th><th className="px-3 py-2 font-600">Cast / Character</th><th className="px-3 py-2 text-right font-600">P/U</th><th className="px-3 py-2 text-right font-600">MU</th><th className="px-3 py-2 text-right font-600">Set</th></tr></thead>
                  <tbody>
                    {cast.map((r, i) => (
                      <tr key={r.actor} className="border-t border-line/70">
                        <td className="px-3 py-2"><span className="flex h-5 w-5 items-center justify-center rounded bg-electric/15 font-mono text-[11px] font-700 text-electric-soft">{r.no || i + 1}</span></td>
                        <td className="px-3 py-2"><div className="text-[12.5px] font-600 text-zinc-200">{r.actor}</div><div className="text-[11px] text-zinc-500">{r.character}{r.status ? ' · ' + r.status : ''}</div></td>
                        <td className="px-3 py-2 text-right font-mono text-[12px] text-zinc-400"><Edit value={r.pickup} onChange={v => setCastCall(r.actor,'pickup',v)} placeholder="—" className="font-mono text-[12px] text-zinc-400" /></td>
                        <td className="px-3 py-2 text-right font-mono text-[12px] text-zinc-300"><Edit value={r.mu} onChange={v => setCastCall(r.actor,'mu',v)} placeholder="—" className="font-mono text-[12px] text-zinc-300" /></td>
                        <td className="px-3 py-2 text-right font-mono text-[12.5px] font-700 text-head"><Edit value={r.set} onChange={v => setCastCall(r.actor,'set',v)} placeholder="—" className="font-mono text-[12.5px] font-700 text-head" /></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>}
            </SheetSection>
          </div>

          {/* schedule — lined from this day's scenes */}
          <SheetSection icon="CalendarClock" title="Shooting Schedule" right={<div className="flex items-center gap-2"><button onClick={addBreak} className="no-print flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[11.5px] font-700 text-zinc-300 transition-colors hover:border-amber/50 hover:text-amber"><Icon name="Coffee" size={13} /> Add break</button><ProFlowPill label="Lined from storyboard" /><span className="font-mono text-[11px] text-zinc-500">{dayScenes.length} sc · {dayShots} shots</span></div>}>
            <div className="relative">
              <div className="absolute bottom-3 left-[74px] top-3 w-px bg-line" />
              <div className="space-y-1.5">
                <div className="group relative flex items-stretch gap-4">
                  <div className="w-[54px] shrink-0 pt-2.5 text-right font-mono text-[12px] font-600 text-zinc-300">{fmtClock(startMin)}</div>
                  <div className="relative flex w-4 shrink-0 justify-center"><span className="mt-3 h-2.5 w-2.5 rounded-full bg-electric ring-4 ring-ink-850" /></div>
                  <div className="mb-1.5 flex-1 rounded-xl border border-line bg-ink-900 p-3">
                    <div className="flex items-center gap-2"><Chip tone="electric">Call</Chip><span className="text-[13px] font-600 text-zinc-100">General crew call · Breakfast</span></div>
                    <div className="mt-1.5 flex items-center gap-1.5 text-[11.5px] text-zinc-500"><Icon name="Users" size={11} />{crew.length} crew · {cast.length} cast</div>
                  </div>
                </div>

                {timeline.map(item => {
                  if (item.kind === 'break') {
                    const brk = item.brk;
                    return (
                      <div key={brk.id} className="group relative flex items-stretch gap-4">
                        <div className="w-[54px] shrink-0 pt-2.5 text-right font-mono text-[12px] font-600 text-zinc-300">{item.at}</div>
                        <div className="relative flex w-4 shrink-0 justify-center"><span className="mt-3 h-2.5 w-2.5 rounded-full bg-amber ring-4 ring-ink-850" /></div>
                        <div className="mb-1.5 flex flex-1 flex-wrap items-center gap-2 rounded-xl border border-amber/30 bg-amber/[0.06] p-3">
                          <button onClick={() => { const order = ['Lunch', 'Coffee', 'Move', 'Setup']; const nx = order[(order.indexOf(brk.type) + 1) % order.length]; patchBreak(brk.id, { type: nx, label: nx === 'Move' ? 'Company move' : nx }); }} title="Change break type"><Chip tone={TAG_TONES[brk.type] || 'amber'}>{brk.type}</Chip></button>
                          <Edit value={brk.label} onChange={v => patchBreak(brk.id, { label: v })} className="text-[13px] font-600 text-zinc-100" />
                          <div className="ml-auto flex items-center gap-2 font-mono text-[11.5px] text-zinc-500">
                            <span className="flex items-center gap-0.5"><Edit value={String(brk.dur)} onChange={v => patchBreak(brk.id, { dur: Math.max(0, parseInt(v) || 0) })} align="right" className="w-6 font-mono text-[11.5px]" />m</span>
                            <select value={brk.afterNo == null ? '' : String(brk.afterNo)} onChange={e => patchBreak(brk.id, { afterNo: e.target.value || null })}
                              className="no-print rounded-md border border-line bg-ink-900 px-1.5 py-1 text-[11px] text-zinc-300 outline-none focus:border-electric/60">
                              <option value="">Top</option>
                              {dayScenes.map(s => <option key={s.no} value={s.no}>After SC {s.no}</option>)}
                            </select>
                            <button onClick={() => rmBreak(brk.id)} title="Remove break" className="no-print text-zinc-600 hover:text-[#FF4D6D]"><Icon name="X" size={12} /></button>
                          </div>
                        </div>
                      </div>
                    );
                  }
                  const { at, s } = item;
                  const shots = s.shots || [];
                  const status = s.sched?.status || 'Planned';
                  return (
                    <div key={s.id} className="group relative flex items-stretch gap-4">
                      <div className="w-[54px] shrink-0 pt-2.5 text-right font-mono text-[12px] font-600 text-zinc-300">{s.sched?.call || at}</div>
                      <div className="relative flex w-4 shrink-0 justify-center"><span className={cx('mt-3 h-2.5 w-2.5 rounded-full ring-4 ring-ink-850', status === 'Shot' ? 'bg-mint' : status === 'Scheduled' ? 'bg-electric' : status === 'Hold' ? 'bg-zinc-500' : 'bg-amber')} /></div>
                      <div className="mb-1.5 flex-1 rounded-xl border border-line bg-ink-900 p-3">
                        <div className="flex items-start justify-between gap-2">
                          <div className="flex min-w-0 items-center gap-2"><Chip tone="electric">SC {s.no}</Chip><span className="truncate text-[13px] font-600 text-zinc-100">{s.title}</span></div>
                          <div className="flex shrink-0 items-center gap-2"><Chip tone={SCHED_TONE[status] || 'neutral'}>{status}</Chip><span className="hidden font-mono text-[11px] text-zinc-500 sm:block">{s.sched?.est || s.dur}</span></div>
                        </div>
                        <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-zinc-500">
                          <span className="inline-flex items-center gap-1.5"><Icon name="MapPin" size={11} />{s.loc}</span>
                          <span className="inline-flex items-center gap-1.5"><Icon name="Sun" size={11} />{s.tod}</span>
                          <span className="inline-flex items-center gap-1.5"><Icon name="FileText" size={11} />{s.pages} pp</span>
                          {(s.cast || []).length > 0 && <span className="inline-flex items-center gap-1.5"><Icon name="Users" size={11} />{s.cast.length} cast</span>}
                        </div>
                        <div className="mt-2.5 border-t border-line/70 pt-2.5">
                          {shots.length === 0 ? <div className="text-[11px] italic text-zinc-600">No shots lined yet — add setups in the Storyboard</div>
                          : <div className="flex flex-wrap items-center gap-1.5">
                              <span className="mr-0.5 font-mono text-[10px] uppercase tracking-[0.12em] text-zinc-600">{shots.length} shot{shots.length === 1 ? '' : 's'}</span>
                              {shots.map(sh => (
                                <span key={sh.id} className="inline-flex items-center gap-1 rounded-md border border-line bg-ink-850 px-1.5 py-0.5 font-mono text-[10.5px] text-zinc-300">
                                  <span className="font-700 text-electric-soft">{sh.shot}</span><span className="text-zinc-400">{sh.type}</span>{sh.lens && <span className="text-zinc-600">· {sh.lens}</span>}
                                </span>
                              ))}
                            </div>}
                        </div>
                      </div>
                    </div>
                  );
                })}

                <div className="group relative flex items-stretch gap-4">
                  <div className="w-[54px] shrink-0 pt-2.5 text-right font-mono text-[12px] font-600 text-zinc-300">{wrapAt}</div>
                  <div className="relative flex w-4 shrink-0 justify-center"><span className="mt-3 h-2.5 w-2.5 rounded-full bg-electric ring-4 ring-ink-850" /></div>
                  <div className="mb-1.5 flex-1 rounded-xl border border-line bg-ink-900 p-3">
                    <div className="flex items-center gap-2"><Chip tone="electric">Wrap</Chip><span className="text-[13px] font-600 text-zinc-100">Estimated camera wrap</span></div>
                    <div className="mt-1.5 flex items-center gap-1.5 text-[11.5px] text-zinc-500"><Icon name="Clock" size={11} />Computed from {dayScenes.length} scenes · {dayShots} shots</div>
                  </div>
                </div>
              </div>
            </div>
          </SheetSection>
        </>
        )}
      </Panel>

      {/* unscheduled bucket — assign straight onto a day */}
      {unscheduled.length > 0 && (
        <div className="no-print mt-4 rounded-2xl border border-amber/30 bg-amber/[0.05] p-4">
          <div className="mb-2.5 flex items-center gap-2">
            <Icon name="CalendarOff" size={15} className="text-amber" />
            <h4 className="font-display text-[13.5px] font-700 text-head">Unscheduled scenes</h4>
            <span className="font-mono text-[11px] text-zinc-500">{unscheduled.length} — drop them on a day</span>
          </div>
          <div className="space-y-2">
            {unscheduled.map(s => (
              <div key={s.id} className="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-xl border border-line bg-ink-900 p-2.5">
                <span className="flex h-6 items-center rounded-md bg-ink-700 px-2 font-mono text-[12px] font-700 text-electric-soft">{s.no}</span>
                <span className="text-[13px] font-600 text-zinc-100">{s.title}</span>
                <span className="flex items-center gap-1 text-[11px] text-zinc-500"><Icon name="MapPin" size={11} />{s.loc}</span>
                <div className="ml-auto"><DayPicker value={0} days={days} onChange={n => assignDay(s.id, n)} /></div>
              </div>
            ))}
          </div>
        </div>
      )}
      {pubShare && <WeblinkModal onClose={() => setPubShare(false)} />}
    </div>
  );
}

/* ---- shareable web link modal ---- */
/* ---- real shareable web link (encodes a read-only payload into share.html) ---- */
function buildShareURL(data) {
  try {
    const json = JSON.stringify(data);
    const enc = encodeURIComponent(btoa(unescape(encodeURIComponent(json))));
    return new URL('share.html', location.href).href + '#' + enc;
  } catch (e) { return ''; }
}
function expiryMs() {
  let e = '7d';
  try { e = (window.SLStore.get('silverlines.settings.v1', {})).expiry || '7d'; } catch (x) {}
  if (e === 'never') return null;
  if (e === '24h') return Date.now() + 864e5;
  return Date.now() + 7 * 864e5;
}
function buildCallsheetPayload() {
  const scenes = (typeof sbLoad === 'function' ? sbLoad() : []).map(s => ({ no: s.no, title: s.title, loc: s.loc, dur: s.dur }));
  return {
    kicker: 'Call Sheet',
    title: (window.PROJ_NAME || 'Crimson Skies'),
    subtitle: 'Shooting day · ' + (typeof fmtDayDate === 'function' ? fmtDayDate(1) : ''),
    stats: [{ l: 'General call', v: '06:00' }, { l: 'Scenes', v: String(scenes.length) }, { l: 'Status', v: 'On schedule' }],
    locations: (typeof LOCATIONS !== 'undefined' ? LOCATIONS : []).map(l => ({ name: l.name, address: l.address })),
    schedule: scenes,
    crew: (typeof CREW_CALL !== 'undefined' ? CREW_CALL : []).map(c => ({ name: c.name, dept: c.dept, role: c.role, call: c.call })),
    cast: (typeof CAST_CALL !== 'undefined' ? CAST_CALL : []).map(c => ({ name: c.name, role: c.role || c.char, call: c.call })),
    hospital: (typeof HOSPITAL !== 'undefined' ? HOSPITAL : null),
  };
}

function WeblinkModal({ onClose }) {
  const [copied, setCopied] = useState(false);
  const [pass, setPass] = useState(false);
  const [code] = useState(() => String(Math.floor(1000 + Math.random() * 9000)));
  const [noExpiry, setNoExpiry] = useState(false);
  const exp = noExpiry ? null : expiryMs();
  const url = buildShareURL({ v: 1, type: 'callsheet', exp, pass: pass ? code : null, payload: buildCallsheetPayload() });
  const copy = () => {
    try { navigator.clipboard && navigator.clipboard.writeText(url); } catch (e) {}
    setCopied(true); setTimeout(() => setCopied(false), 1600);
    window.toast && window.toast('Share link copied', 'Link2');
  };
  return (
    <div className="no-print fixed inset-0 z-50 flex items-center justify-center p-4" onClick={onClose}>
      <div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
      <div onClick={e => e.stopPropagation()} className="fade-up relative w-full max-w-md overflow-hidden rounded-2xl border border-line bg-ink-850 p-5">
        <button onClick={onClose} className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-lg bg-ink-800 text-zinc-400 hover:text-zinc-200"><Icon name="X" size={16} /></button>
        <div className="flex items-center gap-2"><Icon name="Globe" size={18} className="text-electric-soft" /><h3 className="font-display text-[20px] font-600 text-head">Shareable web link</h3></div>
        <p className="mt-1 text-[12.5px] text-zinc-400">A real read-only link — opens this call sheet in any browser, no app, no login. {exp ? 'Expires ' + new Date(exp).toLocaleDateString() + '.' : 'Never expires.'}</p>
        <div className="mt-4 flex items-center gap-2 rounded-xl border border-line bg-ink-900 p-2 pl-3">
          <Icon name="Link2" size={14} className="shrink-0 text-zinc-500" />
          <span className="flex-1 truncate font-mono text-[12px] text-zinc-300">{url.replace(/^https?:\/\//, '').slice(0, 42)}…</span>
          <button onClick={copy}
            className={cx('flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-700 transition-colors', copied ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
            <Icon name={copied ? 'Check' : 'Copy'} size={13} />{copied ? 'Copied' : 'Copy'}
          </button>
        </div>
        <div className="mt-3 flex items-center gap-3 rounded-xl border border-line bg-ink-900 px-3 py-2.5">
          <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-400"><Icon name="KeyRound" size={15} /></span>
          <div className="flex-1"><div className="text-[12.5px] font-600 text-zinc-200">Require passcode</div><div className="text-[11px] text-zinc-500">{pass ? <>Viewers enter <span className="font-mono font-700 text-electric-soft">{code}</span> — share it separately</> : 'Anyone with the link can view'}</div></div>
          <Switch on={pass} onChange={setPass} />
        </div>
        <div className="mt-3 flex items-center gap-3 rounded-xl border border-line bg-ink-900 px-3 py-2.5">
          <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-400"><Icon name="Clock" size={15} /></span>
          <div className="flex-1"><div className="text-[12.5px] font-600 text-zinc-200">Never expires</div><div className="text-[11px] text-zinc-500">{noExpiry ? 'Link stays live indefinitely' : 'Uses your default link expiry'}</div></div>
          <Switch on={noExpiry} onChange={setNoExpiry} />
        </div>
        <div className="mt-4 flex gap-2">
          <a href={url} target="_blank" rel="noopener" className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-line py-2.5 text-[12.5px] font-700 text-zinc-200 hover:border-ink-600"><Icon name="ExternalLink" size={14} /> Open online view</a>
          <button onClick={copy} className="flex flex-1 items-center justify-center gap-2 rounded-xl bg-cta py-2.5 text-[12.5px] font-700 text-cta-ink hover:bg-cta-hover"><Icon name="Copy" size={14} /> Copy link</button>
        </div>
      </div>
    </div>
  );
}

function ProductionModule({ mobile }) {
  const { subtabs, setSubtab } = useContext(AccessContext);
  const tab = (subtabs && subtabs.production) || 'callsheet';
  const setTab = id => setSubtab('production', id);
  const [weblink, setWeblink] = useState(false);
  const tabs = [
    { id:'callsheet', label:'Call Sheet', icon:'ClipboardList' },
    { id:'breakdown', label:'AD Breakdown', icon:'ListTree' },
  ];
  return (
    <div className="fade-up">
      <SectionTitle kicker="Phase 03 · Production" icon="Video" title={tab === 'breakdown' ? 'AD Breakdown' : 'Call Sheet'}
        action={tab === 'callsheet' ? <div className="no-print flex flex-wrap items-center gap-2">
          <Chip tone="mint"><span className="h-1.5 w-1.5 rounded-full bg-mint animate-pulse" /> On schedule</Chip>
          <button onClick={() => window.print()} className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[12.5px] font-700 text-zinc-200 hover:border-ink-600"><Icon name="FileDown" size={14} /> Export PDF</button>
          <button onClick={() => setWeblink(true)} className="flex items-center gap-1.5 rounded-lg bg-cta px-3 py-2 text-[12.5px] font-700 text-cta-ink hover:bg-cta-hover"><Icon name="Link2" size={14} /> Share link</button>
        </div> : null} />

      {/* tab bar */}
      <div className="no-print mb-4 inline-flex rounded-xl border border-line bg-ink-850 p-1">
        {tabs.map(t => (
          <button key={t.id} onClick={() => setTab(t.id)}
            className={cx('flex items-center gap-2 rounded-lg px-3.5 py-2 font-display text-[12.5px] transition-colors',
              tab === t.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
            <Icon name={t.icon} size={14} />{t.label}
          </button>
        ))}
      </div>

      {tab === 'callsheet' && (
        <>
          <div className="no-print mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] font-600 text-zinc-500">
            <span className="flex items-center gap-1.5"><Icon name="Pencil" size={12} className="text-electric-soft" /> Every field is editable — click to change</span>
            <span className="flex items-center gap-1.5"><Icon name="Smartphone" size={12} className="text-electric-soft" /> Live on every linked crew phone</span>
          </div>
          <div id="print-callsheet" className="mx-auto max-w-[1000px]">
            <CallDoc mobile={mobile} />
            <div className="mt-4 hidden items-center justify-between text-[10px] text-zinc-500 print:flex">
              <span>{PROJECT.code} · {PROJECT.client}</span><span>Generated by SILVERLINES · silverlines.app</span>
            </div>
          </div>
        </>
      )}
      {tab === 'breakdown' && <ADBreakdown mobile={mobile} />}

      {weblink && <WeblinkModal onClose={() => setWeblink(false)} />}
    </div>
  );
}

window.ProductionModule = ProductionModule;


/* ===== mod_post ===== */
/* ============================================================
   MODULE 5 — Post-Production & Approval
   Review Hub: timecoded frame comments, no login required
   ============================================================ */
const DURATION = 42; // seconds
const fmt = s => `${String(Math.floor(s / 60)).padStart(2,'0')}:${String(Math.floor(s % 60)).padStart(2,'0')}`;

/* ---- review verdict: approve / request changes (persisted) ---- */
function ReviewVerdict() {
  const [v, setV] = useState(() => { try { return localStorage.getItem('silverlines.verdict.v04') || ''; } catch (e) { return ''; } });
  const set = (val, msg, icon) => {
    setV(val);
    try { localStorage.setItem('silverlines.verdict.v04', val); } catch (e) {}
    window.toast(msg, icon);
    window.notify && window.notify(msg, icon, 'post');
  };
  return (
    <div className="mt-3 flex gap-2">
      <button onClick={() => set('changes', 'Change request sent to the edit team', 'RotateCcw')}
        className={cx('flex flex-1 items-center justify-center gap-1.5 rounded-xl border py-2.5 text-[12.5px] font-600 transition-colors',
          v === 'changes' ? 'border-amber/40 bg-amber/10 text-amber' : 'border-line text-zinc-300 hover:border-ink-600')}>
        <Icon name="RotateCcw" size={13} /> {v === 'changes' ? 'Changes requested' : 'Request changes'}
      </button>
      <button onClick={() => set('approved', 'Hero Cut v04 approved — client notified', 'Check')}
        className={cx('flex flex-1 items-center justify-center gap-1.5 rounded-xl py-2.5 text-[12.5px] font-700 transition-colors',
          v === 'approved' ? 'border border-mint/40 bg-mint/15 text-mint' : 'bg-mint text-ink-950 hover:bg-mint/90')}>
        <Icon name="Check" size={13} /> {v === 'approved' ? 'Approved v04' : 'Approve v04'}
      </button>
    </div>
  );
}

function PostModule({ mobile }) {
  const [time, setTime] = useState(14);
  const [playing, setPlaying] = useState(false);
  const [comments, setComments] = useState(REVIEW_COMMENTS);
  const [draft, setDraft] = useState(null); // {x,y,t}
  const [text, setText] = useState('');
  const frameRef = useRef(null);
  const listRef = useRef(null);

  useEffect(() => {
    if (!playing) return;
    const id = setInterval(() => {
      setTime(t => {
        if (t >= DURATION) { setPlaying(false); return DURATION; }
        return +(t + 0.25).toFixed(2);
      });
    }, 250);
    return () => clearInterval(id);
  }, [playing]);

  const active = comments.filter(c => Math.abs(c.t - time) < 2);

  const onFrameClick = e => {
    if (mobile) return;
    const r = frameRef.current.getBoundingClientRect();
    const x = ((e.clientX - r.left) / r.width) * 100;
    const y = ((e.clientY - r.top) / r.height) * 100;
    setPlaying(false);
    setDraft({ x:Math.max(4, Math.min(96, x)), y:Math.max(8, Math.min(88, y)), t:time });
    setText('');
  };

  const submit = () => {
    if (!text.trim()) { setDraft(null); return; }
    setComments(c => [...c, { ...draft, by:'You (Guest)', color:'#22D48A', role:'Guest', text:text.trim() }].sort((a, b) => a.t - b.t));
    setDraft(null); setText('');
  };

  const seek = t => { setTime(t); setPlaying(false); };

  return (
    <div className="fade-up">
      <SectionTitle kicker="Phase 04 · Post & Review" icon="MessageSquareReply" title="Review Hub"
        action={<Chip tone="mint"><Icon name="UserCheck" size={12} /> No login required</Chip>} />

      <div className={cx('grid gap-5', mobile ? 'grid-cols-1' : 'grid-cols-12')}>
        {/* ---- player ---- */}
        <div className={cx(mobile ? '' : 'col-span-8')}>
          <Panel pad={false} className="overflow-hidden">
            {/* asset bar */}
            <div className="flex items-center justify-between border-b border-line px-4 py-3">
              <div className="flex items-center gap-2.5">
                <span className="flex h-7 w-7 items-center justify-center rounded-lg bg-electric/12 text-electric-soft"><Icon name="Film" size={15} /></span>
                <div>
                  <div className="text-[13px] font-600 text-zinc-100">Crimson_Skies_v04_Director.mov</div>
                  <div className="font-mono text-[10.5px] text-zinc-500">ProRes · 1920×816 · 24fps · Rev 04</div>
                </div>
              </div>
              <Chip tone="amber"><Icon name="Eye" size={11} /> Pending approval</Chip>
            </div>

            {/* frame */}
            <div ref={frameRef} onClick={onFrameClick}
              className="scan relative aspect-[1920/816] cursor-crosshair select-none" style={{ background:'#161226' }}>
              <div className="absolute inset-0 bg-gradient-to-tr from-[#2A1B4D]/70 via-transparent to-[#0B0E1A]/70" />
              {/* center hint */}
              <div className="absolute inset-0 flex flex-col items-center justify-center gap-2 text-center">
                <button onClick={e => { e.stopPropagation(); setPlaying(p => !p); }}
                  className="flex h-14 w-14 items-center justify-center rounded-full bg-white/90 text-ink-950 shadow-xl backdrop-blur transition-transform hover:scale-105">
                  <Icon name={playing ? 'Pause' : 'Play'} size={24} />
                </button>
                {!mobile && <span className="rounded-md bg-black/45 px-2 py-1 font-mono text-[10px] text-zinc-300 backdrop-blur-sm">click any frame to leave a timecoded note</span>}
              </div>

              {/* timecode overlay */}
              <div className="absolute left-3 top-3 rounded-md bg-black/55 px-2 py-1 font-mono text-[11px] font-600 text-white backdrop-blur-sm">
                {fmt(time)}<span className="text-zinc-500"> / {fmt(DURATION)}</span>
              </div>

              {/* active comment pins */}
              {active.map((c, i) => (
                <div key={i} className="fade-up absolute z-10 -translate-x-1/2 -translate-y-full" style={{ left:`${c.x}%`, top:`${c.y}%` }}>
                  <div className="mb-1 max-w-[180px] rounded-lg border border-line bg-ink-850/95 p-2 shadow-xl backdrop-blur">
                    <div className="flex items-center gap-1.5">
                      <Avatar name={c.by} color={c.color} size={16} />
                      <span className="text-[10.5px] font-600 text-zinc-200">{c.by}</span>
                    </div>
                    <div className="mt-1 text-[11px] leading-snug text-zinc-300">{c.text}</div>
                  </div>
                  <span className="ml-1 block h-3 w-3 rounded-full border-2 border-white" style={{ background:c.color }} />
                </div>
              ))}

              {/* draft composer */}
              {draft && (
                <div className="fade-up absolute z-20 w-[230px] -translate-x-1/2" style={{ left:`${draft.x}%`, top:`${draft.y}%` }}
                  onClick={e => e.stopPropagation()}>
                  <div className="rounded-xl border border-mint/40 bg-ink-850 p-2.5 shadow-2xl">
                    <div className="mb-1.5 flex items-center gap-1.5 font-mono text-[10px] text-mint"><Icon name="Crosshair" size={11} /> @ {fmt(draft.t)}</div>
                    <textarea autoFocus value={text} onChange={e => setText(e.target.value)}
                      onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } }}
                      placeholder="Leave a frame note…" rows={2}
                      className="w-full resize-none rounded-lg border border-line bg-ink-900 p-2 text-[12px] text-zinc-200 outline-none focus:border-mint/50" />
                    <div className="mt-1.5 flex justify-end gap-1.5">
                      <button onClick={() => setDraft(null)} className="rounded-md px-2.5 py-1 text-[11px] font-600 text-zinc-400 hover:text-zinc-200">Cancel</button>
                      <button onClick={submit} className="rounded-md bg-mint px-3 py-1 text-[11px] font-700 text-ink-950 hover:bg-mint/90">Post</button>
                    </div>
                  </div>
                  <span className="ml-1 block h-3 w-3 rounded-full border-2 border-white bg-mint" />
                </div>
              )}
            </div>

            {/* scrubber + controls */}
            <div className="border-t border-line p-3.5">
              <div className="group relative h-6">
                {/* track */}
                <div className="absolute top-1/2 h-1.5 w-full -translate-y-1/2 overflow-hidden rounded-full bg-ink-700">
                  <div className="h-full rounded-full bg-electric" style={{ width:`${(time / DURATION) * 100}%` }} />
                </div>
                {/* comment markers */}
                {comments.map((c, i) => (
                  <button key={i} onClick={() => seek(c.t)} title={`${c.by} @ ${fmt(c.t)}`}
                    className="absolute top-1/2 z-10 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-ink-850 transition-transform hover:scale-150"
                    style={{ left:`${(c.t / DURATION) * 100}%`, background:c.color }} />
                ))}
                {/* playhead */}
                <div className="pointer-events-none absolute top-1/2 z-20 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white shadow ring-2 ring-electric"
                  style={{ left:`${(time / DURATION) * 100}%` }} />
                {/* invisible range */}
                <input type="range" min="0" max={DURATION} step="0.25" value={time}
                  onChange={e => seek(+e.target.value)}
                  className="absolute inset-0 h-full w-full cursor-pointer opacity-0" />
              </div>
              <div className="mt-2 flex items-center gap-3">
                <button onClick={() => setPlaying(p => !p)} className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-200 hover:bg-ink-600">
                  <Icon name={playing ? 'Pause' : 'Play'} size={15} />
                </button>
                <button onClick={() => seek(Math.max(0, time - 5))} className="text-zinc-400 hover:text-zinc-200"><Icon name="Rewind" size={15} /></button>
                <button onClick={() => seek(Math.min(DURATION, time + 5))} className="text-zinc-400 hover:text-zinc-200"><Icon name="FastForward" size={15} /></button>
                <span className="font-mono text-[12px] text-zinc-400">{fmt(time)} / {fmt(DURATION)}</span>
                <div className="ml-auto flex items-center gap-1.5 font-mono text-[11px] text-zinc-500">
                  <Icon name="MessageSquare" size={12} /> {comments.length} notes
                </div>
              </div>
            </div>
          </Panel>
        </div>

        {/* ---- comment thread ---- */}
        <div className={cx(mobile ? '' : 'col-span-4')}>
          <Panel pad={false} className="flex flex-col overflow-hidden" >
            <div className="flex items-center justify-between border-b border-line p-4">
              <h3 className="font-display text-[15px] font-600 text-head">Frame Notes</h3>
              <div className="flex -space-x-2">
                {PEOPLE.slice(0, 3).map(p => <Avatar key={p.name} name={p.name} color={p.color} size={24} ring />)}
              </div>
            </div>
            <div ref={listRef} className="max-h-[460px] space-y-1 overflow-y-auto p-2">
              {comments.map((c, i) => {
                const isActive = Math.abs(c.t - time) < 1.5;
                return (
                  <button key={i} onClick={() => seek(c.t)}
                    className={cx('flex w-full gap-3 rounded-xl border p-3 text-left transition-colors',
                      isActive ? 'border-electric/40 bg-electric/8' : 'border-transparent hover:bg-ink-800')}>
                    <Avatar name={c.by} color={c.color} size={30} />
                    <div className="min-w-0 flex-1">
                      <div className="flex items-center gap-2">
                        <span className="text-[12.5px] font-600 text-zinc-100">{c.by}</span>
                        <Chip tone="neutral" className="!px-1.5 !py-0.5 !text-[10px]">{c.role}</Chip>
                      </div>
                      <div className="mt-0.5 text-[12.5px] leading-snug text-zinc-300">{c.text}</div>
                      <div className="mt-1.5 flex items-center gap-1 font-mono text-[10.5px] font-600 text-electric-soft">
                        <Icon name="Clock" size={10} /> {fmt(c.t)}
                      </div>
                    </div>
                  </button>
                );
              })}
            </div>
            <div className="border-t border-line p-3">
              <div className="flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-3 py-2 text-[12.5px] text-zinc-500">
                <Icon name="MousePointerClick" size={14} className="text-mint" />
                {mobile ? 'Tap markers to review notes' : 'Click the frame to add a note'}
              </div>
              <ReviewVerdict />
            </div>
          </Panel>
        </div>
      </div>
    </div>
  );
}

window.PostModule = PostModule;


/* ===== mod_access ===== */
/* ============================================================
   DEPARTMENT ACCESS — producer grants/revokes scoped phone access
   Linked live to production · revocable at any time
   ============================================================ */
function AccessRow({ a, granted, onToggle, mobile }) {
  const toolIcon = a.tool === 'floorplan' ? 'Lightbulb' : a.tool === 'purchase' ? 'ReceiptText' : 'ClipboardList';
  return (
    <div className={cx('rounded-2xl border bg-ink-900 p-4 transition-colors',
      granted ? 'border-line' : 'border-line/60 opacity-80')}>
      <div className="flex items-start gap-3">
        <div className="relative">
          <Avatar name={a.member} color={a.color} size={42} />
          <span className={cx('absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full border-2 border-ink-900',
            granted ? 'bg-mint' : 'bg-zinc-600')} />
        </div>
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2">
            <span className="truncate text-[14px] font-700 text-zinc-100">{a.member}</span>
            {granted
              ? <Chip tone="mint"><span className="h-1.5 w-1.5 rounded-full bg-mint proflow-pulse" /> Live-linked</Chip>
              : <Chip tone="neutral"><Icon name="Lock" size={10} /> Revoked</Chip>}
          </div>
          <div className="truncate text-[12px] text-zinc-500">{a.role} · {a.dept}</div>
        </div>
        <Switch on={granted} onChange={onToggle} accent="proflow" />
      </div>

      {/* granted tool */}
      <div className="mt-3 flex items-center gap-2.5 rounded-xl border border-line bg-ink-850 px-3 py-2.5">
        <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-proflow/12 text-proflow-soft">
          <Icon name={toolIcon} size={16} />
        </span>
        <div className="min-w-0 flex-1">
          <div className="text-[10px] uppercase tracking-[0.1em] text-zinc-500">Granted tool</div>
          <div className="truncate text-[12.5px] font-600 text-zinc-200">{a.toolLabel}</div>
        </div>
        <Icon name={granted ? 'Smartphone' : 'SmartphoneNfc'} size={15} className={granted ? 'text-mint' : 'text-zinc-600'} />
      </div>

      {/* scope chips */}
      <div className="mt-2.5 flex flex-wrap gap-1.5">
        {a.scope.map(s => (
          <span key={s} className={cx('inline-flex items-center gap-1 rounded-full border px-2 py-[3px] text-[10.5px] font-600',
            granted ? 'border-line bg-ink-800 text-zinc-300' : 'border-line/60 bg-ink-850 text-zinc-600 line-through')}>
            <Icon name="Check" size={9} sw={3} />{s}
          </span>
        ))}
      </div>

      <div className="mt-3 flex items-center justify-between border-t border-line/70 pt-2.5 text-[11px]">
        <span className="flex items-center gap-1.5 text-zinc-500"><Icon name="Smartphone" size={11} />{a.device}</span>
        <span className={cx('flex items-center gap-1.5', granted ? 'text-zinc-400' : 'text-zinc-600')}>
          <Icon name="Clock" size={11} />{granted ? a.lastActive : 'no access'}
        </span>
      </div>
    </div>
  );
}

/* ---- invite crew modal (real QR + join link) ---- */
function InviteModal({ onClose }) {
  const [copied, setCopied] = useState(false);
  const link = useMemo(() => { try { return new URL('mobile.html', location.href).href; } catch (e) { return 'mobile.html'; } }, []);
  const QR = window.QrCode;
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className="fade-up relative w-full max-w-sm rounded-2xl border border-line bg-ink-850 p-5">
        <button onClick={onClose} className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-lg bg-ink-800 text-zinc-400 hover:text-zinc-200"><Icon name="X" size={16} /></button>
        <div className="flex items-center gap-2"><Icon name="QrCode" size={18} className="text-proflow-soft" /><h3 className="font-display text-[20px] font-600 text-head">Invite crew</h3></div>
        <p className="mt-1 text-[12.5px] text-zinc-400">Scan to open the mobile view. Per-department crew sign-in activates with the backend.</p>
        <div className="mx-auto mt-4 w-fit rounded-xl bg-white p-3">
          {QR ? <QR value={link} size={186} /> : <div style={{ width: 186, height: 186 }} className="flex items-center justify-center text-[11px] font-600 text-zinc-500">QR unavailable</div>}
        </div>
        <div className="mt-4 flex items-center gap-2 rounded-xl border border-line bg-ink-900 p-2 pl-3">
          <span className="flex-1 truncate font-mono text-[12px] text-zinc-300">{link}</span>
          <button onClick={() => { try { navigator.clipboard && navigator.clipboard.writeText(link); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1600); }}
            className={cx('flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-700 transition-colors', copied ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
            <Icon name={copied ? 'Check' : 'Copy'} size={13} />{copied ? 'Copied' : 'Copy'}
          </button>
        </div>
      </div>
    </div>
  );
}

function AccessModule({ mobile }) {
  const { access, setAccess } = useContext(AccessContext);
  const [invite, setInvite] = useState(false);
  const granted = ACCESS.filter(a => access[a.id]).length;

  return (
    <div className="fade-up">
      <SectionTitle kicker="Crew & Departments" icon="KeyRound" title="Department Access"
        action={<Chip tone="mint"><Icon name="Radio" size={12} /> {granted} devices linked</Chip>} />

      {/* explainer banner */}
      <div className="mb-5 flex flex-col gap-3 rounded-2xl border border-proflow/25 bg-proflow/5 p-4 sm:flex-row sm:items-center">
        <span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-proflow/15 text-proflow-soft">
          <Icon name="Smartphone" size={22} />
        </span>
        <div className="flex-1">
          <div className="text-[14px] font-700 text-head">Plan crew phone access — decide what each department sees <span className="ml-1 align-middle rounded border border-amber/40 bg-amber/10 px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider text-amber">Preview</span></div>
          <div className="text-[12.5px] text-zinc-400">Map each department to the tool they&rsquo;ll see on their phone. This is a preview of that layout — access becomes enforceable once crew sign-in ships with the backend.</div>
        </div>
        <button onClick={() => setInvite(true)} className="flex shrink-0 items-center justify-center gap-2 rounded-xl bg-cta px-3.5 py-2.5 text-[12.5px] font-700 text-cta-ink hover:bg-cta-hover">
          <Icon name="QrCode" size={15} /> Invite crew
        </button>
      </div>
      {invite && <InviteModal onClose={() => setInvite(false)} />}

      <div className={cx('grid gap-3.5', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
        {ACCESS.map(a => (
          <AccessRow key={a.id} a={a} granted={!!access[a.id]} mobile={mobile}
            onToggle={() => setAccess(m => ({ ...m, [a.id]: !m[a.id] }))} />
        ))}
      </div>

      <div className="mt-4 flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-4 py-3 text-[12px] text-zinc-500">
        <Icon name="Info" size={14} className="text-electric-soft" />
        Switch the header to <span className="font-600 text-zinc-300">Mobile</span> and pick a crew member to preview exactly what they see on set.
      </div>
    </div>
  );
}

window.AccessModule = AccessModule;


/* ===== mod_floorplan ===== */
/* ============================================================
   FLOOR PLAN DESIGNER — technical Shot-Designer-style stage plan
   • Point fixtures (lights / camera / grip / props) — drag + rotate
   • Adjustable WALLS and CAMERA / ACTOR MOVE paths — drag endpoints,
     live length read-out in feet, snap-to-grid
   • Auto equipment list + drag-drop rental/equipment import
   • Share lighting plan with the DOP
   ============================================================ */
const FP_LOCATIONS = ['Cockpit — Stage 4', 'Tarmac — Runway B', 'Hangar 7 — Reveal'];
function fpDef(key) { return FIXTURES.find(f => f.key === key) || FIXTURES[0]; }

/* ---- production-grade plan symbols (schematic top-down, front = +x) ---- */
const FP_DIRECTIONAL = ['hmi', 'tungsten', 'led', 'kino', 'camera', 'dolly', 'move', 'flag', 'bounce', 'diff', 'wall', 'window', 'door', 'furniture'];
function FixtureSymbol({ type, sz = 34 }) {
  const d = fpDef(type);
  const c = d.color;
  const soft = c + '26';
  const m = 17, V = 34;
  const S = ch => <svg width={sz} height={sz} viewBox={`0 0 ${V} ${V}`} style={{ overflow: 'visible', display: 'block' }}>{ch}</svg>;
  switch (type) {
    case 'hmi': case 'tungsten':
      return S(<g>
        <circle cx={m - 2} cy={m} r="9" fill={soft} stroke={c} strokeWidth="1.6" />
        {type === 'tungsten'
          ? <g stroke={c} strokeWidth="1.1"><line x1={m - 6} y1={m - 3} x2={m + 2} y2={m + 3} /><line x1={m - 6} y1={m + 3} x2={m + 2} y2={m - 3} /></g>
          : <circle cx={m - 2} cy={m} r="2.3" fill={c} />}
        <path d={`M ${m + 5.5} ${m - 7.2} A 9 9 0 0 1 ${m + 5.5} ${m + 7.2}`} fill="none" stroke={c} strokeWidth="2.3" />
        <line x1={m + 5} y1={m - 7} x2={m + 12} y2={m - 11} stroke={c} strokeWidth="1.5" strokeLinecap="round" />
        <line x1={m + 5} y1={m + 7} x2={m + 12} y2={m + 11} stroke={c} strokeWidth="1.5" strokeLinecap="round" />
      </g>);
    case 'led':
      return S(<g>
        <rect x={m - 9} y={m - 11} width="9" height="22" rx="1.5" fill={soft} stroke={c} strokeWidth="1.6" />
        <g stroke={c} strokeWidth="1" opacity="0.75">{[-7, -2, 3, 8].map(y => <line key={y} x1={m - 6.5} y1={m + y} x2={m - 2.5} y2={m + y} />)}</g>
        <line x1={m} y1={m - 11} x2={m} y2={m + 11} stroke={c} strokeWidth="2.4" strokeLinecap="round" />
      </g>);
    case 'kino':
      return S(<g>
        <rect x={m - 8} y={m - 12} width="7" height="24" rx="1.5" fill={soft} stroke={c} strokeWidth="1.4" />
        <g stroke={c} strokeWidth="1.3" opacity="0.9">{[-6, -4, -2].map(x => <line key={x} x1={m + x} y1={m - 9} x2={m + x} y2={m + 9} />)}</g>
        <line x1={m - 1} y1={m - 12} x2={m - 1} y2={m + 12} stroke={c} strokeWidth="2" strokeLinecap="round" />
      </g>);
    case 'china':
      return S(<g>
        <circle cx={m} cy={m} r="10" fill={soft} stroke={c} strokeWidth="1.4" />
        <circle cx={m} cy={m} r="5.5" fill="none" stroke={c} strokeWidth="1.1" opacity="0.8" />
        <circle cx={m} cy={m} r="1.8" fill={c} />
      </g>);
    case 'practical':
      return S(<g>
        <circle cx={m} cy={m} r="6" fill={soft} stroke={c} strokeWidth="1.5" />
        <circle cx={m} cy={m} r="2" fill={c} />
        <g stroke={c} strokeWidth="1.2" strokeLinecap="round">{[0, 45, 90, 135, 180, 225, 270, 315].map(a => { const r = a * Math.PI / 180; return <line key={a} x1={m + Math.cos(r) * 8} y1={m + Math.sin(r) * 8} x2={m + Math.cos(r) * 11} y2={m + Math.sin(r) * 11} />; })}</g>
      </g>);
    case 'camera':
      return S(<g>
        <rect x={m - 9} y={m - 7} width="13" height="14" rx="1.6" fill={c + 'd9'} stroke="#0009" strokeWidth="1" />
        <rect x={m - 6} y={m - 10.5} width="6" height="3.4" rx="1" fill={c} />
        <path d={`M ${m + 4} ${m - 5} L ${m + 11.5} ${m - 7.5} L ${m + 11.5} ${m + 7.5} L ${m + 4} ${m + 5} Z`} fill={c} stroke="#0009" strokeWidth="0.8" />
      </g>);
    case 'dolly':
      return S(<g>
        <rect x={m - 10} y={m - 8} width="20" height="16" rx="2.5" fill={soft} stroke={c} strokeWidth="1.5" />
        {[[-6, -8], [6, -8], [-6, 8], [6, 8]].map(([x, y], i) => <circle key={i} cx={m + x} cy={m + y} r="2.4" fill={c} />)}
        <rect x={m - 3.5} y={m - 4} width="8" height="8" rx="1.2" fill={c + 'cc'} />
        <path d={`M ${m + 4.5} ${m - 3} L ${m + 9} ${m - 4.5} L ${m + 9} ${m + 4.5} L ${m + 4.5} ${m + 3} Z`} fill={c} />
      </g>);
    case 'move':
      return S(<g stroke={c} strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round"><path d={`M ${m - 8} ${m} L ${m + 4} ${m}`} /><path d={`M ${m - 1} ${m - 5} L ${m + 6} ${m} L ${m - 1} ${m + 5}`} /></g>);
    case 'flag':
      return S(<rect x={m - 2.6} y={m - 13} width="5.2" height="26" rx="1" fill="#14161c" stroke={c} strokeWidth="1.5" />);
    case 'bounce':
      return S(<g>
        <rect x={m - 3} y={m - 13} width="6" height="26" rx="1" fill={soft} stroke={c} strokeWidth="1.4" />
        <g stroke={c} strokeWidth="0.9" opacity="0.85">{[-9, -5, -1, 3, 7, 11].map(y => <line key={y} x1={m - 2.5} y1={m + y} x2={m + 2.5} y2={m + y - 2} />)}</g>
        <line x1={m + 3} y1={m - 13} x2={m + 3} y2={m + 13} stroke={c} strokeWidth="2" />
      </g>);
    case 'diff':
      return S(<rect x={m - 3} y={m - 13} width="6" height="26" rx="1" fill="none" stroke={c} strokeWidth="1.5" strokeDasharray="2.2 2.2" />);
    case 'wall':
      return S(<rect x={m - 2.6} y={m - 13} width="5.2" height="26" rx="0.5" fill={c} />);
    case 'door':
      return S(<g fill="none" stroke={c} strokeWidth="1.6" strokeLinecap="round">
        <line x1={m - 9} y1={m + 9} x2={m - 9} y2={m - 9} />
        <line x1={m - 9} y1={m - 9} x2={m + 7} y2={m - 9} />
        <path d={`M ${m + 7} ${m - 9} A 16 16 0 0 1 ${m - 9} ${m + 7}`} strokeDasharray="2 2" opacity="0.8" />
      </g>);
    case 'window':
      return S(<g stroke={c} strokeWidth="1.4"><rect x={m - 2} y={m - 13} width="4" height="26" fill={soft} /><line x1={m} y1={m - 13} x2={m} y2={m + 13} /></g>);
    case 'furniture':
      return S(<g fill={soft} stroke={c} strokeWidth="1.5"><rect x={m - 10} y={m - 7} width="20" height="14" rx="2.5" /><rect x={m - 10} y={m - 7} width="5" height="14" rx="2" /></g>);
    case 'mark':
      return S(<g>
        <circle cx={m} cy={m} r="8.5" fill={c + '33'} stroke={c} strokeWidth="1.6" />
        <circle cx={m} cy={m} r="3" fill={c} />
        <g stroke={c} strokeWidth="1.3" strokeLinecap="round"><line x1={m} y1={m - 11} x2={m} y2={m - 7} /><line x1={m} y1={m + 7} x2={m} y2={m + 11} /><line x1={m - 11} y1={m} x2={m - 7} y2={m} /><line x1={m + 7} y1={m} x2={m + 11} y2={m} /></g>
      </g>);
    default:
      return S(<circle cx={m} cy={m} r="8" fill={soft} stroke={c} strokeWidth="1.6" />);
  }
}

/* ---- photometric throw: soft cone for lights, FOV wedge for camera ---- */
function Beam({ id, type, color, rot }) {
  const cam = type === 'camera';
  const len = cam ? 168 : 128;
  const half = (cam ? 0.62 : 0.82) * len;
  const gid = 'fpbeam' + id;
  return (
    <svg className="pointer-events-none absolute" width={len} height={half * 2}
      style={{ left: 0, top: 0, transform: `translateY(-50%) rotate(${rot}deg)`, transformOrigin: '0 50%', overflow: 'visible', zIndex: -1 }}>
      <defs>
        <linearGradient id={gid} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stopColor={color} stopOpacity={cam ? 0.26 : 0.30} />
          <stop offset="0.7" stopColor={color} stopOpacity={cam ? 0.05 : 0.06} />
          <stop offset="1" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <polygon points={`8,${half} ${len},${half * 0.06} ${len},${half * 1.94}`} fill={`url(#${gid})`} />
      {cam && <g stroke={color} strokeWidth="1" opacity="0.45" fill="none">
        <line x1="8" y1={half} x2={len} y2={half * 0.06} /><line x1="8" y1={half} x2={len} y2={half * 1.94} />
      </g>}
    </svg>
  );
}

function FixtureNode({ it, selected, readOnly, onDown, onSelect }) {
  const d = fpDef(it.type);
  const rot = it.rot || 0;
  const directional = !!d.beam || !!d.arrow || FP_DIRECTIONAL.includes(it.type);
  return (
    <div className="absolute" style={{ left: it.x + '%', top: it.y + '%', zIndex: selected ? 30 : 12 }}>
      {d.beam && <Beam id={it.id} type={it.type} color={d.color} rot={rot} />}
      {d.arrow && <div className="pointer-events-none absolute" style={{ left: 0, top: 0, width: 84, height: 0, transform: `translateY(-50%) rotate(${rot}deg)`, transformOrigin: '0 50%', borderTop: `2px dashed ${d.color}` }} />}
      {selected && <span className="pointer-events-none absolute" style={{ left: 0, top: 0, width: 44, height: 44, transform: 'translate(-50%,-50%)', borderRadius: '50%', boxShadow: `0 0 0 1.5px #fff, 0 0 0 4px ${d.color}55` }} />}
      <div className="pointer-events-none absolute" style={{ left: 0, top: 0, transform: `translate(-50%,-50%) rotate(${directional ? rot : 0}deg)`, filter: 'drop-shadow(0 1px 2.5px rgba(0,0,0,.55))' }}>
        <FixtureSymbol type={it.type} />
      </div>
      <button onPointerDown={onDown} onClick={e => { e.stopPropagation(); onSelect(); }} aria-label={d.label}
        className="absolute select-none" style={{ left: 0, top: 0, width: 36, height: 36, transform: 'translate(-50%,-50%)', background: 'transparent', border: 0, padding: 0, touchAction: 'none', cursor: readOnly ? 'pointer' : 'grab' }} />
    </div>
  );
}

function PaletteItem({ d, active, onClick }) {
  const isFix = FIXTURES.some(f => f.key === d.key);
  return (
    <button onClick={onClick}
      className={cx('flex items-center gap-2 rounded-lg border px-2.5 py-2 text-left transition-colors',
        active ? 'border-electric bg-electric/12' : 'border-line bg-ink-900 hover:border-ink-600')}>
      <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md" style={{ background: isFix ? 'rgba(7,9,15,.55)' : d.color + '22', color: d.color }}>
        {isFix ? <FixtureSymbol type={d.key} sz={26} /> : <Icon name={d.icon} size={15} />}
      </span>
      <span className="truncate text-[12px] font-600 text-zinc-200">{d.label}</span>
    </button>
  );
}

/* ---- annotation tools + photo pins for the plan ---- */
const FP_ANNOT = [
  { key: 'label', label: 'Label', icon: 'Tag', color: '#F5D469', kindTag: 'note' },
  { key: 'rednote', label: 'Remark (red)', icon: 'Type', color: '#FF4D6D', kindTag: 'note' },
  { key: 'photo', label: 'Photo pin', icon: 'Image', color: '#9DB2D0', kindTag: 'photo' },
];
const fpShrink = file => new Promise(res => {
  const rd = new FileReader();
  rd.onload = () => {
    const img = new Image();
    img.onload = () => {
      const c = document.createElement('canvas');
      const w = Math.min(420, img.width || 420);
      c.width = w; c.height = Math.max(1, Math.round(w * (img.height || 1) / (img.width || 1)));
      c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
      res(c.toDataURL('image/jpeg', 0.82));
    };
    img.src = rd.result;
  };
  rd.readAsDataURL(file);
});
function PhotoPin({ ph, selected, readOnly, onDown, onSelect, onSrc }) {
  const [over, setOver] = useState(false);
  const inputRef = useRef(null);
  return (
    <div onPointerDown={onDown} onClick={e => { e.stopPropagation(); onSelect(); }}
      onDragOver={e => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={async e => { if (readOnly) return; e.preventDefault(); e.stopPropagation(); setOver(false); const f = e.dataTransfer.files[0]; if (f && /^image\//.test(f.type)) onSrc(await fpShrink(f)); }}
      className={cx('absolute -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-md shadow-lg', selected ? 'ring-2 ring-white/70' : 'ring-1 ring-black/40')}
      style={{ left: ph.x + '%', top: ph.y + '%', width: (ph.w || 18) + '%', zIndex: selected ? 34 : 12, cursor: readOnly ? 'default' : 'grab', touchAction: 'none', background: '#0B0E1A' }}>
      {ph.src
        ? <img src={ph.src} alt="" draggable="false" style={{ width: '100%', display: 'block' }} />
        : <div className={cx('flex aspect-video w-full flex-col items-center justify-center gap-1 border border-dashed text-[9px] font-600',
            over ? 'border-proflow text-proflow-soft' : 'border-zinc-600 text-zinc-500')}
            onClick={e => { e.stopPropagation(); onSelect(); if (!readOnly && inputRef.current) inputRef.current.click(); }}>
            <Icon name="ImagePlus" size={13} /> drop still
          </div>}
      <input ref={inputRef} type="file" accept="image/*" className="hidden"
        onChange={async e => { const f = e.target.files[0]; if (f) onSrc(await fpShrink(f)); e.target.value = ''; }} />
    </div>
  );
}

/* ---- print layout: page 1 = whole scene fit to page, page 2 = info (both landscape) ---- */
function FpPrintRow({ dot, name, val }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0', borderBottom: '1px solid #eee', fontSize: 11.5 }}>
      {dot && <span style={{ width: 9, height: 9, borderRadius: 9, background: dot, flex: 'none' }} />}
      <span style={{ flex: 1 }}>{name}</span>
      <span style={{ fontFamily: "'JetBrains Mono',monospace", fontWeight: 600 }}>{val}</span>
    </div>
  );
}
function FpPrintHead({ children, mt }) {
  return <div style={{ fontSize: 12, fontWeight: 800, fontStyle: 'italic', borderBottom: '2px solid #1a1a1a', paddingBottom: 4, marginTop: mt || 0, marginBottom: 8 }}>{children}</div>;
}
function FloorPlanPrint({ items, scaleFt, loc, size, byCat, imported }) {
  const segs = items.filter(i => i.kind === 'segment');
  const fixt = items.filter(i => i.kind === 'fixture');
  const notes = items.filter(i => i.kind === 'note');
  let minX = 100, maxX = 0, minY = 100, maxY = 0;
  items.forEach(it => {
    if (it.kind === 'segment') { minX = Math.min(minX, it.x1, it.x2); maxX = Math.max(maxX, it.x1, it.x2); minY = Math.min(minY, it.y1, it.y2); maxY = Math.max(maxY, it.y1, it.y2); }
    else if (it.x != null) { minX = Math.min(minX, it.x); maxX = Math.max(maxX, it.x); minY = Math.min(minY, it.y); maxY = Math.max(maxY, it.y); }
  });
  if (!items.length) { minX = 0; maxX = 100; minY = 0; maxY = 100; }
  const pad = 7; minX -= pad; maxX += pad; minY -= pad; maxY += pad;
  const aspect = (size.w / size.h) || 1.3;
  const WORLD_W = 1000, WORLD_H = WORLD_W / aspect;
  const AREA_W = 1080, AREA_H = 600;
  const bwPx = (maxX - minX) / 100 * WORLD_W, bhPx = (maxY - minY) / 100 * WORLD_H;
  const k = Math.min(AREA_W / bwPx, AREA_H / bhPx) || 1;
  const offX = (AREA_W - bwPx * k) / 2 - (minX / 100 * WORLD_W) * k;
  const offY = (AREA_H - bhPx * k) / 2 - (minY / 100 * WORLD_H) * k;
  const cellP = (5 / scaleFt) * WORLD_W * k;
  const today = new Date().toLocaleDateString(slLocale(), { year: 'numeric', month: 'short', day: 'numeric' });
  const segLen = s => { const dx = (s.x2 - s.x1) / 100 * size.w, dy = (s.y2 - s.y1) / 100 * size.h; return (Math.hypot(dx, dy) / size.w) * scaleFt; };
  const segAng = s => Math.atan2((s.y2 - s.y1) / 100 * size.h, (s.x2 - s.x1) / 100 * size.w) * 180 / Math.PI;
  const lensPath = s => { const mx = (s.x1 + s.x2) / 2, my = (s.y1 + s.y2) / 2, dx = s.x2 - s.x1, dy = s.y2 - s.y1, L = Math.hypot(dx, dy) || 1, nx = -dy / L, ny = dx / L, b = L * 0.17; return `M ${s.x1} ${s.y1} Q ${mx + nx * b} ${my + ny * b} ${s.x2} ${s.y2} Q ${mx + nx * b * 0.22} ${my + ny * b * 0.22} ${s.x1} ${s.y1} Z`; };
  const moves = segs.filter(s => s.type !== 'wall');
  const walls = segs.filter(s => s.type === 'wall');
  return (
    <div style={{ fontFamily: "'Barlow',sans-serif", color: '#1a1a1a' }}>
      {/* ===== PAGE 1 — the plan ===== */}
      <div className="fp-page" style={{ display: 'flex', flexDirection: 'column' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderBottom: '2px solid #1a1a1a', paddingBottom: 8, marginBottom: 10 }}>
          <div>
            <div style={{ fontSize: 10.5, letterSpacing: '.16em', textTransform: 'uppercase', color: '#888' }}>Floor Plan · Lighting &amp; Blocking</div>
            <div style={{ fontSize: 26, fontWeight: 800, fontStyle: 'italic', lineHeight: 1.1 }}>{loc}</div>
          </div>
          <div style={{ textAlign: 'right', fontSize: 10.5, color: '#666', fontFamily: "'JetBrains Mono',monospace", lineHeight: 1.6 }}>
            <div>SILVERLINES · Crimson Skies</div>
            <div>stage {lenLabel(scaleFt)} wide · grid 5ft</div>
            <div>{today}</div>
          </div>
        </div>
        <div style={{ position: 'relative', flex: 1, minHeight: AREA_H, border: '1px solid #cfc8ba', borderRadius: 6, overflow: 'hidden', background: '#fcfbf7',
          backgroundImage: 'linear-gradient(rgba(64,86,140,.26) 1px,transparent 1px),linear-gradient(90deg,rgba(64,86,140,.26) 1px,transparent 1px),linear-gradient(rgba(64,86,140,.11) 1px,transparent 1px),linear-gradient(90deg,rgba(64,86,140,.11) 1px,transparent 1px)',
          backgroundSize: `${cellP * 5}px ${cellP * 5}px, ${cellP * 5}px ${cellP * 5}px, ${cellP}px ${cellP}px, ${cellP}px ${cellP}px`,
          backgroundPosition: `${offX}px ${offY}px, ${offX}px ${offY}px, ${offX}px ${offY}px, ${offX}px ${offY}px` }}>
          <div style={{ position: 'absolute', left: 0, top: 0, width: WORLD_W, height: WORLD_H, transformOrigin: '0 0', transform: `translate(${offX}px,${offY}px) scale(${k})` }}>
            <svg width={WORLD_W} height={WORLD_H} viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, overflow: 'visible' }}>
              {segs.map(s => { const d = segDef(s.type);
                if (d.lens) return <path key={s.id} d={lensPath(s)} fill={d.color} opacity="0.7" stroke="#5E9963" strokeWidth="0.35" />;
                return <line key={s.id} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke={d.color} strokeWidth={d.w} vectorEffect="non-scaling-stroke" strokeLinecap={s.type === 'wall' ? 'butt' : 'round'} strokeDasharray={d.dash ? '6 5' : undefined} />;
              })}
            </svg>
            {segs.map(s => { const d = segDef(s.type); const ang = segAng(s); const mx = (s.x1 + s.x2) / 2, my = (s.y1 + s.y2) / 2;
              const txt = s.label ? s.label + (s.type === 'blackflag' ? '' : ' · ' + lenLabel(segLen(s))) : (s.type === 'wall' ? lenLabel(segLen(s)) : '');
              return (
                <div key={s.id}>
                  {d.arrow && <div style={{ position: 'absolute', left: s.x2 + '%', top: s.y2 + '%', transform: `translate(-50%,-50%) rotate(${ang}deg)`, width: 0, height: 0, borderLeft: `9px solid ${d.color}`, borderTop: '5px solid transparent', borderBottom: '5px solid transparent' }} />}
                  {txt && <div style={{ position: 'absolute', left: mx + '%', top: my + '%', transform: 'translate(-50%,-50%)', whiteSpace: 'nowrap', background: '#1a1a1a', color: '#fff', borderRadius: 3, padding: '1px 4px', fontSize: 8, fontFamily: "'JetBrains Mono',monospace" }}>{txt}</div>}
                </div>
              );
            })}
            {fixt.map(it => <FixtureNode key={it.id} it={it} selected={false} readOnly={true} onSelect={() => {}} onDown={() => {}} />)}
            {fixt.filter(f => f.label).map(f => <div key={'l' + f.id} style={{ position: 'absolute', left: f.x + '%', top: f.y + '%', transform: 'translate(-50%,22px)', whiteSpace: 'nowrap', background: '#1a1a1a', color: '#fff', borderRadius: 4, padding: '1px 5px', fontSize: 8, fontWeight: 700 }}>{f.label}</div>)}
            {notes.map(n => <div key={n.id} style={{ position: 'absolute', left: n.x + '%', top: n.y + '%', transform: 'translate(-50%,-50%)', maxWidth: '30%', fontSize: 9, fontWeight: 800, lineHeight: 1.1, padding: '2px 4px', borderRadius: 3, background: n.tone === 'red' ? 'transparent' : '#F5D469', color: n.tone === 'red' ? '#d11' : '#1A1505' }}>{n.text}</div>)}
          </div>
        </div>
      </div>
      {/* ===== PAGE 2 — equipment, moves, legend, notes ===== */}
      <div className="fp-page">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderBottom: '2px solid #1a1a1a', paddingBottom: 8, marginBottom: 16 }}>
          <div>
            <div style={{ fontSize: 10.5, letterSpacing: '.16em', textTransform: 'uppercase', color: '#888' }}>Plan Notes &amp; Equipment</div>
            <div style={{ fontSize: 24, fontWeight: 800, fontStyle: 'italic' }}>{loc}</div>
          </div>
          <div style={{ textAlign: 'right', fontSize: 10.5, color: '#666', fontFamily: "'JetBrains Mono',monospace" }}>{today}</div>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40 }}>
          <div>
            <FpPrintHead>Equipment list</FpPrintHead>
            {byCat.map(g => (
              <div key={g.cat} style={{ marginBottom: 12 }}>
                <div style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: '.12em', textTransform: 'uppercase', color: '#999', marginBottom: 2 }}>{g.cat}</div>
                {g.rows.map(r => <FpPrintRow key={r.key} dot={r.color} name={r.label} val={'×' + r.n} />)}
              </div>
            ))}
            {imported && imported.length > 0 && (
              <div style={{ marginBottom: 12 }}>
                <div style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: '.12em', textTransform: 'uppercase', color: '#999', marginBottom: 2 }}>Imported · Rental</div>
                {imported.map((r, i) => <FpPrintRow key={i} dot="#F57F45" name={r.name + ' · ' + r.dept} val={'×' + r.qty} />)}
              </div>
            )}
          </div>
          <div>
            <FpPrintHead>Camera &amp; actor moves</FpPrintHead>
            {moves.length ? moves.map(s => <FpPrintRow key={s.id} dot={segDef(s.type).color} name={s.label || segDef(s.type).label} val={s.type === 'blackflag' ? 'neg fill' : lenLabel(segLen(s))} />) : <div style={{ fontSize: 11, color: '#999' }}>None</div>}
            <FpPrintHead mt={18}>Set dimensions</FpPrintHead>
            {walls.length ? walls.map((s, i) => <FpPrintRow key={s.id} name={'Wall ' + (i + 1)} val={lenLabel(segLen(s))} />) : <div style={{ fontSize: 11, color: '#999' }}>None</div>}
            {notes.length > 0 && <>
              <FpPrintHead mt={18}>Notes</FpPrintHead>
              {notes.map(n => <div key={n.id} style={{ fontSize: 11.5, marginBottom: 4, color: n.tone === 'red' ? '#c00' : '#333' }}>• {n.text}</div>)}
            </>}
          </div>
        </div>
        <div style={{ marginTop: 22 }}>
          <FpPrintHead>Legend</FpPrintHead>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px 26px' }}>
            {['camera', 'hmi', 'led', 'kino', 'china', 'flag', 'mark'].map(t => (
              <div key={t} style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 11 }}>
                <span style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><FixtureSymbol type={t} sz={24} /></span>
                {fpDef(t).label}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function FloorPlanModule({ mobile, readOnly = false }) {
  const [loc, setLoc] = useState(FP_LOCATIONS[0]);
  /* one plan per location, all kept under a single store (migrates the old single-plan key) */
  const [allPlans, setAllPlans] = useState(() => {
    const s = window.SLStore.get('silverlines.floorplan.v2'); if (s && typeof s === 'object' && !Array.isArray(s)) return s;
    let v1 = window.SLStore.get('silverlines.floorplan.v1', null);
    return { [FP_LOCATIONS[0]]: (Array.isArray(v1) && v1.length) ? v1 : (window.PROJ_BLANK ? [] : FLOORPLAN_INIT) };
  });
  const items = allPlans[loc] || [];
  const setItems = updater => setAllPlans(prev => {
    const cur = prev[loc] || [];
    const next = typeof updater === 'function' ? updater(cur) : updater;
    const all = { ...prev, [loc]: next };
    window.SLStore.set('silverlines.floorplan.v2', all);
    return all;
  });
  /* paper (day) vs screen (night) — canvas turns white like a printed plan */
  const [paper, setPaper] = useState(() => { try { return document.documentElement.dataset.theme === 'day'; } catch (e) { return false; } });
  useEffect(() => {
    const el = document.documentElement;
    const obs = new MutationObserver(() => setPaper(el.dataset.theme === 'day'));
    obs.observe(el, { attributes: true, attributeFilter: ['data-theme'] });
    return () => obs.disconnect();
  }, []);
  const [big, setBig] = useState(false);
  useEffect(() => {
    if (!big) return;
    const h = e => { if (e.key === 'Escape') setBig(false); };
    window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
  }, [big]);
  const [tool, setTool] = useState(null);       // fixture key or segment key
  const [toolKind, setToolKind] = useState(null); // 'fixture' | 'segment'
  const [sel, setSel] = useState(null);
  const [cat, setCat] = useState('Build');
  const [shared, setShared] = useState(false);
  const [snap, setSnap] = useState(true);
  const [toolsOpen, setToolsOpen] = useState(!mobile);
  const [equipOpen, setEquipOpen] = useState(!mobile);
  const [scaleFt, setScaleFt] = useState(40);   // set width in feet
  const [imported, setImported] = useState([]);
  const [size, setSize] = useState({ w: 600, h: 460 });
  const canvasRef = useRef(null);
  const drag = useRef(null);                     // {id, mode:'move'|'a'|'b'|'fix', last:{x,y}}
  const [view, setView] = useState({ x: 0, y: 0, k: 1 });   // pan (px) + zoom
  const viewRef = useRef(view); viewRef.current = view;
  const pan = useRef(null);                       // {sx,sy,ox,oy,moved}
  const nextId = useRef(0);
  if (!nextId.current) nextId.current = items.reduce((m, i) => Math.max(m, (parseInt(String(i.id).replace(/\D/g, '')) || 0) + 1), 1000);

  // wire ResizeObserver + non-passive wheel to the live canvas node (survives remount)
  const roRef = useRef(null);
  const onWheel = React.useCallback(e => {
    e.preventDefault();
    const el = canvasRef.current; if (!el) return;
    const r = el.getBoundingClientRect();
    const sx = e.clientX - r.left, sy = e.clientY - r.top;
    setView(v => {
      const cl = (x, a, b) => Math.max(a, Math.min(b, x));
      const k2 = cl(v.k * (e.deltaY < 0 ? 1.12 : 1 / 1.12), 0.3, 5);
      const wx = (sx - v.x) / v.k, wy = (sy - v.y) / v.k;
      return { k: k2, x: sx - wx * k2, y: sy - wy * k2 };
    });
  }, []);
  const setCanvasEl = React.useCallback(node => {
    if (roRef.current) { roRef.current.disconnect(); roRef.current = null; }
    if (canvasRef.current) canvasRef.current.removeEventListener('wheel', onWheel);
    canvasRef.current = node;
    if (!node) return;
    const measure = () => { const r = node.getBoundingClientRect(); setSize({ w: r.width, h: r.height }); };
    const ro = new ResizeObserver(measure); ro.observe(node); roRef.current = ro; measure();
    node.addEventListener('wheel', onWheel, { passive: false });
  }, [onWheel]);

  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  const snapv = v => snap ? Math.round(v / 2.5) * 2.5 : v;
  /* screen → world percent (accounts for pan + zoom); no hard frame boundary */
  const ptFromEvent = e => {
    const r = canvasRef.current.getBoundingClientRect();
    const v = viewRef.current;
    const wx = (e.clientX - r.left - v.x) / v.k;
    const wy = (e.clientY - r.top - v.y) / v.k;
    return { x: clamp(wx / size.w * 100, -800, 900), y: clamp(wy / size.h * 100, -800, 900) };
  };

  useEffect(() => {
    const move = e => {
      if (pan.current) {
        const p = pan.current;
        const dx = e.clientX - p.sx, dy = e.clientY - p.sy;
        if (Math.abs(dx) + Math.abs(dy) > 3) p.moved = true;
        setView(v => ({ ...v, x: p.ox + dx, y: p.oy + dy }));
        return;
      }
      if (!drag.current || !canvasRef.current) return;
      const p = ptFromEvent(e);
      const { id, mode } = drag.current;
      setItems(its => its.map(o => {
        if (o.id !== id) return o;
        if (mode === 'fix') return { ...o, x: snapv(p.x), y: snapv(p.y) };
        if (mode === 'a') return { ...o, x1: snapv(p.x), y1: snapv(p.y) };
        if (mode === 'b') return { ...o, x2: snapv(p.x), y2: snapv(p.y) };
        if (mode === 'move') {
          const dx = p.x - drag.current.last.x, dy = p.y - drag.current.last.y;
          return { ...o, x1: o.x1 + dx, y1: o.y1 + dy, x2: o.x2 + dx, y2: o.y2 + dy };
        }
        return o;
      }));
      if (drag.current.mode === 'move') drag.current.last = p;
    };
    const up = () => {
      if (pan.current) { const moved = pan.current.moved; pan.current = null; if (!moved) setSel(null); return; }
      drag.current = null;
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
    return () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
  }, [snap, size.w, size.h]);

  const zoomBy = f => setView(v => {
    const r = canvasRef.current.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height / 2;
    const k2 = clamp(v.k * f, 0.3, 5);
    const wx = (cx - v.x) / v.k, wy = (cy - v.y) / v.k;
    return { k: k2, x: cx - wx * k2, y: cy - wy * k2 };
  });
  const resetView = () => setView({ x: 0, y: 0, k: 1 });

  /* export the whole scene as a 2-page landscape PDF (browser print → save as PDF) */
  const downloadPDF = () => {
    const st = document.createElement('style');
    st.id = 'fp-page-style';
    st.textContent = '@media print{@page{size:A4 landscape;margin:8mm}}';
    document.head.appendChild(st);
    document.body.classList.add('fp-print');
    const cleanup = () => { document.body.classList.remove('fp-print'); const s = document.getElementById('fp-page-style'); if (s) s.remove(); window.removeEventListener('afterprint', cleanup); };
    window.addEventListener('afterprint', cleanup);
    setTimeout(() => { window.print(); }, 90);
  };

  /* keyboard: +/- zoom, 0 reset, arrows pan (when the plan is focused/open) */
  useEffect(() => {
    if (readOnly) return;
    const onKey = e => {
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
      if (e.key === '+' || e.key === '=') { e.preventDefault(); zoomBy(1.25); }
      else if (e.key === '-' || e.key === '_') { e.preventDefault(); zoomBy(1 / 1.25); }
      else if (e.key === '0') { e.preventDefault(); resetView(); }
      else if (e.key === 'ArrowLeft') { e.preventDefault(); setView(v => ({ ...v, x: v.x + 60 })); }
      else if (e.key === 'ArrowRight') { e.preventDefault(); setView(v => ({ ...v, x: v.x - 60 })); }
      else if (e.key === 'ArrowUp') { e.preventDefault(); setView(v => ({ ...v, y: v.y + 60 })); }
      else if (e.key === 'ArrowDown') { e.preventDefault(); setView(v => ({ ...v, y: v.y - 60 })); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [readOnly, big, toolsOpen]);

  /* press empty floor (or middle-mouse anywhere) to pan; tool active = place instead */
  const onCanvasDown = e => {
    if (e.button === 1) e.preventDefault();
    if (e.button === 1 || !tool) {
      pan.current = { sx: e.clientX, sy: e.clientY, ox: viewRef.current.x, oy: viewRef.current.y, moved: false };
    }
  };

  const startDrag = (id, mode, e) => {
    if (readOnly) return; e.stopPropagation();
    drag.current = { id, mode, last: ptFromEvent(e) };
    setSel(id);
  };

  const placeAt = e => {
    if (readOnly || !tool || !canvasRef.current) { setSel(null); return; }
    const p = ptFromEvent(e);
    const id = 'x' + (nextId.current++);
    if (toolKind === 'segment') {
      const len = 16;
      setItems(its => [...its, { id, kind: 'segment', type: tool, x1: p.x - len / 2, y1: p.y, x2: p.x + len / 2, y2: p.y }]);
    } else if (toolKind === 'note') {
      setItems(its => [...its, { id, kind: 'note', x: p.x, y: p.y, text: tool === 'rednote' ? 'remark…' : 'LABEL', tone: tool === 'rednote' ? 'red' : 'yellow' }]);
    } else if (toolKind === 'photo') {
      setItems(its => [...its, { id, kind: 'photo', x: p.x, y: p.y, w: 18, src: null }]);
    } else {
      setItems(its => [...its, { id, kind: 'fixture', type: tool, x: snapv(p.x), y: snapv(p.y), rot: 0 }]);
    }
    setSel(id); setShared(false);
  };

  const selItem = items.find(i => i.id === sel);
  const update = (id, patch) => { setItems(its => its.map(o => o.id === id ? { ...o, ...patch } : o)); setShared(false); };
  const remove = id => {
    const prev = items; const it = items.find(i => i.id === id);
    const label = it ? (it.kind === 'fixture' ? (FIXTURES.find(f => f.key === it.type) || {}).label : it.kind === 'note' ? 'note' : 'segment') : 'element';
    setItems(its => its.filter(o => o.id !== id)); setSel(null); setShared(false);
    window.pushUndo && window.pushUndo(`Removed ${label}`, () => setItems(prev));
  };

  // geometry helpers
  const segLenFt = s => {
    const dx = (s.x2 - s.x1) / 100 * size.w, dy = (s.y2 - s.y1) / 100 * size.h;
    const px = Math.hypot(dx, dy);
    return (px / size.w) * scaleFt;
  };
  const segAngle = s => Math.atan2((s.y2 - s.y1) / 100 * size.h, (s.x2 - s.x1) / 100 * size.w) * 180 / Math.PI;
  /* lens path for black / neg-fill crescents */
  const lensPath = s => {
    const mx = (s.x1 + s.x2) / 2, my = (s.y1 + s.y2) / 2;
    const dx = s.x2 - s.x1, dy = s.y2 - s.y1;
    const L = Math.hypot(dx, dy) || 1;
    const nx = -dy / L, ny = dx / L, b = L * 0.17;
    return `M ${s.x1} ${s.y1} Q ${mx + nx * b} ${my + ny * b} ${s.x2} ${s.y2} Q ${mx + nx * b * 0.22} ${my + ny * b * 0.22} ${s.x1} ${s.y1} Z`;
  };

  const fixtures = items.filter(i => i.kind === 'fixture');
  const segments = items.filter(i => i.kind === 'segment');

  /* blueprint grid geometry — minor = 5ft, major = 25ft */
  const gMinX = 100 / (scaleFt / 5);
  const gMinY = gMinX * (size.w / size.h || 1.3);
  const gMajX = gMinX * 5, gMajY = gMinY * 5;
  /* screen-space grid cell (pans + zooms with the view) */
  const cellPx = Math.max(6, (5 / scaleFt) * size.w * view.k);
  const majPx = cellPx * 5;

  // equipment tally
  const counts = {};
  fixtures.forEach(i => { counts[i.type] = (counts[i.type] || 0) + 1; });
  const byCat = FIXTURE_CATS.map(c => ({ cat: c, rows: FIXTURES.filter(f => f.cat === c && counts[f.key]).map(f => ({ ...f, n: counts[f.key] })) })).filter(g => g.rows.length);

  const importSample = (name) => ([
    { item: 'ARRI SkyPanel S60-C', qty: '4', dept: 'Lighting' },
    { item: 'Astera Titan Tube', qty: '8', dept: 'Lighting' },
    { item: 'Matthews C-Stand 40"', qty: '12', dept: 'Grip' },
    { item: 'Fisher 11 Dolly + track', qty: '1', dept: 'Grip' },
  ]);
  const onImport = (rows) => {
    const mapped = rows.map(r => ({
      name: pick(r, ['item', 'name', 'desc', 'equip']) || 'Item',
      qty: pick(r, ['qty', 'quantity', 'count']) || '1',
      dept: pick(r, ['dept', 'category', 'type']) || 'Rental',
    })).filter(r => r.name);
    if (mapped.length) setImported(im => [...im, ...mapped]);
  };

  /* ---- canvas ---- */
  const canvas = (
    <div ref={setCanvasEl} onClick={placeAt} onPointerDown={onCanvasDown}
      className={cx('relative w-full select-none overflow-hidden rounded-2xl border', tool && !readOnly ? 'cursor-crosshair' : 'cursor-grab', paper ? 'border-[#d9d2c4]' : 'border-line')}
      style={(() => {
        const major = paper ? 'rgba(64,86,140,.34)' : 'rgb(var(--line)/.5)';
        const minor = paper ? 'rgba(64,86,140,.14)' : 'rgb(var(--line)/.2)';
        const pos = `${view.x}px ${view.y}px, ${view.x}px ${view.y}px, ${view.x}px ${view.y}px, ${view.x}px ${view.y}px`;
        return {
          height: mobile ? 320 : '100%', touchAction: 'none',
          backgroundColor: paper ? '#fcfbf7' : 'rgb(var(--ink-950))',
          backgroundImage: `linear-gradient(${major} 1px,transparent 1px),linear-gradient(90deg,${major} 1px,transparent 1px),linear-gradient(${minor} 1px,transparent 1px),linear-gradient(90deg,${minor} 1px,transparent 1px)${paper ? '' : ',radial-gradient(120% 120% at 50% 0%, rgb(var(--ink-900)) 0%, rgb(var(--ink-950)) 70%)'}`,
          backgroundSize: `${majPx}px ${majPx}px, ${majPx}px ${majPx}px, ${cellPx}px ${cellPx}px, ${cellPx}px ${cellPx}px${paper ? '' : ', 100% 100%'}`,
          backgroundPosition: paper ? pos : `${pos}, 0 0`,
          boxShadow: paper ? 'inset 0 0 0 1px rgba(0,0,0,.03)' : 'inset 0 0 160px rgba(0,0,0,.55)',
        };
      })()}>
      {/* scale chip */}
      <div className="pointer-events-none absolute left-3 top-3 z-40 flex items-center gap-2">
        <span className={cx('rounded-md border px-2 py-1 font-mono text-[10px] backdrop-blur-sm', paper ? 'border-[#d9d2c4] bg-white/80 text-zinc-700' : 'border-line/70 bg-black/55 text-zinc-200')}>{loc}</span>
        <span className={cx('rounded-md border px-2 py-1 font-mono text-[10px] backdrop-blur-sm', paper ? 'border-[#d9d2c4] bg-white/80 text-zinc-500' : 'border-line/70 bg-black/55 text-zinc-400')}>grid 5ft · {lenLabel(scaleFt)} wide</span>
      </div>

      {/* === pan/zoom world layer (everything placed lives here) === */}
      <div style={{ position: 'absolute', left: 0, top: 0, width: size.w, height: size.h, transformOrigin: '0 0', transform: `translate(${view.x}px, ${view.y}px) scale(${view.k})` }}>
      {/* SVG segments */}
      <svg className="absolute inset-0 h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none" style={{ zIndex: 8, overflow: 'visible' }}>
        {segments.map(s => {
          const d = segDef(s.type);
          if (d.lens) return (
            <g key={s.id}>
              <path d={lensPath(s)} fill={d.color} opacity={sel === s.id ? 0.95 : 0.75} stroke="#5E9963" strokeWidth="0.35" />
              {!readOnly && <path d={lensPath(s)} fill="transparent" style={{ cursor: 'move', pointerEvents: 'fill' }}
                onPointerDown={e => startDrag(s.id, 'move', e)} onClick={e => { e.stopPropagation(); setSel(s.id); }} />}
            </g>
          );
          return (
            <g key={s.id}>
              <line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke={d.color}
                strokeWidth={d.w} vectorEffect="non-scaling-stroke" strokeLinecap={s.type === 'wall' ? 'butt' : 'round'}
                strokeDasharray={d.dash ? '6 5' : undefined} style={{ vectorEffect: 'non-scaling-stroke' }} opacity={sel === s.id ? 1 : 0.92} />
              {!readOnly && <line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="transparent" strokeWidth="14"
                vectorEffect="non-scaling-stroke" style={{ cursor: 'move', pointerEvents: 'stroke' }}
                onPointerDown={e => startDrag(s.id, 'move', e)} onClick={e => { e.stopPropagation(); setSel(s.id); }} />}
            </g>
          );
        })}
      </svg>

      {/* segment arrowheads, labels, endpoint handles (HTML) */}
      {segments.map(s => {
        const d = segDef(s.type); const ang = segAngle(s);
        const mx = (s.x1 + s.x2) / 2, my = (s.y1 + s.y2) / 2;
        return (
          <div key={s.id} style={{ zIndex: sel === s.id ? 29 : 9 }}>
            {d.arrow && (
              <div className="pointer-events-none absolute" style={{ left: s.x2 + '%', top: s.y2 + '%',
                transform: `translate(-50%,-50%) rotate(${ang}deg)`, width: 0, height: 0,
                borderLeft: `9px solid ${d.color}`, borderTop: '5px solid transparent', borderBottom: '5px solid transparent' }} />
            )}
            {/* length / label — keep the plan calm: dims on walls + selected, names always */}
            <div className="pointer-events-none absolute -translate-x-1/2 -translate-y-1/2" style={{ left: mx + '%', top: my + '%', zIndex: 30 }}>
              {s.type === 'blackflag'
                ? ((s.label || sel === s.id) && <span className="whitespace-nowrap rounded-md px-1.5 py-[2px] text-[9px] font-700 tracking-wide" style={{ background: 'rgba(9,11,17,.86)', color: '#86C98B', border: '1px solid #86C98B66' }}>{s.label || 'BLACK'}</span>)
                : ((s.type === 'wall' || s.label || sel === s.id) && <span className="whitespace-nowrap rounded bg-black/65 px-1.5 py-0.5 font-mono text-[9.5px] font-600 text-white backdrop-blur-sm">
                    {s.label ? s.label + ' · ' : ''}{lenLabel(segLenFt(s))}
                  </span>)}
            </div>
            {/* endpoints — only on the selected path */}
            {!readOnly && sel === s.id && ['a', 'b'].map(h => (
              <div key={h} onPointerDown={e => startDrag(s.id, h, e)}
                className="absolute h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow"
                style={{ left: (h === 'a' ? s.x1 : s.x2) + '%', top: (h === 'a' ? s.y1 : s.y2) + '%', background: d.color, cursor: 'grab', touchAction: 'none', zIndex: 31 }} />
            ))}
          </div>
        );
      })}

      {/* fixtures */}
      {fixtures.map(it => (
        <FixtureNode key={it.id} it={it} selected={sel === it.id} readOnly={readOnly}
          onSelect={() => setSel(it.id)} onDown={e => startDrag(it.id, 'fix', e)} />
      ))}
      {/* unit labels (free — follow their unit, editable in inspector) */}
      {fixtures.filter(f => f.label).map(f => (
        <div key={'lbl' + f.id} className="pointer-events-none absolute z-[33] whitespace-nowrap rounded-md px-1.5 py-[2px] text-[9.5px] font-700 tracking-wide"
          style={{ left: f.x + '%', top: f.y + '%', transform: 'translate(-50%, 22px)', background: 'rgba(9,11,17,.86)', color: '#E7ECF5', border: `1px solid ${fpDef(f.type).color}66`, boxShadow: '0 1px 5px rgba(0,0,0,.45)' }}>{f.label}</div>
      ))}
      {/* free annotation notes */}
      {items.filter(i => i.kind === 'note').map(n => (
        <div key={n.id} onPointerDown={e => startDrag(n.id, 'fix', e)} onClick={e => { e.stopPropagation(); setSel(n.id); }}
          className={cx('absolute -translate-x-1/2 -translate-y-1/2 select-none whitespace-pre-wrap px-1.5 py-[3px] text-[10px] font-800 leading-tight',
            sel === n.id && 'ring-1 ring-white/70')}
          style={{ left: n.x + '%', top: n.y + '%', zIndex: 32, maxWidth: '32%', borderRadius: 3, touchAction: 'none',
            cursor: readOnly ? 'default' : 'grab',
            background: n.tone === 'red' ? 'transparent' : '#F5D469',
            color: n.tone === 'red' ? '#FF3B30' : '#1A1505' }}>
          {n.text}
        </div>
      ))}
      {/* photo pins */}
      {items.filter(i => i.kind === 'photo').map(ph => (
        <PhotoPin key={ph.id} ph={ph} selected={sel === ph.id} readOnly={readOnly}
          onDown={e => startDrag(ph.id, 'fix', e)} onSelect={() => setSel(ph.id)} onSrc={src => update(ph.id, { src })} />
      ))}
      </div>{/* === end world layer === */}

      {/* zoom + pan controls (screen-space) */}
      <div className="absolute bottom-3 left-1/2 z-40 flex -translate-x-1/2 items-center gap-0.5 rounded-xl border border-line bg-ink-900/90 p-1 shadow-xl backdrop-blur">
        <button onClick={() => zoomBy(1 / 1.25)} title="Zoom out" className="flex h-7 w-7 items-center justify-center rounded-lg text-zinc-300 hover:bg-ink-700 hover:text-white"><Icon name="Minus" size={15} /></button>
        <button onClick={resetView} title="Reset view" className="min-w-[48px] rounded-lg px-2 py-1 font-mono text-[11px] font-600 text-zinc-300 hover:bg-ink-700 hover:text-white">{Math.round(view.k * 100)}%</button>
        <button onClick={() => zoomBy(1.25)} title="Zoom in" className="flex h-7 w-7 items-center justify-center rounded-lg text-zinc-300 hover:bg-ink-700 hover:text-white"><Icon name="Plus" size={15} /></button>
        <span className="mx-0.5 h-5 w-px bg-line" />
        <button onClick={resetView} title="Recenter" className="flex h-7 items-center gap-1 rounded-lg px-2 text-[11px] font-600 text-zinc-300 hover:bg-ink-700 hover:text-white"><Icon name="Locate" size={13} /><span className="hidden sm:inline">Center</span></button>
      </div>

      {!items.length && <div className="pointer-events-none absolute inset-0 flex items-center justify-center text-[12px] text-zinc-600">Pick a tool, then click the floor</div>}
      {!readOnly && <div className="pointer-events-none absolute bottom-3 right-3 z-30 hidden rounded-md border border-line/60 bg-black/45 px-2 py-1 font-mono text-[9.5px] text-zinc-500 backdrop-blur-sm lg:block">drag to pan · scroll / +− to zoom · arrows to move</div>}
    </div>
  );

  /* ---- toolbar button helper ---- */
  const TBtn = ({ active, onClick, icon, label }) => (
    <button onClick={onClick} title={label}
      className={cx('flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[12px] font-600 transition-colors',
        active ? 'border-electric/40 bg-electric/12 text-electric-soft' : 'border-line text-zinc-400 hover:text-zinc-200')}>
      <Icon name={icon} size={14} /><span className="hidden md:inline">{label}</span>
    </button>
  );

  /* ---- top toolbar ---- */
  const toolbar = (
    <div className="flex flex-wrap items-center gap-2 rounded-2xl border border-line bg-ink-900 px-3 py-2">
      <span className="flex items-center gap-2">
        <span className="flex h-7 w-7 items-center justify-center rounded-lg bg-electric/15 text-electric-soft"><Icon name="Lightbulb" size={15} /></span>
        <span className="hidden font-display text-[14px] text-head sm:block">Floor Plan</span>
      </span>
      <span className="mx-1 hidden h-5 w-px bg-line sm:block" />
      <div className="flex items-center gap-1.5">
        <Icon name="MapPin" size={13} className="text-amber" />
        <select value={loc} onChange={e => { setLoc(e.target.value); setSel(null); setTool(null); resetView(); }}
          className="rounded-lg border border-line bg-ink-850 px-2.5 py-1.5 text-[12.5px] font-600 text-zinc-200 outline-none">
          {FP_LOCATIONS.map(l => <option key={l}>{l}</option>)}
        </select>
      </div>
      <div className="hidden items-center gap-2 text-[12px] text-zinc-400 md:flex">
        <Icon name="Ruler" size={13} className="shrink-0" />
        <span className="hidden lg:inline">Width</span>
        <input type="range" min="10" max="200" step="5" value={scaleFt}
          onChange={e => setScaleFt(Math.max(5, +e.target.value || 5))}
          className="w-24 accent-electric xl:w-36" />
        <span className="w-12 shrink-0 font-mono text-[12px] font-600 text-electric-soft">{lenLabel(scaleFt)}</span>
      </div>
      {!readOnly && (
        <button onClick={() => setSnap(s => !s)}
          className={cx('flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[12px] font-600 transition-colors',
            snap ? 'border-electric/40 bg-electric/10 text-electric-soft' : 'border-line text-zinc-400')}>
          <Icon name="Grid3x3" size={13} /><span className="hidden sm:inline"> Snap</span> {snap ? 'on' : 'off'}
        </button>
      )}
      <div className="ml-auto flex items-center gap-1.5">
        <span className="mr-1 hidden items-center gap-3 text-[11px] text-zinc-500 lg:flex">
          <span className="flex items-center gap-1"><Icon name="Spline" size={12} /> {segments.length}</span>
          <span className="flex items-center gap-1"><Icon name="Boxes" size={12} /> {fixtures.length}</span>
        </span>
        {!readOnly && <TBtn active={toolsOpen} onClick={() => setToolsOpen(o => !o)} icon="PencilRuler" label="Tools" />}
        <TBtn active={equipOpen} onClick={() => setEquipOpen(o => !o)} icon="ListChecks" label="Gear" />
        <TBtn active={big} onClick={() => setBig(b => !b)} icon={big ? 'Minimize2' : 'Maximize2'} label={big ? 'Exit' : 'Full'} />
        <TBtn active={false} onClick={downloadPDF} icon="FileDown" label="PDF" />
        {!readOnly && (
          <button onClick={() => setShared(true)}
            className={cx('flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-700 transition-colors',
              shared ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
            <Icon name={shared ? 'Check' : 'Share2'} size={14} /><span className="hidden sm:inline">{shared ? 'Shared' : 'Share'}</span>
          </button>
        )}
      </div>
    </div>
  );

  /* ---- floating tools palette ---- */
  const paletteEl = (
    <div className="flex h-full flex-col overflow-hidden rounded-2xl border border-line bg-ink-900/95 shadow-2xl backdrop-blur">
      <div className="flex items-center justify-between border-b border-line px-3 py-2.5">
        <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-zinc-300">Tools</div>
        <button onClick={() => setToolsOpen(false)} className="text-zinc-500 hover:text-zinc-200"><Icon name="X" size={15} /></button>
      </div>
      <div className="flex flex-wrap gap-1 border-b border-line p-2">
        {['Build', ...FIXTURE_CATS].map(c => (
          <button key={c} onClick={() => setCat(c)}
            className={cx('rounded-md px-2 py-1 text-[11px] font-600 transition-colors', cat === c ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>{c}</button>
        ))}
      </div>
      <div className="grid min-h-0 flex-1 grid-cols-1 content-start gap-1.5 overflow-y-auto p-2.5">
        {cat === 'Build'
          ? [...SEGMENTS.map(d => (
            <PaletteItem key={d.key} d={{ ...d, icon: d.key === 'wall' ? 'Minus' : d.key === 'cammove' ? 'Video' : d.key === 'blackflag' ? 'Leaf' : 'Footprints' }}
              active={tool === d.key} onClick={() => { const on = tool === d.key; setTool(on ? null : d.key); setToolKind(on ? null : 'segment'); setSel(null); }} />
          )), ...FP_ANNOT.map(d => (
            <PaletteItem key={d.key} d={d} active={tool === d.key}
              onClick={() => { const on = tool === d.key; setTool(on ? null : d.key); setToolKind(on ? null : d.kindTag); setSel(null); }} />
          ))]
          : FIXTURES.filter(f => f.cat === cat).map(d => (
            <PaletteItem key={d.key} d={d} active={tool === d.key}
              onClick={() => { const on = tool === d.key; setTool(on ? null : d.key); setToolKind(on ? null : 'fixture'); setSel(null); }} />
          ))}
      </div>
      <div className={cx('m-2.5 rounded-lg border px-2.5 py-2 text-[11px]', tool ? 'border-electric/40 bg-electric/8 text-electric-soft' : 'border-line bg-ink-900 text-zinc-500')}>
        {tool
          ? <span className="flex items-center gap-1.5"><Icon name="MousePointerClick" size={12} /> Click floor to add{toolKind === 'segment' ? ' · drag endpoints to shape' : ''}</span>
          : <span className="flex items-center gap-1.5"><Icon name="Hand" size={12} /> Pick a tool to place</span>}
      </div>
    </div>
  );

  /* ---- floating inspector ---- */
  const inspectorEl = selItem && !readOnly ? (
    <Panel className="fade-up shrink-0 shadow-2xl">
      {selItem.kind === 'fixture' ? (
        <div>
          <div className="mb-3 flex items-center justify-between">
            <div className="flex items-center gap-2">
              <span className="flex h-7 w-7 items-center justify-center rounded-md bg-ink-950/60"><FixtureSymbol type={selItem.type} sz={22} /></span>
              <span className="text-[13px] font-700 text-head">{fpDef(selItem.type).label}</span>
            </div>
            <button onClick={() => setSel(null)} className="text-zinc-500 hover:text-zinc-300"><Icon name="X" size={15} /></button>
          </div>
          <label className="mb-1 block text-[11px] font-600 uppercase tracking-[0.1em] text-zinc-500">Rotation · {selItem.rot}°</label>
          <input type="range" min="0" max="360" value={selItem.rot} onChange={e => update(selItem.id, { rot: +e.target.value })} className="w-full accent-electric" />
        </div>
      ) : selItem.kind === 'segment' ? (
        <div>
          <div className="mb-3 flex items-center justify-between">
            <div className="flex items-center gap-2">
              <span className="h-3 w-6 rounded-full" style={{ background: segDef(selItem.type).color }} />
              <span className="text-[13px] font-700 text-head">{segDef(selItem.type).label}</span>
            </div>
            <button onClick={() => setSel(null)} className="text-zinc-500 hover:text-zinc-300"><Icon name="X" size={15} /></button>
          </div>
          <div className="flex items-center justify-between rounded-lg border border-line bg-ink-900 px-3 py-2">
            <span className="text-[11px] uppercase tracking-[0.1em] text-zinc-500">Length</span>
            <span className="font-display text-[24px] font-600 leading-none text-head">{lenLabel(segLenFt(selItem))}</span>
          </div>
          <p className="mt-2 text-[11px] text-zinc-500">Drag either endpoint on the plan to resize or re-angle.</p>
        </div>
      ) : selItem.kind === 'note' ? (
        <div>
          <div className="mb-3 flex items-center justify-between">
            <span className="text-[13px] font-700 text-head">Annotation</span>
            <button onClick={() => setSel(null)} className="text-zinc-500 hover:text-zinc-300"><Icon name="X" size={15} /></button>
          </div>
          <textarea value={selItem.text} onChange={e => update(selItem.id, { text: e.target.value })} rows={3}
            className="w-full resize-none rounded-lg border border-line bg-ink-900 p-2.5 text-[12.5px] text-zinc-200 outline-none focus:border-electric/60" />
          <div className="mt-2 flex items-center gap-1.5">
            {[['yellow', 'Label'], ['red', 'Red remark']].map(([t, l]) => (
              <button key={t} onClick={() => update(selItem.id, { tone: t })}
                className={cx('rounded-lg border px-2.5 py-1.5 text-[11px] font-700 transition-colors',
                  selItem.tone === t ? (t === 'red' ? 'border-[#FF4D6D]/50 bg-[#FF4D6D]/10 text-[#FF4D6D]' : 'border-amber/50 bg-amber/10 text-amber') : 'border-line text-zinc-500 hover:text-zinc-300')}>{l}</button>
            ))}
          </div>
        </div>
      ) : selItem.kind === 'photo' ? (
        <div>
          <div className="mb-3 flex items-center justify-between">
            <span className="text-[13px] font-700 text-head">Photo pin</span>
            <button onClick={() => setSel(null)} className="text-zinc-500 hover:text-zinc-300"><Icon name="X" size={15} /></button>
          </div>
          <label className="mb-1 block text-[11px] font-600 uppercase tracking-[0.1em] text-zinc-500">Width · {selItem.w || 18}%</label>
          <input type="range" min="8" max="45" value={selItem.w || 18} onChange={e => update(selItem.id, { w: +e.target.value })} className="w-full accent-electric" />
          {selItem.src && (
            <button onClick={() => update(selItem.id, { src: null })}
              className="mt-2 flex w-full items-center justify-center gap-1.5 rounded-lg border border-line py-2 text-[12px] font-600 text-zinc-300 hover:border-ink-600"><Icon name="ImageOff" size={13} /> Clear image</button>
          )}
          <p className="mt-2 text-[11px] text-zinc-500">Drop any still onto the pin — or click it to browse.</p>
        </div>
      ) : null}
      {(selItem.kind === 'fixture' || selItem.kind === 'segment') && (
        <div className="mt-3">
          <label className="mb-1 block text-[11px] font-600 uppercase tracking-[0.1em] text-zinc-500">Unit label</label>
          <input value={selItem.label || ''} onChange={e => update(selItem.id, { label: e.target.value })}
            placeholder={selItem.kind === 'segment' && selItem.type === 'blackflag' ? 'BLACK' : 'e.g. 300C · 4x4 + Grid'}
            className="w-full rounded-lg border border-line bg-ink-900 px-2.5 py-2 text-[12.5px] text-zinc-200 outline-none focus:border-electric/60" />
        </div>
      )}
      <div className="mt-3 grid grid-cols-2 gap-2">
        <button onClick={() => { const id = 'x' + (nextId.current++); const base = selItem.kind === 'segment' ? { ...selItem, id, x1: selItem.x1 + 4, y1: selItem.y1 + 4, x2: selItem.x2 + 4, y2: selItem.y2 + 4 } : { ...selItem, id, x: selItem.x + 4, y: selItem.y + 4 }; setItems(its => [...its, base]); setSel(id); }}
          className="flex items-center justify-center gap-1.5 rounded-lg border border-line py-2 text-[12px] font-600 text-zinc-300 hover:border-ink-600"><Icon name="Copy" size={13} /> Duplicate</button>
        <button onClick={() => remove(selItem.id)}
          className="flex items-center justify-center gap-1.5 rounded-lg border border-[#FF4D6D]/30 bg-[#FF4D6D]/10 py-2 text-[12px] font-600 text-[#FF4D6D] hover:bg-[#FF4D6D]/15"><Icon name="Trash2" size={13} /> Delete</button>
      </div>
    </Panel>
  ) : null;

  /* ---- floating equipment list ---- */
  const equipEl = (
    <Panel pad={false} className="flex min-h-0 flex-col overflow-hidden shadow-2xl">
      <div className="flex items-center justify-between border-b border-line p-3">
        <div className="flex items-center gap-2"><Icon name="ListChecks" size={14} className="text-zinc-400" /><span className="text-[12.5px] font-700 text-zinc-200">Equipment List</span></div>
        <div className="flex items-center gap-2">
          <span className="font-mono text-[11px] text-zinc-500">{fixtures.length + imported.reduce((a, r) => a + (+r.qty || 1), 0)}</span>
          {!mobile && <button onClick={() => setEquipOpen(false)} className="text-zinc-500 hover:text-zinc-200"><Icon name="X" size={14} /></button>}
        </div>
      </div>
      <div className="min-h-0 flex-1 overflow-y-auto p-2" style={{ maxHeight: mobile ? 230 : undefined }}>
        {byCat.map(g => (
          <div key={g.cat} className="mb-1">
            <div className="px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] text-zinc-600">{g.cat}</div>
            {g.rows.map(r => (
              <div key={r.key} className="flex items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-ink-800">
                <span className="h-2.5 w-2.5 rounded-full" style={{ background: r.color }} />
                <span className="flex-1 text-[12.5px] text-zinc-300">{r.label}</span>
                <span className="font-mono text-[12px] font-600 text-zinc-200">×{r.n}</span>
              </div>
            ))}
          </div>
        ))}
        {imported.length > 0 && (
          <div className="mb-1">
            <div className="flex items-center gap-1.5 px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] text-electric-soft"><Icon name="Download" size={11} /> Imported · Rental</div>
            {imported.map((r, i) => (
              <div key={i} className="flex items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-ink-800">
                <span className="h-2.5 w-2.5 rounded-full bg-electric" />
                <span className="flex-1 truncate text-[12.5px] text-zinc-300">{r.name}<span className="text-zinc-600"> · {r.dept}</span></span>
                <span className="font-mono text-[12px] font-600 text-zinc-200">×{r.qty}</span>
              </div>
            ))}
          </div>
        )}
        {!byCat.length && !imported.length && <div className="p-3 text-[12px] text-zinc-600">No units yet.</div>}
      </div>
      {!readOnly && (
        <div className="border-t border-line p-3">
          <DropImport compact icon="PackagePlus" label="Import rental / equipment list"
            hint="Drop a vendor quote — CSV imports instantly" onRows={onImport} sample={importSample} />
        </div>
      )}
    </Panel>
  );

  /* ---- desktop fullscreen-first editor ---- */
  const editor = (
    <div className="flex flex-col" style={{ height: big ? '100%' : 'max(560px, calc(100vh - 152px))' }}>
      {toolbar}
      <div className="relative mt-3 min-h-0 flex-1">
        {canvas}
        {!readOnly && toolsOpen && (
          <div className="absolute bottom-3 left-3 top-3 z-40 w-[244px]">{paletteEl}</div>
        )}
        {(inspectorEl || equipOpen) && (
          <div className="no-scrollbar absolute bottom-3 right-3 top-3 z-40 flex w-[288px] flex-col gap-3 overflow-y-auto">
            {inspectorEl}
            {equipOpen && equipEl}
          </div>
        )}
      </div>
    </div>
  );

  /* ---- mobile / read-only stacked layout ---- */
  const stacked = (
    <div className="fade-up space-y-3">
      {toolbar}
      <div style={{ height: 320 }}>{canvas}</div>
      {inspectorEl}
      {equipEl}
    </div>
  );

  const content = mobile ? stacked : editor;

  return (
    <>
      {big && !mobile ? (
        <div className="fixed inset-0 z-[70] bg-ink-950 p-4">{editor}</div>
      ) : content}
      {ReactDOM.createPortal(
        <div id="fp-print"><FloorPlanPrint items={items} scaleFt={scaleFt} loc={loc} size={size} byCat={byCat} imported={imported} /></div>,
        document.body
      )}
    </>
  );
}

window.FloorPlanModule = FloorPlanModule;


/* ===== mod_purchase ===== */
/* ============================================================
   PURCHASING — Art dept pre-buy list, shared live with producer
   Per-set items with prices · running cost vs budget
   ============================================================ */
const PSTATUS = {
  suggested: { label:'Suggested', tone:'amber',   icon:'Lightbulb' },
  approved:  { label:'Approved',  tone:'electric', icon:'CircleCheck' },
  purchased: { label:'Purchased', tone:'mint',     icon:'ShoppingCart' },
};
const PSTATUS_ORDER = ['suggested', 'approved', 'purchased'];

function NumEdit({ value, prefix = '', onChange, className = '' }) {
  return (
    <span className={className}>
      {prefix}<Edit value={String(value)} className={className}
        onChange={v => { const n = parseFloat(String(v).replace(/[^0-9.]/g, '')); onChange(isNaN(n) ? 0 : n); }} />
    </span>
  );
}

function PurchaseRow({ it, onCycle, onQty, onPrice, mobile }) {
  const st = PSTATUS[it.status];
  return (
    <div className="flex items-center gap-3 border-t border-line/70 px-3 py-2.5 hover:bg-ink-800">
      <button onClick={onCycle} title="Click to advance status"
        className="shrink-0"><Chip tone={st.tone}><Icon name={st.icon} size={11} />{st.label}</Chip></button>
      <div className="min-w-0 flex-1">
        <div className="truncate text-[13px] font-600 text-zinc-100">{it.name}</div>
        <div className="flex items-center gap-1.5 text-[11px] text-zinc-500">
          <span>{it.cat}</span><span className="text-zinc-700">·</span><span>{it.vendor}</span>
        </div>
      </div>
      <div className="shrink-0 text-right">
        <div className="font-mono text-[11px] text-zinc-500">
          <NumEdit value={it.qty} onChange={onQty} className="text-zinc-300" /> × <NumEdit value={it.price} prefix="$" onChange={onPrice} className="text-zinc-300" />
        </div>
        <div className="font-display text-[14px] font-700 text-head">{money(it.qty * it.price)}</div>
      </div>
    </div>
  );
}

/* persisted per project — the art dept's list + its own budget. Seeded from the
   demo sets on Crimson, empty on a real (blank) production. */
const PURCHASE_KEY = 'silverlines.purchase.' + (window.PROJ_ID || 'crimson') + '.v1';
function purchaseLoad() {
  const s = window.SLStore.get(PURCHASE_KEY);
  if (s && Array.isArray(s.sets)) return { sets: s.sets, artBudget: +s.artBudget || ART_BUDGET };
  return { sets: window.PROJ_BLANK ? [] : PURCHASE_SETS, artBudget: ART_BUDGET };
}
/* live rollup consumed by the Budget module + owner P&L. committed = purchased +
   approved (suggested is excluded — it isn't money the producer has agreed to). */
window.purchaseRollup = function () {
  try {
    const s = window.SLStore.get(PURCHASE_KEY);
    const sets = (s && Array.isArray(s.sets)) ? s.sets : (window.PROJ_BLANK ? [] : PURCHASE_SETS);
    const all = sets.flatMap(set => set.items || []);
    const sum = st => all.filter(i => i.status === st).reduce((a, i) => a + (+i.qty || 0) * (+i.price || 0), 0);
    const purchased = sum('purchased'), approved = sum('approved');
    return { purchased, approved, committed: purchased + approved, suggested: sum('suggested'), count: all.length };
  } catch (e) { return { purchased: 0, approved: 0, committed: 0, suggested: 0, count: 0 }; }
};

function PurchaseModule({ mobile, readOnly = false }) {
  const [store, setStoreState] = useState(purchaseLoad);
  const persist = next => setStoreState(prev => {
    const v = typeof next === 'function' ? next(prev) : next;
    window.SLStore.set(PURCHASE_KEY, v);
    return v;
  });
  const sets = store.sets;
  const artBudget = store.artBudget;
  const setSets = fn => persist(prev => ({ ...prev, sets: typeof fn === 'function' ? fn(prev.sets) : fn }));
  const setArtBudget = v => persist(prev => ({ ...prev, artBudget: Math.max(0, +v || 0) }));
  const curSymP = (window.CURRENCIES && window.CURRENCIES[window.curCode && window.curCode()] || {}).sym || '$';
  const [shared, setShared] = useState(true);
  const [adding, setAdding] = useState(null); // set index
  const [draft, setDraft] = useState({ name:'', qty:1, price:0 });

  const all = sets.flatMap(s => s.items);
  const sum = (st) => all.filter(i => i.status === st).reduce((a, i) => a + i.qty * i.price, 0);
  const purchased = sum('purchased'), approved = sum('approved'), suggested = sum('suggested');
  const committed = purchased + approved;
  const remaining = artBudget - committed;
  const over = committed > artBudget;
  const pct = v => artBudget > 0 ? Math.min(100, (v / artBudget) * 100) : 0;

  const cycle = (si, id) => setSets(s => s.map((set, i) => i !== si ? set : {
    ...set, items: set.items.map(it => it.id !== id ? it : { ...it, status: PSTATUS_ORDER[(PSTATUS_ORDER.indexOf(it.status) + 1) % 3] })
  }));
  const patch = (si, id, p) => setSets(s => s.map((set, i) => i !== si ? set : {
    ...set, items: set.items.map(it => it.id !== id ? it : { ...it, ...p })
  }));
  const addItem = si => {
    if (!draft.name.trim()) { setAdding(null); return; }
    setSets(s => s.map((set, i) => i !== si ? set : {
      ...set, items: [...set.items, { id: 'n' + Date.now(), name: draft.name, cat: 'Props', qty: +draft.qty || 1, price: +draft.price || 0, status: 'suggested', vendor: 'TBD' }]
    }));
    setDraft({ name:'', qty:1, price:0 }); setAdding(null);
  };

  return (
    <div className="fade-up">
      {!readOnly && <SectionTitle kicker="Art Dept · Production Design" icon="ReceiptText" title="Purchasing & Pre-Buy"
        action={<button onClick={() => setShared(true)}
          className={cx('flex items-center gap-2 rounded-xl px-3.5 py-2.5 text-[12.5px] font-700 transition-colors',
            shared ? 'bg-mint/15 text-mint' : 'bg-cta text-cta-ink hover:bg-cta-hover')}>
          <Icon name={shared ? 'Check' : 'Share2'} size={15} />{shared ? 'Shared with Producer' : 'Share with Producer'}
        </button>} />}

      {/* budget header */}
      <Panel className="mb-5">
        <div className="flex flex-wrap items-end justify-between gap-3">
          <div>
            <div className="text-[11px] uppercase tracking-[0.12em] text-zinc-500">Art Department Budget</div>
            {readOnly
              ? <div className="font-display text-[32px] font-700 leading-none text-head">{money(artBudget)}</div>
              : <div className="font-display text-[32px] font-700 leading-none text-head"><NumEdit value={artBudget} prefix={curSymP} onChange={setArtBudget} className="text-head" /></div>}
          </div>
          <div className="text-right">
            <div className="text-[11px] uppercase tracking-[0.12em] text-zinc-500">{over ? 'Over budget' : 'Remaining'}</div>
            <div className={cx('font-display text-[32px] font-700 leading-none', over ? 'text-[#FF4D6D]' : 'text-mint')}>{money(Math.abs(remaining))}</div>
          </div>
        </div>
        {/* stacked bar */}
        <div className="mt-4 flex h-3 overflow-hidden rounded-full bg-ink-700">
          <div style={{ width: pct(purchased) + '%' }} className="bg-mint transition-all" />
          <div style={{ width: pct(approved) + '%' }} className="bg-electric transition-all" />
          <div style={{ width: pct(suggested) + '%' }} className="scan-anim transition-all" />
        </div>
        <div className="mt-3 grid grid-cols-3 gap-3">
          {[
            { l:'Purchased', v:purchased, c:'#22D48A', d:'Spent & confirmed' },
            { l:'Approved', v:approved, c:'#F57F45', d:'Committed to buy' },
            { l:'Suggested', v:suggested, c:'#FFB020', d:'Awaiting producer' },
          ].map(s => (
            <div key={s.l} className="rounded-xl border border-line bg-ink-900 p-3">
              <div className="flex items-center gap-1.5 text-[11px] font-600 text-zinc-400"><span className="h-2 w-2 rounded-full" style={{ background: s.c }} />{s.l}</div>
              <div className="mt-1 font-display text-[22px] font-700 leading-none text-head">{money(s.v)}</div>
              <div className="text-[10.5px] text-zinc-500">{s.d}</div>
            </div>
          ))}
        </div>
        {suggested > 0 && !readOnly && (
          <div className="mt-3 flex items-center gap-2 rounded-lg border border-amber/30 bg-amber/8 px-3 py-2 text-[12px] text-amber">
            <Icon name="BellRing" size={13} />{money(suggested)} in suggestions waiting for producer approval — visible to them live.
          </div>
        )}
      </Panel>

      {/* per-set lists */}
      <div className="space-y-4">
        {sets.map((set, si) => {
          const subtotal = set.items.reduce((a, i) => a + i.qty * i.price, 0);
          return (
            <Panel key={set.set} pad={false} className="overflow-hidden">
              <div className="flex items-center justify-between gap-3 border-b border-line p-3.5">
                <div className="flex items-center gap-2.5">
                  <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-ink-700 text-zinc-300"><Icon name="Boxes" size={16} /></span>
                  <div>
                    <div className="text-[13.5px] font-700 text-zinc-100">{set.set}</div>
                    <div className="flex items-center gap-1 text-[11px] text-zinc-500"><Icon name="MapPin" size={10} />{set.location}</div>
                  </div>
                </div>
                <div className="text-right">
                  <div className="text-[10px] uppercase tracking-[0.1em] text-zinc-500">Subtotal</div>
                  <div className="font-display text-[22px] font-600 leading-none text-head">{money(subtotal)}</div>
                </div>
              </div>

              <div>
                {set.items.map(it => (
                  <PurchaseRow key={it.id} it={it} mobile={mobile}
                    onCycle={() => !readOnly && cycle(si, it.id)}
                    onQty={v => patch(si, it.id, { qty: v })}
                    onPrice={v => patch(si, it.id, { price: v })} />
                ))}
              </div>

              {!readOnly && (adding === si ? (
                <div className="flex flex-wrap items-center gap-2 border-t border-line bg-ink-900 p-3">
                  <input autoFocus value={draft.name} onChange={e => setDraft(d => ({ ...d, name: e.target.value }))}
                    placeholder="Item to buy…" className="min-w-[160px] flex-1 rounded-lg border border-line bg-ink-850 px-3 py-2 text-[12.5px] text-zinc-200 outline-none focus:border-electric/60" />
                  <input type="number" value={draft.qty} onChange={e => setDraft(d => ({ ...d, qty: e.target.value }))}
                    className="w-16 rounded-lg border border-line bg-ink-850 px-2 py-2 text-[12.5px] text-zinc-200 outline-none" placeholder="Qty" />
                  <input type="number" value={draft.price} onChange={e => setDraft(d => ({ ...d, price: e.target.value }))}
                    className="w-24 rounded-lg border border-line bg-ink-850 px-2 py-2 text-[12.5px] text-zinc-200 outline-none" placeholder="$ each" />
                  <button onClick={() => addItem(si)} className="rounded-lg bg-cta px-3.5 py-2 text-[12.5px] font-700 text-cta-ink hover:bg-cta-hover">Add</button>
                  <button onClick={() => setAdding(null)} className="rounded-lg px-2 py-2 text-[12.5px] font-600 text-zinc-400">Cancel</button>
                </div>
              ) : (
                <button onClick={() => setAdding(si)} className="flex w-full items-center justify-center gap-2 border-t border-line py-2.5 text-[12.5px] font-600 text-zinc-500 hover:bg-ink-800 hover:text-electric-soft">
                  <Icon name="Plus" size={14} /> Suggest an item for this set
                </button>
              ))}
            </Panel>
          );
        })}
      </div>
    </div>
  );
}

window.PurchaseModule = PurchaseModule;


/* ===== app ===== */
/* ============================================================
   SETTINGS — profile, appearance, notifications, defaults
   ============================================================ */
const __ACCT0 = (window.Auth && window.Auth.current()) || {};
const SETTINGS_DEFAULTS = {
  name: __ACCT0.name || 'Producer', role: __ACCT0.role || 'Producer', email: __ACCT0.email || '', phone: '',
  notifCallsheet: true, notifUploads: true, notifPurchase: true, notifComments: false,
  units: 'ft', clock: '24h', expiry: '7d', pwDefault: true, twoFA: false,
  grain: true, motion: true,
};
function useSettings() {
  const [s, setS] = useState(() => {
    try { return { ...SETTINGS_DEFAULTS, ...(window.SLStore.get('silverlines.settings.v1', {})) }; }
    catch (e) { return { ...SETTINGS_DEFAULTS }; }
  });
  const set = p => setS(o => {
    const n = { ...o, ...p };
    window.SLStore.set('silverlines.settings.v1', n);
    return n;
  });
  return [s, set];
}

/* floor-plan length respecting the units setting */
const lenLabel = ft => {
  try { if ((window.SLStore.get('silverlines.settings.v1', {})).units === 'm') return (ft * 0.3048).toFixed(1) + 'm'; } catch (e) {}
  return ft.toFixed(1) + 'ft';
};

function Seg({ value, options, onChange }) {
  return (
    <div className="flex shrink-0 items-center rounded-lg border border-line bg-ink-900 p-0.5" role="radiogroup">
      {options.map(([v, l]) => (
        <button key={v} role="radio" aria-checked={value === v ? 'true' : 'false'} onClick={() => onChange(v)}
          className={cx('rounded-[7px] px-2.5 py-1.5 font-display text-[11.5px] transition-colors',
            value === v ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>{l}</button>
      ))}
    </div>
  );
}

function SettingRow({ icon, label, sub, children }) {
  return (
    <div className="flex items-center gap-3 px-1 py-3">
      <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-zinc-400"><Icon name={icon} size={16} /></span>
      <div className="min-w-0 flex-1">
        <div className="text-[13.5px] font-600 text-zinc-100">{label}</div>
        {sub && <div className="text-[11.5px] text-zinc-500">{sub}</div>}
      </div>
      {children}
    </div>
  );
}

function SettingsModule({ mobile }) {
  /* account-wide identity, appearance, notifications & base defaults now live in
     the Account overlay (window.openAccount); this screen is production-scoped. */
  const [owned] = useOwnedProjects();               // subscribes so the panel re-renders on rename
  const curPid = window.PROJ_ID;
  const curProd = owned.find(p => p.pid === curPid);
  const isDemoProd = curPid === 'crimson';
  const [, force] = useState(0);                     // re-render after a per-production edit (shoot date)
  return (
    <div className="fade-up">
      <SectionTitle kicker="This production" icon="Settings" title="Settings" />
      <div className={cx('grid items-start gap-4', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
        {/* account & preferences — identity, appearance, notifications & base defaults live here */}
        <Panel className={mobile ? '' : 'col-span-2'}>
          <div className="flex items-center gap-3">
            <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-electric/12 text-electric-soft"><Icon name="UserCog" size={18} /></span>
            <div className="min-w-0 flex-1">
              <h3 className="font-display text-[15px] text-head">Account &amp; preferences</h3>
              <p className="text-[12px] text-zinc-500">These preferences apply to your whole account.</p>
            </div>
            <button onClick={() => window.openAccount && window.openAccount()}
              className="flex shrink-0 items-center gap-2 rounded-xl border border-line bg-ink-900 px-3.5 py-2.5 text-[12.5px] font-600 text-zinc-200 transition-colors hover:border-electric/50 hover:text-electric-soft">
              <Icon name="Settings" size={14} /> Open account settings</button>
          </div>
        </Panel>

        {/* this production — currency & schedule overrides (defaults come from your account) */}
        <Panel>
          <h3 className="mb-1 font-display text-[15px] text-head">Schedule &amp; currency</h3>
          <SettingRow icon="CircleDollarSign" label="Currency" sub="This production — defaults to your account currency">
            <select value={curCode()}
              onChange={e => { setCurrency(e.target.value); window.toast(`Currency set to ${e.target.value}`, 'CircleDollarSign'); }}
              className="shrink-0 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 font-display text-[12px] text-zinc-200 outline-none focus:border-electric/60">
              {Object.keys(CURRENCIES).map(k => <option key={k} value={k}>{k} · {CURRENCIES[k].sym.trim()}</option>)}
            </select>
          </SettingRow>
          <SettingRow icon="CalendarDays" label="Shoot start date" sub="Day 1 — drives call-sheet dates & sun times">
            <input type="date" value={isoOf(shootStart())}
              onChange={e => { if (!e.target.value) return; const b = window.SLStore.get('silverlines.brief.v1', {}) || {}; b.startDate = e.target.value; window.SLStore.set('silverlines.brief.v1', b); force(n => n + 1); window.toast('Shoot start date updated', 'CalendarDays'); }}
              className="shrink-0 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 font-display text-[12px] text-zinc-200 outline-none focus:border-electric/60" />
          </SettingRow>
        </Panel>

        {/* production */}
        <Panel>
          <h3 className="mb-1 font-display text-[15px] text-head">Production</h3>
          <SettingRow icon="Clapperboard" label="Name" sub={isDemoProd ? 'The shared demo can’t be renamed' : 'Shown across the app and on exports'}>
            {isDemoProd
              ? <span className="text-[12.5px] text-zinc-500">Crimson Skies</span>
              : <div className="min-w-[150px] text-right text-[13px] font-600 text-zinc-100">
                  <Edit value={(curProd && curProd.name) || PROJECT.name} align="right" className="text-[13px] font-600"
                    onChange={v => window.slRenameProduction && window.slRenameProduction(curPid, v)} />
                </div>}
          </SettingRow>
          <SettingRow icon="Hash" label="Code" sub="Auto — from the production name">
            <span className="font-mono text-[12px] text-zinc-400">{PROJECT.code}</span>
          </SettingRow>
          <SettingRow icon="Activity" label="Status" sub="Where this production sits in its lifecycle">
            <span className="text-[12.5px] text-zinc-300">{PROJECT.status}</span>
          </SettingRow>
          {isDemoProd && <div className="mt-1 rounded-lg border border-amber/25 bg-amber/8 px-3 py-2 text-[11.5px] leading-relaxed text-amber">This is the shared demo — create your own production from the project menu to rename and edit it freely.</div>}
        </Panel>

        {/* production data */}
        <Panel className={mobile ? '' : 'col-span-2'}>
          <h3 className="mb-1 font-display text-[15px] text-head">Production data</h3>
          <p className="mb-1 px-1 text-[11.5px] leading-relaxed text-zinc-500">Sign-in security, devices &amp; share-link defaults live in your account settings.</p>
          <div className="mt-2 flex flex-wrap items-center gap-2">
            <button onClick={() => window.toast('Project data export queued — we will email you the archive', 'FileDown')}
              className="flex items-center gap-2 rounded-xl border border-line px-3.5 py-2.5 text-[12.5px] font-600 text-zinc-300 transition-colors hover:border-ink-600">
              <Icon name="FileDown" size={14} /> Export project data</button>
            <button onClick={() => window.toast('Archiving is locked while the production is active', 'Archive')}
              className="flex items-center gap-2 rounded-xl border border-[#FF4D6D]/30 bg-[#FF4D6D]/10 px-3.5 py-2.5 text-[12.5px] font-600 text-[#FF4D6D] transition-colors hover:bg-[#FF4D6D]/15">
              <Icon name="Archive" size={14} /> Archive production</button>
          </div>
        </Panel>
      </div>
    </div>
  );
}
window.SettingsModule = SettingsModule;

/* ============================================================
   STRIPBOARD — scene scheduling board
   Real shooting-order board built on the live storyboard scenes.
   Drag scene strips between shoot days; per-day banners roll up
   pages, screen time and cast. Day assignment persists to the
   shared storyboard (sched.day), so Pre-Pro + Call Sheets stay in
   sync. Classic stripboard colour key by INT/EXT + time of day.
   ============================================================ */
function stripEighths(pages) {
  if (!pages) return 0;
  const m = /(?:(\d+)\s+)?(?:(\d+)\/(\d+))?/.exec(String(pages).trim());
  const whole = m && m[1] ? +m[1] : (/^\d+$/.test(String(pages).trim()) ? +pages : 0);
  const frac = m && m[2] && m[3] ? (+m[2] / +m[3]) : 0;
  return whole + frac;
}
const fmtEighths = e => {
  if (!e) return '0';
  let whole = Math.floor(e);
  let eighths = Math.round((e - whole) * 8);
  if (eighths === 8) { whole += 1; eighths = 0; } // carry: 8/8 is a full page
  return (whole ? whole : '') + (whole && eighths ? ' ' : '') + (eighths ? eighths + '/8' : (whole ? '' : '0'));
};
function stripMin(dur) {
  if (!dur) return 0;
  const h = /(\d+)\s*h/.exec(dur); const m = /(\d+)\s*m/.exec(dur);
  return (h ? +h[1] * 60 : 0) + (m ? +m[1] : 0);
}
const fmtMin = t => { const h = Math.floor(t / 60), m = t % 60; return (h ? h + 'h ' : '') + (m ? m + 'm' : (h ? '' : '0m')); };
const stripIntExt = loc => /backlot|runway|tarmac|exterior|\bext\b|outdoor|street|field/i.test(loc || '') ? 'EXT' : 'INT';
/* stripboard colour key — a managed LIST of keys (label + colour) the user can
   add to, rename, recolour and delete. Each strip resolves to a key either by
   manual assignment or, failing that, an auto guess from INT/EXT × day/night.
   Persisted per project. */
const STRIPKEY_KEY = 'silverlines.stripkey.' + (window.PROJ_ID || 'crimson') + '.v1';
const STRIPASSIGN_KEY = 'silverlines.stripassign.' + (window.PROJ_ID || 'crimson') + '.v1';
const STRIPKEY_DEFAULT = [
  { id:'intDay',   label:'INT · Day',   color:'#C9A24B' },
  { id:'extDay',   label:'EXT · Day',   color:'#E0B341' },
  { id:'intNight', label:'INT · Night', color:'#5E7CE0' },
  { id:'extNight', label:'EXT · Night', color:'#3FA46A' },
];
function stripKeyLoad() {
  try {
    const s = window.SLStore.get(STRIPKEY_KEY, null);
    if (Array.isArray(s) && s.length) return s;
    if (s && typeof s === 'object') return STRIPKEY_DEFAULT.map(k => ({ ...k, ...(s[k.id] || {}) })); // migrate old object form
  } catch (e) {}
  return STRIPKEY_DEFAULT.map(k => ({ ...k }));
}
const stripAssignLoad = () => window.SLStore.get(STRIPASSIGN_KEY, {});
function hexA(hex, a) {
  const h = String(hex || '#888').replace('#', '');
  const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
  const n = parseInt(f, 16) || 0;
  return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
}
function autoKeyId(sc, keys) {
  const ie = stripIntExt(sc.loc);
  const night = /night|dusk|magic|evening/i.test(sc.tod || '');
  const guess = ie === 'INT' ? (night ? 'intNight' : 'intDay') : (night ? 'extNight' : 'extDay');
  return (keys.find(k => k.id === guess) || keys[0] || STRIPKEY_DEFAULT[0]).id;
}
function resolveKey(sc, keys, assign) {
  const aid = assign[sc.id];
  return (aid && keys.find(k => k.id === aid)) || keys.find(k => k.id === autoKeyId(sc, keys)) || keys[0] || STRIPKEY_DEFAULT[0];
}
function stripTone(sc, keys, assign) {
  const k = resolveKey(sc, keys, assign);
  return { bar: k.color, tint: hexA(k.color, 0.13), key: k.label, auto: !assign[sc.id] };
}
const DAY_PAGE_CAP = 6;   // soft cap of script pages per shoot day
const DAY_MIN_CAP = 600;  // soft cap of screen-minutes per shoot day (10h)

/* small popover to assign a key (or revert to Auto) to one strip */
function KeyPicker({ keys, currentId, isAuto, onPick, onAuto }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [open]);
  const cur = keys.find(k => k.id === currentId);
  return (
    <span ref={ref} className="relative inline-block">
      <button onClick={e => { e.stopPropagation(); setOpen(o => !o); }} title="Change key"
        className="inline-flex items-center gap-1 rounded-md border border-line bg-ink-850 px-1.5 py-[1px] font-mono text-[9px] font-700 tracking-wide text-zinc-300 hover:border-ink-600">
        <span className="h-2 w-2 rounded-[3px]" style={{ background: cur ? cur.color : '#888' }} />
        {cur ? cur.label : '—'}{isAuto ? <span className="text-zinc-600">·auto</span> : null}
      </button>
      {open && (
        <div className="absolute left-0 z-30 mt-1 w-44 rounded-xl border border-line bg-ink-850 p-1.5 shadow-xl shadow-black/40">
          <button onClick={() => { onAuto(); setOpen(false); }}
            className={cx('flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] hover:bg-ink-800', isAuto ? 'font-700 text-electric-soft' : 'text-zinc-400')}>
            <Icon name="Wand2" size={12} /> Auto
          </button>
          {keys.map(k => (
            <button key={k.id} onClick={() => { onPick(k.id); setOpen(false); }}
              className={cx('flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] hover:bg-ink-800', !isAuto && k.id === currentId ? 'font-700 text-zinc-100' : 'text-zinc-300')}>
              <span className="h-3 w-3 rounded-[3px]" style={{ background: k.color }} />{k.label}
            </button>
          ))}
        </div>
      )}
    </span>
  );
}

function Strip({ sc, onDrag, onDay, mobile, keys, assign, onAssign }) {
  const tone = stripTone(sc, keys, assign);
  const curId = resolveKey(sc, keys, assign).id;
  const cast = (sc.cast || []).length;
  return (
    <div draggable={!mobile} onDragStart={e => onDrag(e, sc)}
      className={cx('group/st flex items-stretch overflow-hidden rounded-lg border border-line transition-colors hover:border-ink-600',
        mobile ? '' : 'cursor-grab active:cursor-grabbing')}
      style={{ background: tone.tint }}>
      <div className="w-1.5 shrink-0" style={{ background: tone.bar }} />
      <div className="min-w-0 flex-1 px-3 py-2">
        <div className="flex items-center gap-2">
          <span className="font-mono text-[12px] font-700 text-head">{sc.no}</span>
          <KeyPicker keys={keys} currentId={curId} isAuto={tone.auto}
            onPick={id => onAssign(sc.id, id)} onAuto={() => onAssign(sc.id, null)} />
          <span className="truncate text-[12.5px] font-600 text-zinc-100">{sc.title}</span>
          <span className="ml-auto shrink-0"><SpotBadge spotId={sceneSpot(sc)} /></span>
        </div>
        <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10.5px] text-zinc-500">
          <span className="inline-flex items-center gap-1"><Icon name="MapPin" size={10} />{sc.loc}</span>
          <span className="inline-flex items-center gap-1"><Icon name="FileText" size={10} />{fmtEighths(stripEighths(sc.pages))} pg</span>
          <span className="inline-flex items-center gap-1"><Icon name="Clock" size={10} />{sc.dur}</span>
          {cast > 0 && <span className="inline-flex items-center gap-1"><Icon name="Users" size={10} />{cast}</span>}
          {openAsks(sc) > 0 && <span className="inline-flex items-center gap-1 text-amber" title="Open client/photographer requests"><Icon name="MessageSquareDot" size={10} />{openAsks(sc)} ask{openAsks(sc) === 1 ? '' : 's'}</span>}
        </div>
      </div>
      {mobile && (
        <div className="flex flex-col border-l border-line">
          <button onClick={() => onDay(sc, -1)} className="flex flex-1 items-center justify-center px-2 text-zinc-500 hover:text-electric-soft"><Icon name="ChevronUp" size={14} /></button>
          <button onClick={() => onDay(sc, 1)} className="flex flex-1 items-center justify-center border-t border-line px-2 text-zinc-500 hover:text-electric-soft"><Icon name="ChevronDown" size={14} /></button>
        </div>
      )}
    </div>
  );
}

function NewDayDrop({ day, onDrop, mobile }) {
  const [over, setOver] = useState(false);
  if (mobile) return null;
  return (
    <div
      onDragOver={e => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={e => { setOver(false); onDrop(e, day); }}
      className={cx('flex min-h-[140px] w-full flex-col items-center justify-center gap-1.5 rounded-2xl border-2 border-dashed text-center transition-colors',
        over ? 'border-electric/70 bg-electric/10 text-electric-soft' : 'border-ink-600/70 text-zinc-600 hover:border-ink-500 hover:text-zinc-400')}>
      <Icon name="CalendarPlus" size={18} />
      <div className="font-display text-[13px] font-700">Day {day}</div>
      <div className="px-3 text-[10.5px] leading-snug">Drop a strip here to<br />start a new shoot day</div>
    </div>
  );
}

function DayColumn({ day, label, date, scenes, onDrop, onDrag, onDay, mobile, keys, assign, onAssign }) {
  const [over, setOver] = useState(false);
  const pages = scenes.reduce((a, s) => a + stripEighths(s.pages), 0);
  const mins = scenes.reduce((a, s) => a + stripMin(s.dur), 0);
  const cast = new Set(scenes.flatMap(s => s.cast || [])).size;
  const heavy = day > 0 && (pages > DAY_PAGE_CAP || mins > DAY_MIN_CAP);
  return (
    <div
      onDragOver={e => { if (!mobile) { e.preventDefault(); setOver(true); } }}
      onDragLeave={() => setOver(false)}
      onDrop={e => { setOver(false); onDrop(e, day); }}
      className={cx('flex w-full flex-col rounded-2xl border bg-ink-850 transition-colors',
        over ? 'border-electric/60 ring-1 ring-electric/40' : 'border-line')}>
      {/* day banner */}
      <div className={cx('rounded-t-2xl border-b px-3.5 py-3', day === 0 ? 'border-line bg-ink-900' : heavy ? 'border-[#FF4D6D]/30 bg-[#FF4D6D]/8' : 'border-line bg-ink-900')}>
        <div className="flex items-center justify-between">
          <div className="font-display text-[14px] font-700 text-head">{label}</div>
          <span className="rounded-full border border-line bg-ink-850 px-2 py-0.5 font-mono text-[10px] font-700 text-zinc-400">{scenes.length}</span>
        </div>
        {date && <div className="mt-0.5 text-[11px] text-zinc-500">{date}</div>}
        {day > 0 && (
          <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10.5px]">
            <span className={cx('inline-flex items-center gap-1 font-600', pages > DAY_PAGE_CAP ? 'text-[#FF4D6D]' : 'text-zinc-400')}><Icon name="FileText" size={10} />{fmtEighths(pages)} pg</span>
            <span className={cx('inline-flex items-center gap-1 font-600', mins > DAY_MIN_CAP ? 'text-[#FF4D6D]' : 'text-zinc-400')}><Icon name="Clock" size={10} />{fmtMin(mins)}</span>
            {cast > 0 && <span className="inline-flex items-center gap-1 font-600 text-zinc-400"><Icon name="Users" size={10} />{cast} cast</span>}
          </div>
        )}
        {heavy && <div className="mt-1.5 flex items-center gap-1 text-[10px] font-600 text-[#FF4D6D]"><Icon name="TriangleAlert" size={11} /> Heavy day — consider redistributing</div>}
      </div>
      {/* strips */}
      <div className="flex-1 space-y-2 p-2.5">
        {scenes.length === 0
          ? <div className="rounded-lg border border-dashed border-ink-600 py-6 text-center text-[11px] text-zinc-600">{mobile ? 'No scenes' : 'Drop scenes here'}</div>
          : scenes.map(s => <Strip key={s.id} sc={s} onDrag={onDrag} onDay={onDay} mobile={mobile} keys={keys} assign={assign} onAssign={onAssign} />)}
      </div>
    </div>
  );
}

/* ---------- scheduling conflicts \u2014 same cast member, overlapping call windows, same day ---------- */
function stripParseClock(s) {
  const m = /^\s*(\d{1,2}):(\d{2})/.exec(String(s || ''));
  return m ? (+m[1]) * 60 + (+m[2]) : null;
}
function stripParseDurMin(s) {
  const str = String(s || ''); let mins = 0;
  const h = /(\d+)\s*h/.exec(str); if (h) mins += (+h[1]) * 60;
  const m = /(\d+)\s*m/.exec(str); if (m) mins += (+m[1]);
  return mins || 60;
}
function findScheduleConflicts(scenes) {
  const byDay = {};
  scenes.forEach(s => {
    const day = (s.sched && s.sched.day) || 0;
    if (!day) return;
    (s.cast || []).forEach(actor => {
      byDay[day] = byDay[day] || {};
      (byDay[day][actor] = byDay[day][actor] || []).push(s);
    });
  });
  const conflicts = [];
  Object.keys(byDay).forEach(day => {
    Object.keys(byDay[day]).forEach(actor => {
      const list = byDay[day][actor];
      if (list.length < 2) return;
      const ranges = list.map(s => {
        const start = stripParseClock(s.sched && s.sched.call);
        if (start == null) return null;
        return { s, start, end: start + stripParseDurMin((s.sched && s.sched.est) || s.dur) };
      }).filter(Boolean);
      for (let i = 0; i < ranges.length; i++) for (let j = i + 1; j < ranges.length; j++) {
        const a = ranges[i], b = ranges[j];
        if (a.start < b.end && b.start < a.end) conflicts.push({ day: +day, actor, a: a.s, b: b.s });
      }
    });
  });
  return conflicts;
}
const stripFmtClock = m => `${String(Math.floor(m / 60) % 24).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;

function StripboardModule({ mobile, readOnly = false }) {
  const [scenes, setScenes] = useStoryboard();
  const [activeSpot] = useActiveSpot();
  const view = scenes.filter(s => inActiveSpot(s, activeSpot));
  const briefDays = getShootDays();
  const maxAssigned = scenes.reduce((m, s) => Math.max(m, (s.sched && s.sched.day) || 0), 0);
  const days = Math.max(briefDays, maxAssigned);
  const dragId = useRef(null);
  const [keys, setKeysState] = useState(stripKeyLoad);
  const [assign, setAssignState] = useState(stripAssignLoad);
  const [editKey, setEditKey] = useState(false);
  const setKeys = next => setKeysState(prev => { const v = typeof next === 'function' ? next(prev) : next; window.SLStore.set(STRIPKEY_KEY, v); return v; });
  const setAssign = next => setAssignState(prev => { const v = typeof next === 'function' ? next(prev) : next; window.SLStore.set(STRIPASSIGN_KEY, v); return v; });
  const patchKey = (id, p) => setKeys(ks => ks.map(k => k.id === id ? { ...k, ...p } : k));
  const addKey = () => setKeys(ks => [...ks, { id: 'k' + Date.now(), label: 'New key', color: '#8A8F98' }]);
  const removeKey = id => { setKeys(ks => ks.length > 1 ? ks.filter(k => k.id !== id) : ks); setAssign(a => { const n = { ...a }; Object.keys(n).forEach(sid => { if (n[sid] === id) delete n[sid]; }); return n; }); };
  const assignKey = (sid, kid) => setAssign(a => { const n = { ...a }; if (kid) n[sid] = kid; else delete n[sid]; return n; });
  const resetKey = () => { setKeys(STRIPKEY_DEFAULT.map(k => ({ ...k }))); setAssign({}); };

  const setDay = (id, day) => setScenes(ss => ss.map(s => s.id === id ? { ...s, sched: { ...(s.sched || {}), day } } : s));
  const onDrag = (e, sc) => { dragId.current = sc.id; e.dataTransfer.effectAllowed = 'move'; };
  const onDrop = (e, day) => { e.preventDefault(); if (dragId.current != null) { setDay(dragId.current, day); dragId.current = null; } };
  const onDay = (sc, dir) => { const cur = (sc.sched && sc.sched.day) || 0; const next = Math.max(0, Math.min(days + 1, cur + dir)); setDay(sc.id, next); };

  const byDay = day => view.filter(s => ((s.sched && s.sched.day) || 0) === day);
  const scheduled = view.filter(s => (s.sched && s.sched.day) > 0).length;
  const totalPages = view.reduce((a, s) => a + stripEighths(s.pages), 0);

  const cols = [];
  for (let d = 1; d <= days; d++) cols.push({ day: d, label: 'Day ' + d, date: fmtDayDate(d) });

  return (
    <div className="fade-up">
      {!readOnly && <SectionTitle kicker="Production · Scheduling" icon="CalendarRange" title="Stripboard"
        action={<div className="flex items-center gap-2.5">
          <Chip tone="neutral"><Icon name="Layers" size={12} /> {scheduled}/{view.length} scheduled</Chip>
          <Chip tone="electric"><Icon name="FileText" size={12} /> {fmtEighths(totalPages)} pages</Chip>
        </div>} />}

      {!readOnly && <SpotBar scenes={scenes} />}

      {days > briefDays && (
        <div className="mb-4 flex items-center gap-2 rounded-xl border border-amber/30 bg-amber/8 px-3.5 py-2.5 text-[12px] font-600 text-amber">
          <Icon name="TriangleAlert" size={13} /> Scenes are scheduled across {days} days, but the brief only books {briefDays} — raise the shoot-day count in Development.
        </div>
      )}

      {findScheduleConflicts(view).map((c, i) => {
        const aCall = (c.a.sched && c.a.sched.call) || '?'; const bCall = (c.b.sched && c.b.sched.call) || '?';
        const aEnd = stripParseClock(aCall) != null ? stripFmtClock(stripParseClock(aCall) + stripParseDurMin((c.a.sched && c.a.sched.est) || c.a.dur)) : '?';
        const bEnd = stripParseClock(bCall) != null ? stripFmtClock(stripParseClock(bCall) + stripParseDurMin((c.b.sched && c.b.sched.est) || c.b.dur)) : '?';
        return (
          <div key={i} className="mb-4 flex items-start gap-2 rounded-xl border border-[#FF4D6D]/35 bg-[#FF4D6D]/8 px-3.5 py-2.5 text-[12px] font-600 text-[#FF4D6D]">
            <Icon name="UserX" size={13} className="mt-0.5 shrink-0" />
            <span><b>{c.actor}</b> is double-booked on Day {c.day} — SC {c.a.no} ({aCall}–{aEnd}) overlaps SC {c.b.no} ({bCall}–{bEnd}).</span>
          </div>
        );
      })}

      {/* colour key — add / delete / rename / recolour */}
      <div className="mb-4 rounded-xl border border-line bg-ink-850 px-3.5 py-2.5">
        <div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-[11px] text-zinc-400">
          <span className="font-600 text-zinc-500">Key</span>
          {keys.map(k => (
            <span key={k.id} className="group/k inline-flex items-center gap-1.5">
              {editKey ? (
                <label className="relative inline-flex h-4 w-4 cursor-pointer overflow-hidden rounded ring-1 ring-line" style={{ background: k.color }} title="Change colour">
                  <input type="color" value={k.color} onChange={e => patchKey(k.id, { color: e.target.value })}
                    className="absolute -left-1 -top-1 h-8 w-8 cursor-pointer opacity-0" />
                </label>
              ) : <span className="h-3 w-3 rounded" style={{ background: k.color }} />}
              {editKey
                ? <Edit value={k.label} onChange={v => patchKey(k.id, { label: v })} className="text-[11px] text-zinc-300" />
                : <span>{k.label}</span>}
              {editKey && keys.length > 1 && (
                <button onClick={() => removeKey(k.id)} title="Delete key"
                  className="flex h-4 w-4 items-center justify-center rounded text-zinc-600 hover:text-[#FF4D6D]"><Icon name="X" size={11} /></button>
              )}
            </span>
          ))}
          {editKey && (
            <button onClick={addKey} className="inline-flex items-center gap-1 rounded-lg border border-dashed border-ink-600 px-2 py-0.5 text-[11px] font-600 text-zinc-400 hover:border-electric/50 hover:text-electric-soft">
              <Icon name="Plus" size={11} /> Add key
            </button>
          )}
          {!readOnly && (
            <div className="ml-auto flex items-center gap-2">
              {editKey && <button onClick={resetKey} className="text-[11px] font-600 text-zinc-500 hover:text-zinc-300">Reset</button>}
              <button onClick={() => setEditKey(e => !e)}
                className={cx('inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[11px] font-600 transition-colors',
                  editKey ? 'bg-electric/15 text-electric-soft' : 'text-zinc-500 hover:bg-ink-800 hover:text-zinc-300')}>
                <Icon name={editKey ? 'Check' : 'Pencil'} size={12} />{editKey ? 'Done' : 'Edit key'}
              </button>
            </div>
          )}
        </div>
        {!mobile && !editKey && <div className="mt-1.5 inline-flex items-center gap-1.5 text-[11px] text-zinc-600"><Icon name="MousePointerClick" size={12} /> Drag strips between days · click a strip’s key tag to retag it</div>}
      </div>

      {/* unscheduled bucket */}
      {byDay(0).length > 0 && (
        <div className="mb-4">
          <DayColumn day={0} label="Unscheduled" date={null} scenes={byDay(0)} onDrop={onDrop} onDrag={onDrag} onDay={onDay} mobile={mobile} keys={keys} assign={assign} onAssign={assignKey} />
        </div>
      )}

      {/* day columns */}
      <div className={cx('grid gap-4', mobile ? 'grid-cols-1' : 'grid-cols-2 xl:grid-cols-4')}>
        {cols.map(c => (
          <DayColumn key={c.day} day={c.day} label={c.label} date={c.date} scenes={byDay(c.day)}
            onDrop={onDrop} onDrag={onDrag} onDay={onDay} mobile={mobile} keys={keys} assign={assign} onAssign={assignKey} />
        ))}
        {!readOnly && <NewDayDrop day={days + 1} onDrop={onDrop} mobile={mobile} />}
      </div>
    </div>
  );
}

window.StripboardModule = StripboardModule;

/* ============================================================
   GEAR & PREP — per-department rental manifest + prep/return days
   ONE shared store (project-scoped) read & written by both the
   producer desktop module and the crew phone "My Dept" board, so
   what a 2nd AC checks on set is what the producer sees, live.
     · Prep / Return days toggle on per department (not everyone)
     · Gear = rental manifest + crew-added spares + a "where is it"
       tag (Rental house / Truck / Cart / Set / Case / Returned)
     · Flags when an item is short, over, or unaccounted vs the list
   ============================================================ */
const DEPTLOG_KEY = 'silverlines.deptlog.' + (window.PROJ_ID || 'crimson') + '.v1';
const GEAR_WHERE = ['Rental house', 'Truck', 'Cart', 'Set', 'Case', 'Returned'];

const isoOf = d => { const z = new Date(d.getTime() - d.getTimezoneOffset() * 60000); return z.toISOString().slice(0, 10); };
const prepDefault = () => { const d = shootStart(); d.setDate(d.getDate() - 1); return isoOf(d); };
const returnDefault = () => { const d = shootStart(); d.setDate(d.getDate() + getShootDays()); return isoOf(d); };
const fmtISO = iso => { if (!iso) return '—'; const d = new Date(iso + 'T00:00:00'); return d.toLocaleDateString(slLocale(), { weekday: 'short', month: 'short', day: 'numeric' }); };
const shootRangeText = () => { const s = shootStart(), e = shootStart(); e.setDate(e.getDate() + getShootDays() - 1); const f = d => d.toLocaleDateString(slLocale(), { month: 'short', day: 'numeric' }); return f(s) + ' – ' + f(e); };

const buildGear = (deptId, rows) => rows.map((r, i) => ({ id: deptId + '-' + i, item: r[0], qty: r[1], rentalQty: r[2] == null ? r[1] : r[2], where: r[3] || 'Truck', checked: r[4] !== false, adhoc: !!r[5] }));
function deptlogDefault() {
  const leg = (on, where) => ({ prep: { on, date: prepDefault(), where, note: '' }, ret: { on, date: returnDefault(), where, note: '' } });
  const camera = { ...leg(true, 'Keslow Camera · Burbank'), gear: buildGear('camera', [
    ['ARRI Alexa 35 body', 1, 1, 'Rental house', true], ['Signature Prime set (7 lenses)', 1, 1, 'Rental house', true],
    ['Preston FIZ + motors', 2, 2, 'Cart', true], ['Codex Compact Drive 2TB', 6, 6, 'Cart', true],
    ['Matte box + filter tray set', 1, 1, 'Cart', false], ['V-mount batteries', 7, 8, 'Cart', false],
    ['SDI / BNC cables', 12, 10, 'Truck', true], ['Spare BNC 50ft (grabbed at rental)', 2, 0, 'Truck', true, true],
  ]) };
  const lighting = { ...leg(true, 'Cinelease · Sun Valley'), gear: buildGear('lighting', [
    ['SkyPanel S60-C', 6, 6, 'Truck', true], ['M18 HMI head + ballast', 4, 4, 'Truck', true],
    ['Astera Titan tube', 8, 8, 'Cart', true], ['Distro + spider box', 2, 2, 'Truck', false],
    ['Stinger 25ft', 18, 20, 'Truck', false], ['Generator 24 kW', 1, 1, 'Rental house', true],
  ]) };
  const grip = { ...leg(true, 'Cinelease · Sun Valley'), gear: buildGear('grip', [
    ['Dolly + 12ft track', 1, 1, 'Truck', true], ['C-stand', 12, 12, 'Truck', true],
    ['4×4 flag / net kit', 1, 1, 'Cart', true], ['Combo stand', 6, 6, 'Truck', false], ['Apple box set', 3, 3, 'Truck', true],
  ]) };
  const sound = { prep: { on: false, date: prepDefault(), where: 'Location Sound · N. Hollywood', note: '' }, ret: { on: true, date: returnDefault(), where: 'Location Sound · N. Hollywood', note: '' }, gear: buildGear('sound', [
    ['Sound Devices 833 mixer', 1, 1, 'Cart', true], ['Boom pole + blimp', 2, 2, 'Cart', true],
    ['Lectro lav radio mic', 6, 6, 'Cart', false], ['Timecode box (Tentacle)', 3, 3, 'Cart', true],
  ]) };
  const art = { prep: { on: true, date: prepDefault(), where: 'Art warehouse · Stage 4', note: '' }, ret: { on: false, date: returnDefault(), where: 'Art warehouse · Stage 4', note: '' }, gear: buildGear('art', [
    ['Cockpit instrument props', 1, 1, 'Truck', true], ['Set dressing kit', 1, 1, 'Truck', false], ['Hand props case', 1, 1, 'Cart', true],
  ]) };
  return { camera, lighting, grip, sound, art };
}
function deptlogLoad() { const s = window.SLStore.get(DEPTLOG_KEY); if (s && typeof s === 'object') return s; return deptlogDefault(); }
function useDeptLog() {
  const [data, setData] = useState(deptlogLoad);
  const save = next => setData(prev => { const v = typeof next === 'function' ? next(prev) : next; window.SLStore.set(DEPTLOG_KEY, v); window.dispatchEvent(new Event('sl-deptlog')); return v; });
  useEffect(() => { const h = () => setData(deptlogLoad()); window.addEventListener('sl-deptlog', h); return () => window.removeEventListener('sl-deptlog', h); }, []);
  return [data, save];
}
const EMPTY_ENTRY = { prep: { on: false, date: '', where: '', note: '' }, ret: { on: false, date: '', where: '', note: '' }, gear: [] };
function entryFor(data, id) { const e = (data && data[id]) || {}; return { prep: { ...EMPTY_ENTRY.prep, ...(e.prep || {}) }, ret: { ...EMPTY_ENTRY.ret, ...(e.ret || {}) }, gear: e.gear || [] }; }
function gearStats(gear) {
  const total = gear.length, checked = gear.filter(g => g.checked).length;
  const short = gear.filter(g => !g.adhoc && (+g.qty) < (+g.rentalQty)).length;
  const over = gear.filter(g => !g.adhoc && (+g.qty) > (+g.rentalQty)).length;
  const extras = gear.filter(g => g.adhoc).length;
  const zero = gear.filter(g => (+g.qty) === 0).length;
  return { total, checked, short, over, extras, zero, flags: short + over + zero };
}

function WhereSelect({ value, onChange, disabled }) {
  return (
    <select value={value} disabled={disabled} onChange={e => onChange(e.target.value)} style={{ colorScheme: 'dark' }}
      className="rounded-md border border-line bg-ink-900 px-1.5 py-[3px] font-mono text-[10.5px] text-zinc-300 outline-none focus:border-electric/50 disabled:opacity-60">
      {GEAR_WHERE.map(w => <option key={w} value={w}>{w}</option>)}
    </select>
  );
}

/* shared between producer desktop + crew phone — same store, same edits */
function GearTable({ deptId, data, save, readOnly = false }) {
  const entry = entryFor(data, deptId);
  const gear = entry.gear;
  const stats = gearStats(gear);
  const mut = fn => save(d => { const e = entryFor(d, deptId); return { ...d, [deptId]: { ...e, gear: fn(e.gear) } }; });
  const patch = (id, p) => mut(list => list.map(g => g.id === id ? { ...g, ...p } : g));
  const qtyBy = (id, n) => mut(list => list.map(g => g.id === id ? { ...g, qty: Math.max(0, (+g.qty || 0) + n) } : g));
  const remove = id => {
    const prev = gear; const it = gear.find(g => g.id === id) || {};
    mut(list => list.filter(g => g.id !== id));
    window.pushUndo && window.pushUndo(`Removed "${it.item || 'item'}"`, () => save(d => ({ ...d, [deptId]: { ...entryFor(d, deptId), gear: prev } })));
  };
  const add = () => mut(list => [...list, { id: 'x' + Date.now(), item: 'New item', qty: 1, rentalQty: 0, where: 'Cart', checked: false, adhoc: true }]);

  return (
    <div>
      {stats.flags > 0 && (
        <div className="mb-2.5 flex items-start gap-2 rounded-lg border border-amber/30 bg-amber/8 px-2.5 py-2 text-[11px] font-600 text-amber">
          <Icon name="TriangleAlert" size={13} className="mt-px shrink-0" />
          <span>
            {[stats.short && `${stats.short} short of the rental list`, stats.over && `${stats.over} over`, stats.zero && `${stats.zero} not loaded`].filter(Boolean).join(' · ')} — differs from what was booked.
          </span>
        </div>
      )}
      <div className="space-y-1.5">
        {gear.map(g => {
          const short = !g.adhoc && (+g.qty) < (+g.rentalQty), over = !g.adhoc && (+g.qty) > (+g.rentalQty);
          return (
            <div key={g.id} className="group/g flex items-start gap-2.5 rounded-lg border border-line bg-ink-900 px-2.5 py-2">
              <button onClick={() => !readOnly && patch(g.id, { checked: !g.checked })} disabled={readOnly}
                title={g.checked ? 'Accounted for' : 'Mark as accounted for'}
                className={cx('mt-0.5 flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border transition-colors', g.checked ? 'border-mint bg-mint/20 text-mint' : 'border-ink-600 text-transparent hover:border-zinc-500')}>
                <Icon name="Check" size={11} sw={3.5} />
              </button>
              <div className="min-w-0 flex-1">
                <div className="flex flex-wrap items-center gap-1.5">
                  <Edit value={g.item} onChange={v => patch(g.id, { item: v })} className={cx('text-[12.5px] font-600', g.checked ? 'text-zinc-300' : 'text-zinc-100')} />
                  {g.adhoc && <span className="rounded-full border border-proflow/40 bg-proflow/10 px-1.5 py-px text-[9px] font-700 uppercase tracking-[0.08em] text-proflow-soft">Added</span>}
                </div>
                <div className="mt-1 flex flex-wrap items-center gap-2">
                  <WhereSelect value={g.where} disabled={readOnly} onChange={v => patch(g.id, { where: v })} />
                  {short && <span className="font-mono text-[10px] font-700 text-amber">short {(+g.rentalQty) - (+g.qty)}</span>}
                  {over && <span className="font-mono text-[10px] font-700 text-electric-soft">+{(+g.qty) - (+g.rentalQty)} spare</span>}
                </div>
              </div>
              <div className="shrink-0 text-right">
                <div className="flex items-center gap-1">
                  {!readOnly && <button onClick={() => qtyBy(g.id, -1)} className="flex h-5 w-5 items-center justify-center rounded border border-line text-zinc-400 hover:text-zinc-200">−</button>}
                  <span className={cx('w-5 text-center font-mono text-[13px] font-700', short ? 'text-amber' : over ? 'text-electric-soft' : 'text-head')}>{g.qty}</span>
                  {!readOnly && <button onClick={() => qtyBy(g.id, 1)} className="flex h-5 w-5 items-center justify-center rounded border border-line text-zinc-400 hover:text-zinc-200">+</button>}
                </div>
                <div className="mt-0.5 font-mono text-[9px] text-zinc-600">{g.adhoc ? 'off-list' : 'of ' + g.rentalQty}</div>
              </div>
              {!readOnly && (
                <button onClick={() => remove(g.id)} title="Remove"
                  className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-zinc-700 opacity-0 transition-all hover:bg-[#FF4D6D]/12 hover:text-[#FF4D6D] group-hover/g:opacity-100"><Icon name="X" size={12} /></button>
              )}
            </div>
          );
        })}
        {gear.length === 0 && <div className="rounded-lg border border-dashed border-ink-600 py-5 text-center text-[11.5px] text-zinc-600">No gear listed yet — import the rental manifest or add items.</div>}
      </div>
      {!readOnly && (
        <button onClick={add} className="mt-2 flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-ink-600 py-2 text-[11.5px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
          <Icon name="Plus" size={13} /> Add a spare / off-list item
        </button>
      )}
    </div>
  );
}

/* prep or return leg — toggle + (when on) date & where */
function PrepLeg({ label, icon, leg, accent, onToggle, onSet, readOnly }) {
  return (
    <div className={cx('rounded-xl border p-3 transition-colors', leg.on ? 'border-line bg-ink-900' : 'border-dashed border-ink-700 bg-transparent')}>
      <div className="flex items-center justify-between gap-2">
        <div className="flex items-center gap-1.5 text-[11px] font-700 uppercase tracking-[0.1em]" style={{ color: leg.on ? accent : 'rgb(113 113 122)' }}>
          <Icon name={icon} size={13} />{label}
        </div>
        {!readOnly && (
          <Switch on={leg.on} onChange={onToggle} accent="mint" size="sm" aria-label={label} />
        )}
      </div>
      {leg.on ? (
        <div className="mt-2 space-y-1.5">
          <input type="date" value={leg.date} disabled={readOnly} onChange={e => onSet({ date: e.target.value })} style={{ colorScheme: 'dark' }}
            className="w-full rounded-md border border-line bg-ink-850 px-2 py-1 font-mono text-[12px] text-zinc-200 outline-none focus:border-electric/50 disabled:opacity-70" />
          <div className="flex items-center gap-1.5 text-[11.5px] text-zinc-400">
            <Icon name="MapPin" size={12} className="shrink-0 text-zinc-500" />
            {readOnly ? <span className="truncate">{leg.where || '—'}</span>
              : <Edit value={leg.where} onChange={v => onSet({ where: v })} placeholder="Where — rental house…" className="min-w-0 flex-1 text-[11.5px]" />}
          </div>
        </div>
      ) : <div className="mt-1.5 text-[11px] text-zinc-600">{readOnly ? 'Not scheduled' : 'Off — tap to schedule'}</div>}
    </div>
  );
}

function GearPrepModule({ mobile, readOnly = false }) {
  const [data, save] = useDeptLog();
  const setLeg = (id, which, p) => save(d => { const e = entryFor(d, id); return { ...d, [id]: { ...e, [which]: { ...e[which], ...p } } }; });
  const toggleLeg = (id, which) => save(d => { const e = entryFor(d, id); return { ...d, [id]: { ...e, [which]: { ...e[which], on: !e[which].on, date: e[which].date || (which === 'prep' ? prepDefault() : returnDefault()) } } }; });
  const enable = id => setLeg(id, 'prep', { on: true });

  const active = DEPARTMENTS.filter(dp => { const e = entryFor(data, dp.id); return e.prep.on || e.ret.on || e.gear.length; });
  const inactive = DEPARTMENTS.filter(dp => !active.includes(dp));

  /* cross-department rollup — the producer's "situation at a glance" */
  let prepping = 0, returning = 0, flags = 0, extras = 0, unaccounted = 0, itemsTotal = 0;
  active.forEach(dp => { const e = entryFor(data, dp.id); const s = gearStats(e.gear); if (e.prep.on) prepping++; if (e.ret.on) returning++; flags += s.flags; extras += s.extras; unaccounted += (s.total - s.checked); itemsTotal += s.total; });

  return (
    <div className="fade-up">
      {!readOnly && <SectionTitle kicker="Crew & departments · Logistics" icon="PackageOpen" title="Gear & Prep"
        action={<Chip tone={flags ? 'amber' : 'mint'}><Icon name={flags ? 'TriangleAlert' : 'CircleCheck'} size={12} />{flags ? `${flags} to check` : 'All matches list'}</Chip>} />}

      {/* rollup */}
      <div className={cx('mb-5 grid gap-3', mobile ? 'grid-cols-2' : 'grid-cols-5')}>
        {[
          { ic: 'PackageOpen', l: 'Prepping', v: prepping, tone: '#F57F45' },
          { ic: 'PackageCheck', l: 'Returning', v: returning, tone: '#5FB0E8' },
          { ic: 'ListChecks', l: 'Unaccounted', v: unaccounted, tone: unaccounted ? '#FFB020' : '#22D48A' },
          { ic: 'TriangleAlert', l: 'List flags', v: flags, tone: flags ? '#FF4D6D' : '#22D48A' },
          { ic: 'Plus', l: 'Off-list extras', v: extras, tone: '#9B8BE8' },
        ].map(s => (
          <div key={s.l} className="rounded-xl border border-line bg-ink-900 p-3">
            <div className="flex items-center gap-1.5 text-[10.5px] uppercase tracking-[0.1em] text-zinc-500"><Icon name={s.ic} size={12} />{s.l}</div>
            <div className="mt-1 font-display text-[26px] font-700 leading-none" style={{ color: s.tone }}>{s.v}</div>
          </div>
        ))}
      </div>

      {!readOnly && (
        <div className="mb-4 flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-3.5 py-2.5 text-[12px] text-zinc-500">
          <Icon name="Smartphone" size={13} className="text-electric-soft" />
          <span>Every department's gear list is live on its crew's phone — what a 2nd AC checks at the truck shows up here.</span>
        </div>
      )}

      <div className={cx('grid gap-4', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
        {active.map(dp => {
          const e = entryFor(data, dp.id);
          const s = gearStats(e.gear);
          return (
            <Panel key={dp.id} pad={false} className="overflow-hidden">
              <div className="flex items-center gap-2.5 border-b border-line p-3.5">
                <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-ink-800 text-zinc-300"><Icon name={dp.icon} size={17} /></span>
                <div className="min-w-0 flex-1">
                  <div className="truncate text-[13.5px] font-700 text-head">{dp.name}</div>
                  <div className="truncate text-[11px] text-zinc-500">Head: {dp.head}</div>
                </div>
                <Chip tone={s.checked === s.total && s.total ? 'mint' : 'neutral'}>{s.checked}/{s.total} loaded</Chip>
              </div>
              {!readOnly && (
                <div className="grid grid-cols-2 gap-2.5 border-b border-line p-3.5">
                  <PrepLeg label="Prep day" icon="PackageOpen" accent="#F9A06E" leg={e.prep} readOnly={readOnly}
                    onToggle={() => toggleLeg(dp.id, 'prep')} onSet={p => setLeg(dp.id, 'prep', p)} />
                  <PrepLeg label="Return day" icon="PackageCheck" accent="#5FB0E8" leg={e.ret} readOnly={readOnly}
                    onToggle={() => toggleLeg(dp.id, 'ret')} onSet={p => setLeg(dp.id, 'ret', p)} />
                </div>
              )}
              <div className="p-3.5">
                <div className="mb-2.5 flex items-center gap-2">
                  <Icon name="Boxes" size={14} className="text-zinc-400" />
                  <span className="text-[12px] font-700 text-head">Gear list</span>
                  <span className="font-mono text-[10.5px] text-zinc-500">{s.total} item{s.total === 1 ? '' : 's'}</span>
                </div>
                <GearTable deptId={dp.id} data={data} save={save} readOnly={readOnly} />
              </div>
            </Panel>
          );
        })}
      </div>

      {!readOnly && inactive.length > 0 && (
        <Panel className="mt-4">
          <div className="mb-2.5 flex items-center gap-2 text-[12px] font-600 text-zinc-400">
            <Icon name="Plus" size={13} className="text-zinc-500" /> Add prep / gear tracking for another department
          </div>
          <div className="flex flex-wrap gap-1.5">
            {inactive.map(dp => (
              <button key={dp.id} onClick={() => enable(dp.id)}
                className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[11.5px] font-600 text-zinc-400 transition-colors hover:border-electric/50 hover:text-electric-soft">
                <Icon name={dp.icon} size={13} />{dp.name}
              </button>
            ))}
          </div>
        </Panel>
      )}
    </div>
  );
}
window.GearPrepModule = GearPrepModule;

/* crew-phone section — the department's prep/shoot/return timeline + its gear list */
function CrewGearPrep({ deptId }) {
  const [data, save] = useDeptLog();
  const e = entryFor(data, deptId);
  const s = gearStats(e.gear);
  const legs = [];
  if (e.prep.on) legs.push({ k: 'Prep', icon: 'PackageOpen', color: '#F9A06E', date: fmtISO(e.prep.date), where: e.prep.where });
  legs.push({ k: 'Shoot', icon: 'Clapperboard', color: '#22D48A', date: shootRangeText(), where: 'on set' });
  if (e.ret.on) legs.push({ k: 'Return', icon: 'PackageCheck', color: '#5FB0E8', date: fmtISO(e.ret.date), where: e.ret.where });

  return (
    <div className="space-y-3">
      {/* prep / shoot / return timeline */}
      <div className="rounded-xl border border-line bg-ink-900 p-3.5">
        <div className="mb-2.5 flex items-center gap-1.5 text-[12px] font-700 text-head"><Icon name="CalendarRange" size={14} className="text-electric-soft" />Prep · Shoot · Return</div>
        <div className="space-y-1.5">
          {legs.map(l => (
            <div key={l.k} className="flex items-center gap-2.5 rounded-lg border border-line bg-ink-850 px-2.5 py-2">
              <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg" style={{ background: l.color + '22', color: l.color }}><Icon name={l.icon} size={15} /></span>
              <div className="min-w-0 flex-1">
                <div className="text-[12px] font-700 text-zinc-100">{l.k}</div>
                <div className="truncate text-[10.5px] text-zinc-500">{l.where}</div>
              </div>
              <span className="shrink-0 font-mono text-[11px] font-600 text-zinc-300">{l.date}</span>
            </div>
          ))}
        </div>
      </div>

      {/* gear list — fully editable on the phone */}
      <div className="rounded-xl border border-line bg-ink-900 p-3.5">
        <div className="mb-2.5 flex items-center gap-2">
          <Icon name="Boxes" size={14} className="text-zinc-400" />
          <span className="text-[12px] font-700 text-head">Gear list</span>
          <span className="ml-auto font-mono text-[10.5px] text-zinc-500">{s.checked}/{s.total} loaded</span>
        </div>
        <GearTable deptId={deptId} data={data} save={save} />
      </div>
    </div>
  );
}

/* ============================================================
   APP SHELL — sidebar nav (2 groups), Desktop/Crew-phone toggle
   ============================================================ */
const MODULES = {
  develop: window.DevelopModule,
  spots: window.SpotsModule,
  storage: window.StorageModule,
  script: window.ScriptModule,
  schedule: window.StripboardModule,
  budget: window.BudgetModule,
  postver: window.PostVersionModule,
  dpr: window.DPRModule,
  preprod: window.PreProdModule,
  production: window.ProductionModule,
  post: window.PostModule,
  access: window.AccessModule,
  floorplan: window.FloorPlanModule,
  purchase: window.PurchaseModule,
  linkcapture: window.LinkCaptureModule,
  crewdb: window.CrewDBModule,
  crewlog: window.CrewLogModule,
  gear: window.GearPrepModule,
  casting: window.CastingModule,
  location: window.LocationModule,
  messages: window.MessagesModule,
  settings: SettingsModule,
};
const TOOLS_NAV = [
  { id:'storage', label:'Storage', sub:'Files & transfer', icon:'HardDriveUpload' },
  { id:'budget', label:'Budget', sub:'Line items & topsheet', icon:'Calculator' },
  { id:'linkcapture', label:'Link Capture', sub:'Stills & clips from links', icon:'MonitorPlay' },
];
const ALL_NAV = [...NAV, ...NAV2, ...TOOLS_NAV];
/* Canonical PRODUCTION ORDER. Every menu list renders in this sequence —
   development → shoot → post, then crew, then tools — never in the order the
   producer happened to pick the screens. A menu is a pipeline, not a pile. */
const NAV_ORDER = ALL_NAV.reduce((m, n, i) => { m[n.id] = i; return m; }, {});
const PHASE_IDS = new Set(NAV.map(n => n.id));
const byProductionOrder = (a, b) => (NAV_ORDER[a.id] ?? 999) - (NAV_ORDER[b.id] ?? 999);

function Logo() {
  return (
    <div className="flex items-center gap-2.5">
      <span className="flex h-8 w-8 flex-col items-start justify-center gap-[4px] rounded-[10px] border border-line bg-ink-850 px-[7px]">
        <span className="block h-[2.5px] w-full rounded-full bg-[#EFDD58]"></span>
        <span className="block h-[2.5px] w-[68%] rounded-full bg-[#F43F3E]"></span>
        <span className="block h-[2.5px] w-[40%] rounded-full bg-[#BD2555]"></span>
      </span>
      <div className="leading-none">
        <div className="font-wordmark text-[16px] font-700 text-head">Silverlines</div>
        <div className="mt-0.5 text-[9.5px] font-600 uppercase tracking-[0.22em] text-zinc-500">Production OS</div>
      </div>
    </div>
  );
}

const DemoTag = () => <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-amber/15 px-1.5 py-0.5 text-[8.5px] font-700 uppercase tracking-wide text-amber"><Icon name="FlaskConical" size={9} />Demo</span>;
function ProjectSwitcher() {
  const { goHome } = useContext(AccessContext);
  const [open, setOpen] = useState(false);
  const list = window.slProductions ? window.slProductions() : [];
  const curPid = window.PROJ_ID;
  const isDemo = curPid === 'crimson';
  const cur = list.find(p => p.pid === curPid);
  const curBadge = { code: (cur && cur.code) || (window.PROJ_BLANK ? 'PR' : 'CS'), bg: (cur && cur.bg) || '#5B0952', fg: (cur && cur.fg) || '#F7A8CF' };
  const others = list.filter(p => p.pid !== curPid);
  const switchTo = pid => { setOpen(false); window.openProject && window.openProject(pid); };
  return (
    <div className="relative">
      <button onClick={() => setOpen(o => !o)} className="flex w-full items-center gap-2.5 rounded-xl border border-line bg-ink-900 p-2.5 text-left transition-colors hover:border-ink-600">
        <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg font-display text-[13px] font-700" style={{ background: curBadge.bg, color: curBadge.fg }}>{curBadge.code}</span>
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-1.5"><span className="truncate text-[13px] font-600 text-zinc-100">{PROJECT.name}</span>{isDemo && <DemoTag />}</div>
          <div className="truncate text-[11px] text-zinc-500">{PROJECT.code} · {PROJECT.status}</div>
        </div>
        <Icon name="ChevronsUpDown" size={14} className="shrink-0 text-zinc-500" />
      </button>
      {open && (
        <React.Fragment>
          <div className="fixed inset-0 z-30" onClick={() => setOpen(false)}></div>
          <div className="fade-up absolute left-0 right-0 z-40 mt-2 overflow-hidden rounded-xl border border-line bg-ink-850 shadow-2xl">
            <div className="flex items-center gap-2.5 bg-ink-800 px-3 py-2.5">
              <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg font-display text-[11px] font-700" style={{ background: curBadge.bg, color: curBadge.fg }}>{curBadge.code}</span>
              <div className="min-w-0 flex-1">
                <div className="flex items-center gap-1.5"><span className="truncate text-[12.5px] font-600 text-zinc-100">{PROJECT.name}</span>{isDemo && <DemoTag />}</div>
                <div className="truncate text-[10.5px] text-zinc-500">{PROJECT.code} · {PROJECT.status}</div>
              </div>
              <Icon name="Check" size={14} className="shrink-0 text-mint" />
            </div>
            {others.map(p => (
              <button key={p.pid || p.code} onClick={() => switchTo(p.pid)}
                className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-ink-800">
                <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg font-display text-[11px] font-700" style={{ background: p.bg, color: p.fg }}>{p.code}</span>
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-1.5"><span className="truncate text-[12.5px] font-600 text-zinc-300">{p.name}</span>{p.demo && <DemoTag />}</div>
                  <div className="truncate text-[10.5px] text-zinc-500">{(p.codeTag || (p.code + '-2026'))} · {p.status}</div>
                </div>
                <Icon name="ArrowRight" size={13} className="shrink-0 text-zinc-600" />
              </button>
            ))}
            <button onClick={() => { setOpen(false); goHome && goHome(); }}
              className="flex w-full items-center gap-2 border-t border-line px-3 py-2.5 text-left text-[12px] font-600 text-zinc-400 transition-colors hover:bg-ink-800 hover:text-zinc-200">
              <Icon name="LayoutGrid" size={14} /> All projects
            </button>
            <button onClick={() => { setOpen(false); window.createProduction && window.createProduction(); }}
              className="flex w-full items-center gap-2 border-t border-line px-3 py-2.5 text-left text-[12px] font-600 text-zinc-400 transition-colors hover:bg-ink-800 hover:text-zinc-200">
              <Icon name="Plus" size={14} /> New production
            </button>
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

/* sidebar sub-pages — jump straight to a tab without crossing the page */
const SUBNAV = {
  develop: [
    { id: 'brief', label: 'Concept Brief', icon: 'FileText' },
    { id: 'mood', label: 'Moodboard', icon: 'Images' },
  ],
  preprod: [
    { id: 'story', label: 'Storyboard', icon: 'LayoutGrid' },
    { id: 'depts', label: 'Departments', icon: 'Boxes' },
    { id: 'shots', label: 'Shot List', icon: 'ListChecks' },
    { id: 'breakdown', label: 'Breakdown', icon: 'PackageSearch' },
    { id: 'cater', label: 'Catering & Parking', icon: 'UtensilsCrossed' },
    { id: 'cast', label: 'Cast & Crew', icon: 'Users' },
  ],
  production: [
    { id: 'callsheet', label: 'Call Sheet', icon: 'ClipboardList' },
    { id: 'breakdown', label: 'AD Breakdown', icon: 'ListTree' },
  ],
};

/* one colour family PER SECTION — every card in a section shares its
   section's colour, and the section header wears it too. */
const SECTION_COLORS = {
  tools:      '#BE9A57', // Tools — warm sand
  phases:     '#F57F45', // Production phases — electric orange (brand)
  crew:       '#22D48A', // Crew & departments — mint
};
const NAV_COLORS = {
  storage: SECTION_COLORS.tools, linkcapture: SECTION_COLORS.tools, budget: SECTION_COLORS.tools,
  develop: SECTION_COLORS.phases, preprod: SECTION_COLORS.phases, schedule: SECTION_COLORS.phases, production: SECTION_COLORS.phases, post: SECTION_COLORS.phases,
  script: SECTION_COLORS.phases, postver: SECTION_COLORS.phases, dpr: SECTION_COLORS.phases,
  crewdb: SECTION_COLORS.crew, access: SECTION_COLORS.crew, floorplan: SECTION_COLORS.crew, purchase: SECTION_COLORS.crew, crewlog: SECTION_COLORS.crew, casting: SECTION_COLORS.crew, location: SECTION_COLORS.crew, messages: SECTION_COLORS.crew,
  settings: '#9AA3B2',
};

function NavBtn({ n, idx, on, onClick, rail, pinned, onPin }) {
  const c = NAV_COLORS[n.id] || '#9AA3B2';
  const iconC = `color-mix(in oklab, ${c} 76%, #6B7280)`;
  if (rail) {
    return (
      <button onClick={onClick} title={n.label}
        className={cx('navbtn group relative flex w-full items-center justify-center rounded-xl py-2 transition-colors', !on && 'hover:bg-ink-800')}
        style={on ? { background:`color-mix(in oklab, ${c} 22%, transparent)`, boxShadow:`inset 0 0 0 1px color-mix(in oklab, ${c} 55%, transparent)` } : undefined}>
        <span className="flex h-8 w-8 items-center justify-center rounded-lg" style={{ color: on ? c : iconC }}>
          <Icon name={n.icon} size={17} />
        </span>
      </button>
    );
  }
  return (
    <button onClick={onClick} title={n.sub}
      className={cx('navbtn group flex w-full items-center gap-2.5 rounded-lg px-2 py-[7px] text-left transition-colors', !on && 'hover:bg-ink-800')}
      style={{ '--nc': c, ...(on ? { background:`color-mix(in oklab, ${c} 20%, transparent)`, boxShadow:`inset 0 0 0 1px color-mix(in oklab, ${c} 50%, transparent)` } : {}) }}>
      <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors"
        style={{ background:`color-mix(in oklab, ${c} ${on ? 30 : 13}%, transparent)`, color: on ? c : iconC }}>
        <Icon name={n.icon} size={15} />
      </span>
      <span className="min-w-0 flex-1 truncate font-display text-[12.5px] font-700"
        style={on ? { color: c } : { color: 'rgb(196 203 213)' }}>{n.label}</span>
      <span className="ml-auto flex shrink-0 items-center gap-1">
        {idx != null && <span className="font-mono text-[10px] tabular-nums" style={{ color: on ? `color-mix(in oklab, ${c} 70%, #65697D)` : '#565A6B' }}>{String(idx + 1).padStart(2, '0')}</span>}
        {onPin && (
          <span role="button" tabIndex={0} title={pinned ? 'Remove from favorites' : 'Add to favorites'}
            onClick={e => { e.stopPropagation(); onPin(); }}
            onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onPin(); } }}
            className={cx('flex h-5 w-5 items-center justify-center rounded-md transition-all',
              pinned ? 'text-amber opacity-100' : 'text-zinc-600 opacity-0 group-hover:opacity-100 hover:text-amber')}>
            <Icon name="Star" size={12} style={pinned ? { fill: 'currentColor' } : undefined} />
          </span>
        )}
      </span>
    </button>
  );
}

/* ---- Customize-menu board: pick which screens get pinned to the top ---- */
const NAV_GROUPS = [
  { key: 'tools',  title: 'Tools',               items: () => TOOLS_NAV },
  { key: 'phases', title: 'Production phases',    items: () => NAV },
  { key: 'crew',   title: 'Crew & departments',  items: () => NAV2 },
];
function NavCustomizer({ pins, onToggle, onReset, onClose }) {
  useEffect(() => { const h = e => e.key === 'Escape' && onClose(); document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]);
  return (
    <div className="fixed inset-0 z-[140] flex items-center justify-center p-4" onMouseDown={onClose}>
      <div className="absolute inset-0 bg-black/65 backdrop-blur-sm" />
      <div className="relative flex max-h-[88vh] w-full max-w-[600px] flex-col overflow-hidden rounded-2xl border border-line bg-ink-900 shadow-2xl shadow-black/50" onMouseDown={e => e.stopPropagation()}>
        <div className="flex items-start gap-3 border-b border-line px-5 py-4">
          <span className="mt-0.5 flex h-9 w-9 items-center justify-center rounded-xl bg-amber/14 text-amber"><Icon name="SlidersHorizontal" size={17} /></span>
          <div className="min-w-0 flex-1">
            <div className="font-display text-[16px] font-800 text-head">Add tools</div>
            <div className="text-[12px] text-zinc-500">Tap a screen to add it to your menu — tap again to remove. Everything lives here, ready whenever you need it.</div>
          </div>
          <button onClick={onClose} className="flex h-7 w-7 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-ink-800 hover:text-head"><Icon name="X" size={16} /></button>
        </div>

        <div className="flex-1 overflow-y-auto px-5 py-4">
          {NAV_GROUPS.map(g => {
            const items = g.items();
            const c = SECTION_COLORS[g.key];
            return (
              <div key={g.key} className="mb-5 last:mb-0">
                <div className="mb-2 flex items-center gap-2">
                  <span className="font-display text-[10px] uppercase tracking-[0.16em]" style={{ color: c }}>{g.title}</span>
                  <span className="font-mono text-[10px] text-zinc-600">{items.filter(n => pins.includes(n.id)).length}/{items.length} in menu</span>
                </div>
                <div className="grid grid-cols-2 gap-2">
                  {items.map(n => {
                    const on = pins.includes(n.id);
                    const nc = NAV_COLORS[n.id] || '#9AA3B2';
                    return (
                      <button key={n.id} onClick={() => onToggle(n.id)}
                        className={cx('group flex items-center gap-2.5 rounded-xl border px-2.5 py-2 text-left transition-colors',
                          on ? 'border-amber/45 bg-amber/8' : 'border-line bg-ink-850 hover:border-ink-600')}>
                        <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
                          style={{ background: `color-mix(in oklab, ${nc} 16%, transparent)`, color: nc }}><Icon name={n.icon} size={15} /></span>
                        <span className="min-w-0 flex-1">
                          <span className="block truncate text-[12.5px] font-700 text-zinc-100">{n.label}</span>
                          <span className="block truncate text-[10.5px] text-zinc-500">{n.sub}</span>
                        </span>
                        <Icon name={on ? 'Star' : 'Plus'} size={14} className={on ? 'text-amber' : 'text-zinc-600 group-hover:text-zinc-400'} style={on ? { fill: 'currentColor' } : undefined} />
                      </button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>

        <div className="flex items-center gap-2 border-t border-line px-5 py-3">
          <button onClick={onReset} className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-2 text-[12px] font-600 text-zinc-400 transition-colors hover:text-head"><Icon name="RotateCcw" size={13} /> Reset to default</button>
          <span className="ml-auto font-mono text-[11px] text-zinc-500">{pins.length} in menu</span>
          <button onClick={onClose} className="rounded-xl bg-cta px-4 py-2 text-[12.5px] font-700 text-cta-ink transition-colors hover:bg-cta-hover">Done</button>
        </div>
      </div>
    </div>
  );
}

/* ---- First-run picker: shown when a fresh project is started ---- */
function ProjectStarter({ onConfirm }) {
  const DEF = ['spots', 'schedule', 'production', 'dpr'];
  const [sel, setSel] = useState(() => new Set(DEF));
  const toggle = id => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  return (
    <div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
      <div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
      <div className="relative flex max-h-[90vh] w-full max-w-[640px] flex-col overflow-hidden rounded-2xl border border-line bg-ink-900 shadow-2xl shadow-black/60">
        <div className="border-b border-line px-6 py-5">
          <div className="flex items-center gap-2 text-[11px] font-700 uppercase tracking-[0.18em] text-amber"><Icon name="Sparkles" size={13} /> New production</div>
          <div className="mt-1.5 font-display text-[20px] font-800 text-head">Pick what you need</div>
          <div className="mt-1 max-w-[520px] text-[12.5px] leading-relaxed text-zinc-400">Choose the screens this job actually needs — they land in your <span className="text-amber">Favorites</span> at the top of the menu. Everything else is set up automatically and stays one click away, so you can add it anytime.</div>
        </div>

        <div className="flex-1 overflow-y-auto px-6 py-4">
          {NAV_GROUPS.map(g => {
            const items = g.items();
            const c = SECTION_COLORS[g.key];
            return (
              <div key={g.key} className="mb-5 last:mb-0">
                <div className="mb-2 font-display text-[10px] uppercase tracking-[0.16em]" style={{ color: c }}>{g.title}</div>
                <div className="grid grid-cols-2 gap-2">
                  {items.map(n => {
                    const on = sel.has(n.id);
                    const nc = NAV_COLORS[n.id] || '#9AA3B2';
                    return (
                      <button key={n.id} onClick={() => toggle(n.id)}
                        className={cx('group flex items-center gap-2.5 rounded-xl border px-2.5 py-2 text-left transition-colors',
                          on ? 'border-amber/50 bg-amber/8' : 'border-line bg-ink-850 hover:border-ink-600')}>
                        <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg" style={{ background: `color-mix(in oklab, ${nc} 16%, transparent)`, color: nc }}><Icon name={n.icon} size={15} /></span>
                        <span className="min-w-0 flex-1">
                          <span className="block truncate text-[12.5px] font-700 text-zinc-100">{n.label}</span>
                          <span className="block truncate text-[10.5px] text-zinc-500">{n.sub}</span>
                        </span>
                        <span className={cx('flex h-5 w-5 items-center justify-center rounded-md border', on ? 'border-amber bg-amber text-cta-ink' : 'border-ink-600 text-transparent')}><Icon name="Check" size={12} /></span>
                      </button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>

        <div className="flex items-center gap-2 border-t border-line px-6 py-3.5">
          <button onClick={() => onConfirm(DEF)} className="rounded-lg px-3 py-2 text-[12px] font-600 text-zinc-400 transition-colors hover:text-head">Skip — use defaults</button>
          <span className="ml-auto font-mono text-[11px] text-zinc-500">{sel.size} selected</span>
          <button onClick={() => onConfirm([...sel])} className="inline-flex items-center gap-2 whitespace-nowrap rounded-xl bg-cta px-4 py-2 text-[12.5px] font-700 text-cta-ink transition-colors hover:bg-cta-hover"><Icon name="ArrowRight" size={15} /> Set up menu</button>
        </div>
      </div>
    </div>
  );
}

/* collapsible nav group — header toggles open/closed, count on the right */
function NavSection({ title, color, count, badge, open, onToggle, children }) {
  return (
    <div className="pt-2.5">
      <button onClick={onToggle}
        className="group/sec flex w-full items-center gap-1.5 rounded-md px-2 py-1 transition-colors hover:bg-ink-800/60">
        <Icon name={open ? 'ChevronDown' : 'ChevronRight'} size={12} className="opacity-60" style={{ color }} />
        <span className="font-display text-[10px] uppercase tracking-[0.16em]" style={{ color }}>{title}</span>
        {badge}
        <span className="ml-auto font-mono text-[10px] text-zinc-600">{count}</span>
      </button>
      {open && <div className="mt-1 space-y-0.5">{children}</div>}
    </div>
  );
}

function Sidebar({ module, setModule }) {
  const { access, subtabs, setSubtab } = useContext(AccessContext);
  const acct = window.useAccount ? window.useAccount() : { name: 'Producer', role: 'Producer', color: '#22D48A' };
  const linked = ACCESS.filter(a => access[a.id]).length;

  const sectionOf = id => TOOLS_NAV.some(n => n.id === id) ? 'tools' : NAV2.some(n => n.id === id) ? 'crew' : 'phases';
  const [rail, setRail] = useState(() => { try { const v = localStorage.getItem('silverlines.nav.rail'); return v === null ? true : v === '1'; } catch (e) { return true; } });
  const [open, setOpen] = useState(() => {
    let base = { tools: false, phases: true, crew: false };
    try { const s = JSON.parse(localStorage.getItem('silverlines.nav.sections')); if (s) base = { ...base, ...s }; } catch (e) {}
    return base;
  });
  /* keep the section that holds the current screen expanded */
  useEffect(() => { const s = sectionOf(module); setOpen(o => o[s] ? o : { ...o, [s]: true }); }, [module]);
  const toggle = s => setOpen(o => { const n = { ...o, [s]: !o[s] }; try { localStorage.setItem('silverlines.nav.sections', JSON.stringify(n)); } catch (e) {} return n; });
  const toggleRail = () => setRail(r => { const v = !r; try { localStorage.setItem('silverlines.nav.rail', v ? '1' : '0'); } catch (e) {} return v; });

  /* ---- pinned screens: user-picked favorites that float to the top ---- */
  const DEFAULT_PINS = ['spots', 'schedule', 'production', 'dpr'];
  const lookup = id => ALL_NAV.find(n => n.id === id);
  const [pins, setPins] = useState(() => { try { const s = JSON.parse(localStorage.getItem('silverlines.nav.pinned')); if (Array.isArray(s)) return s; } catch (e) {} return DEFAULT_PINS; });
  const savePins = up => setPins(p => { const v = typeof up === 'function' ? up(p) : up; try { localStorage.setItem('silverlines.nav.pinned', JSON.stringify(v)); } catch (e) {} return v; });
  const isPinned = id => pins.includes(id);
  const togglePin = id => savePins(p => p.includes(id) ? p.filter(x => x !== id) : [...p, id]);
  /* role preview: restricted roles (User/Guest) see exactly the modules they can
     access; Owner/Creator keep their pinned menu. */
  const vis = window.PERMS ? window.PERMS.visibleModules(ALL_NAV.map(n => n.id)) : null;
  const restricted = !!(vis && vis.length < ALL_NAV.length);
  const canSee = id => !vis || vis.indexOf(id) >= 0;
  const pinnedItems = (restricted ? ALL_NAV.filter(n => canSee(n.id)) : pins.map(lookup).filter(Boolean)).sort(byProductionOrder);
  /* number the production-phase screens 01, 02, 03… in pipeline order so the
     menu always reads as an ordered sequence (crew & tools stay unnumbered). */
  let __phaseStep = 0;
  const menuItems = pinnedItems.map(n => ({ n, idx: PHASE_IDS.has(n.id) ? __phaseStep++ : null }));
  const [customizing, setCustomizing] = useState(false);
  /* first-run picker: only on a fresh project that hasn't chosen a menu yet */
  const [firstRun, setFirstRun] = useState(() => { try { return !!window.PROJ_BLANK && localStorage.getItem('silverlines.nav.pinned') === null; } catch (e) { return false; } });
  const finishStarter = list => { savePins(list); setFirstRun(false); };

  const [hover, setHover] = useState(false);
  const foldTo = v => { setRail(v); try { localStorage.setItem('silverlines.nav.rail', v ? '1' : '0'); } catch (e) {} if (v) setHover(false); };
  const renderSub = n => {
    if (!SUBNAV[n.id] || module !== n.id) return null;
    const cur = (subtabs && subtabs[n.id]) || SUBNAV[n.id][0].id;
    const c = NAV_COLORS[n.id] || '#9AA3B2';
    return (
      <div className="fade-up ml-[26px] space-y-0.5 py-1 pl-2" style={{ borderLeft:`1px solid color-mix(in oklab, ${c} 35%, transparent)` }}>
        {SUBNAV[n.id].map(s => {
          const act = cur === s.id;
          return (
            <button key={s.id} onClick={() => setSubtab(n.id, s.id)}
              className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] font-600 transition-colors hover:bg-ink-800"
              style={ act ? { background:`color-mix(in oklab, ${c} 14%, transparent)`, color:c } : { color:'rgb(122 128 144)' } }>
              <Icon name={s.icon} size={12} style={ act ? { color:c } : undefined } />{s.label}
            </button>
          );
        })}
      </div>
    );
  };

  /* ---------- expanded panel — shown when unfolded, or on hover over the rail ---------- */
  const FullPanel = ({ overlay }) => (
    <aside className={cx('flex h-full w-[248px] shrink-0 flex-col overflow-y-auto border-r border-line bg-ink-900/95 p-4',
      overlay ? 'nav-pop shadow-2xl shadow-black/60' : 'bg-ink-900/60')}>
      <div className="flex items-center gap-2 px-1">
        <Logo />
        {overlay ? (
          <button onClick={() => foldTo(false)} title="Keep menu open"
            className="ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-amber/80 transition-colors hover:bg-ink-800 hover:text-amber">
            <Icon name="Pin" size={15} />
          </button>
        ) : (
          <button onClick={() => foldTo(true)} title="Fold menu"
            className="ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-ink-800 hover:text-zinc-300">
            <Icon name="PanelLeftClose" size={16} />
          </button>
        )}
      </div>
      <div className="mt-5"><ProjectSwitcher /></div>

      <nav className="mt-4">
        {pinnedItems.length > 0 ? (
          <>
            <div className="flex items-center gap-1.5 px-2 py-1">
              <Icon name="Star" size={11} className="text-amber" style={{ fill: 'currentColor' }} />
              <span className="font-display text-[10px] uppercase tracking-[0.16em] text-amber">My menu</span>
            </div>
            <div className="mt-1 space-y-0.5">
              {menuItems.map(({ n, idx }) => (
                <React.Fragment key={n.id}>
                  <NavBtn n={n} idx={idx} on={module === n.id} onClick={() => setModule(n.id)} pinned onPin={() => togglePin(n.id)} />
                  {renderSub(n)}
                </React.Fragment>
              ))}
            </div>
          </>
        ) : (
          <div className="px-2 py-3 text-center text-[11.5px] text-zinc-500">No screens yet — add the ones you need below.</div>
        )}

        <button onClick={() => setCustomizing(true)}
          className="mt-2.5 flex w-full items-center gap-2 rounded-xl border border-dashed border-ink-600 px-2.5 py-2.5 text-[12px] font-700 text-zinc-300 transition-colors hover:border-electric/50 hover:text-electric-soft">
          <span className="flex h-5 w-5 items-center justify-center rounded-md bg-electric/14 text-electric-soft"><Icon name="Plus" size={13} /></span>
          Add tools
        </button>
      </nav>

      <div className="mt-auto pt-3">
        <div className="flex items-center gap-2.5 rounded-xl border border-line bg-ink-900 p-2.5">
          <Avatar name={acct.name} color={acct.color} size={32} />
          <div className="min-w-0 flex-1">
            <div className="truncate text-[12.5px] font-600 text-zinc-100">{acct.name}</div>
            <div className="truncate text-[11px] text-zinc-500">{acct.role || 'Producer'} · Owner</div>
          </div>
          <button onClick={() => setModule('settings')} title="Settings"
            className={cx('transition-colors', module === 'settings' ? 'text-electric-soft' : 'text-zinc-500 hover:text-zinc-300')}><Icon name="Settings" size={15} /></button>
        </div>
      </div>
    </aside>
  );

  /* ---------- folded rail (icon-only) — only the selected screens ---------- */
  const RailPanel = () => (
    <aside className="flex h-full w-[64px] shrink-0 flex-col items-center overflow-y-auto border-r border-line bg-ink-900/60 py-4">
      <button onClick={() => foldTo(false)} title="Unfold menu"
        className="flex h-9 w-9 items-center justify-center rounded-xl border border-line bg-ink-850 text-zinc-400 transition-colors hover:text-head">
        <Icon name="PanelLeftOpen" size={17} />
      </button>
      <nav className="mt-4 w-full space-y-1 px-2.5">
        {pinnedItems.map(n => <NavBtn key={n.id} n={n} rail on={module === n.id} onClick={() => setModule(n.id)} />)}
        <button onClick={() => setCustomizing(true)} title="Add tools"
          className="flex w-full items-center justify-center rounded-xl py-2 text-zinc-500 transition-colors hover:bg-ink-800 hover:text-electric-soft">
          <span className="flex h-8 w-8 items-center justify-center rounded-lg border border-dashed border-ink-600"><Icon name="Plus" size={16} /></span>
        </button>
      </nav>
      <div className="mt-auto flex flex-col items-center gap-2 pt-3">
        <Avatar name={acct.name} color={acct.color} size={30} />
        <button onClick={() => setModule('settings')} title="Settings"
          className={cx('transition-colors', module === 'settings' ? 'text-electric-soft' : 'text-zinc-500 hover:text-zinc-300')}><Icon name="Settings" size={16} /></button>
      </div>
    </aside>
  );

  return (
    <div className={cx('relative flex h-full shrink-0', rail ? 'w-[64px]' : 'w-[248px]')}
      onMouseEnter={rail ? () => setHover(true) : undefined}
      onMouseLeave={rail ? () => setHover(false) : undefined}>
      {rail ? <RailPanel /> : <FullPanel overlay={false} />}
      {rail && hover && (
        <div className="absolute left-0 top-0 z-[60] h-full">
          <FullPanel overlay />
        </div>
      )}
      {customizing && <NavCustomizer pins={pins} onToggle={togglePin} onReset={() => savePins(DEFAULT_PINS)} onClose={() => setCustomizing(false)} />}
      {firstRun && <ProjectStarter onConfirm={finishStarter} />}
    </div>
  );
}

/* ---------- Topbar account + settings menu (consolidates theme / language / settings) ---------- */
function TopbarUserMenu({ theme, setTheme, lng, setLng, setModule, goHome }) {
  const [open, setOpen] = useState(false);
  const acct = window.useAccount ? window.useAccount() : { name: 'Producer', role: 'Producer', email: '', color: '#22D48A' };
  const day = theme === 'day';
  return (
    <div className="relative">
      <button onClick={() => setOpen(o => !o)} title="Account & settings"
        className={cx('flex items-center gap-1.5 rounded-full border bg-ink-900 py-0.5 pl-0.5 pr-1.5 transition-colors', open ? 'border-ink-600' : 'border-line hover:border-ink-600')}>
        <Avatar name={acct.name} color={acct.color} size={30} />
        <Icon name="ChevronDown" size={13} className="text-zinc-500" />
      </button>
      {open && (
        <React.Fragment>
          <div className="fixed inset-0 z-30" onClick={() => setOpen(false)}></div>
          <div className="fade-up absolute right-0 z-40 mt-2 w-[268px] overflow-hidden rounded-xl border border-line bg-ink-850 shadow-2xl">
            {/* profile */}
            <div className="flex items-center gap-3 border-b border-line p-3.5">
              <Avatar name={acct.name} color={acct.color} size={40} />
              <div className="min-w-0 flex-1">
                <div className="truncate text-[13px] font-700 text-head">{acct.name}</div>
                <div className="truncate text-[11px] text-zinc-500">{acct.company || (acct.role || 'Producer') + ' · Owner'}</div>
              </div>
              <Chip tone="mint">Owner</Chip>
            </div>
            {/* quick settings */}
            <div className="space-y-2.5 border-b border-line p-3.5">
              <div className="flex items-center justify-between gap-3">
                <span className="flex items-center gap-2 text-[12px] text-zinc-300"><Icon name={day ? 'Sun' : 'Moon'} size={14} className={day ? 'text-amber' : 'text-electric-soft'} />Theme</span>
                <span data-no-i18n="true" style={{ display: 'contents' }}><Seg value={theme} options={[['night', 'Night'], ['day', 'Day']]} onChange={setTheme} /></span>
              </div>
              <div className="flex items-center justify-between gap-3">
                <span className="flex items-center gap-2 text-[12px] text-zinc-300"><Icon name="Globe" size={14} className="text-zinc-500" />Language</span>
                <span data-no-i18n="true" style={{ display: 'contents' }}><Seg value={lng} options={[['en', 'EN'], ['fr', 'FR'], ['de', 'DE']]} onChange={v => { setLng(v); window.setLang && window.setLang(v); }} /></span>
              </div>
            </div>
            {/* menu items */}
            <div className="p-1.5">
              <button onClick={() => { window.openProductionStatus && window.openProductionStatus(); setOpen(false); }}
                className="mb-1 flex w-full items-center gap-2.5 rounded-lg border border-proflow/25 bg-proflow/8 px-2.5 py-2 text-left transition-colors hover:bg-proflow/15">
                <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-proflow/15 text-proflow-soft"><Icon name="Vault" size={14} /></span>
                <span className="min-w-0 flex-1">
                  <span className="block text-[12.5px] font-700 text-head">Production Status</span>
                  <span className="block text-[10px] text-zinc-500">Private P&amp;L · owner only</span>
                </span>
                <Icon name="Lock" size={12} className="shrink-0 text-zinc-500" />
              </button>
              <button onClick={() => { setModule('settings'); setOpen(false); }}
                className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-[12.5px] font-600 text-zinc-300 transition-colors hover:bg-ink-800"><Icon name="Settings" size={14} className="text-zinc-500" />All settings</button>
              <button onClick={() => { goHome(); setOpen(false); }}
                className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-[12.5px] font-600 text-zinc-300 transition-colors hover:bg-ink-800"><Icon name="LayoutGrid" size={14} className="text-zinc-500" />All projects</button>
              {/* View as — preview each role's experience (C) */}
              {window.PERMS && (
                <div className="mt-1 border-t border-line pt-1.5">
                  <div className="px-2.5 py-1 text-[10px] font-700 uppercase tracking-wider text-zinc-600">View as (local preview)</div>
                  <div className="max-h-[184px] overflow-y-auto">
                    {(() => {
                      const opts = [{ role: 'owner', name: 'You', dept: null }, { role: 'creator', name: 'A Creator', dept: null }];
                      try {
                        const ppl = window.SLStore.get('silverlines.people.v1', null) || (typeof readPeopleDB === 'function' ? readPeopleDB() : []);
                        const byId = {}; (ppl || []).forEach(x => { byId[x.id] = x; });
                        const pid = window.PROJ_ID || 'crimson';
                        const ids = pid === 'crimson' ? (window.SLStore.get('silverlines.roster.v1', []) || []) : ((window.SLStore.get('silverlines.assign.v1', {}) || {})[(window.PROJECT && window.PROJECT.code) || ''] || []);
                        ids.slice(0, 6).forEach(id => { const x = byId[id]; if (x) opts.push({ role: 'user', name: x.name, dept: x.dept, personId: id }); });
                      } catch (e) {}
                      opts.push({ role: 'guest', name: 'A Guest (client)', dept: null });
                      const cur = window.PERMS.current();
                      const curKey = cur ? (cur.role + (cur.personId || '')) : 'owner';
                      return opts.map((o, i) => {
                        const active = curKey === (o.role + (o.personId || ''));
                        return (
                          <button key={i} onClick={() => {
                            window.PERMS.viewAs(o.role === 'owner' ? null : { role: o.role, dept: o.dept, name: o.name, personId: o.personId });
                            const vm = window.PERMS.visibleModules(Object.keys(MODULES)); if (o.role !== 'owner' && vm.length) setModule(vm[0]);
                            setOpen(false);
                          }} className={cx('flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[12px] transition-colors hover:bg-ink-800', active ? 'text-electric-soft' : 'text-zinc-300')}>
                            <Icon name={o.role === 'owner' ? 'Crown' : o.role === 'creator' ? 'PenTool' : o.role === 'guest' ? 'UserRound' : 'HardHat'} size={13} className="text-zinc-500" />
                            <span className="min-w-0 flex-1 truncate">{o.name}{o.dept ? (' · ' + o.dept) : ''}</span>
                            <span className="shrink-0 rounded bg-ink-800 px-1.5 text-[9px] font-700 uppercase text-zinc-500">{window.PERMS.label(o.role)}</span>
                            {active && <Icon name="Check" size={12} className="text-mint" />}
                          </button>
                        );
                      });
                    })()}
                  </div>
                </div>
              )}
              <button onClick={() => { setOpen(false); if (window.Auth) { window.Auth.signOut(); window.toast('Signed out — see you on set', 'LogOut'); setTimeout(() => { try { location.reload(); } catch (e) {} }, 250); } }}
                className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-[12.5px] font-600 text-zinc-400 transition-colors hover:bg-ink-800 hover:text-[#FF6B6B]"><Icon name="LogOut" size={14} />Sign out</button>
            </div>
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

function Topbar({ view, setView, module, setModule, theme, setTheme }) {
  const { goHome } = useContext(AccessContext);
  const cur = ALL_NAV.find(n => n.id === module) || (module === 'settings' ? { label: 'Settings' } : null);
  const day = theme === 'day';
  const [lng, setLng] = useState(() => (window.getLang && window.getLang()) || 'en');
  const cycleLang = () => { const nx = { en: 'fr', fr: 'de', de: 'en' }[lng] || 'en'; setLng(nx); window.setLang && window.setLang(nx); };
  const [q, setQ] = useState('');
  const [bell, setBell] = useState(false);
  const { list: notifs, markAll, markOne } = useNotifs();
  const unreadCount = notifs.filter(n => !n.read).length;
  const searchRef = useRef(null);
  useEffect(() => {
    const h = e => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); searchRef.current && searchRef.current.focus(); } };
    window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
  }, []);
  const ql = q.trim().toLowerCase();
  const results = ql ? [
    ...ALL_NAV.filter(n => (n.label + ' ' + n.sub).toLowerCase().includes(ql)).map(n => ({ icon: n.icon, label: n.label, sub: 'Module', id: n.id })),
    ...SCENES.filter(s => (s.no + ' ' + s.title + ' ' + s.loc).toLowerCase().includes(ql)).map(s => ({ icon: 'Clapperboard', label: `SC ${s.no} — ${s.title}`, sub: s.loc, id: 'preprod' })),
    ...PEOPLE.filter(p => (p.name + ' ' + p.role).toLowerCase().includes(ql)).map(p => ({ icon: 'User', label: p.name, sub: p.role, id: 'preprod' })),
    ...(window.loclibLoad ? window.loclibLoad().filter(l => (l.name + ' ' + l.address + ' ' + l.type).toLowerCase().includes(ql)).map(l => ({ icon: 'MapPinned', label: l.name, sub: l.address || 'Location', id: 'location' })) : []),
    ...(window.budgetLoad ? window.budgetLoad().flatMap(c => c.lines.map(l => ({ ...l, cat: c.name }))).filter(l => (l.desc + ' ' + l.cat).toLowerCase().includes(ql)).map(l => ({ icon: 'Receipt', label: l.desc || 'Line item', sub: l.cat, id: 'budget' })) : []),
    ...(window.MSG_CHANNELS ? window.MSG_CHANNELS.filter(c => (c.name + ' ' + c.desc).toLowerCase().includes(ql)).map(c => ({ icon: c.icon || 'Hash', label: '#' + c.name, sub: c.desc, id: 'messages' })) : []),
  ].slice(0, 9) : [];
  const go = r => { setModule(r.id); setQ(''); searchRef.current && searchRef.current.blur(); };
  return (
    <header className="flex items-center gap-4 border-b border-line bg-ink-900/60 px-5 py-3">
      <div className="flex items-center gap-2 whitespace-nowrap text-[12.5px]">
        <button onClick={goHome} className="text-zinc-500 transition-colors hover:text-zinc-200">{PROJECT.name}</button>
        {window.PROJ_ID === 'crimson' && <span className="inline-flex items-center gap-1 rounded-full bg-amber/15 px-2 py-0.5 text-[9.5px] font-700 uppercase tracking-wide text-amber"><Icon name="FlaskConical" size={10} />Demo</span>}
        <Icon name="ChevronRight" size={13} className="text-zinc-700" />
        <span className="font-display text-[12.5px] text-zinc-200">{cur ? cur.label : ''}</span>
      </div>

      <div className="ml-auto flex items-center gap-2.5">
        <div className="relative hidden md:block">
          <div className="flex items-center gap-2 rounded-lg border border-line bg-ink-900 px-3 py-2">
            <Icon name="Search" size={14} className="text-zinc-500" />
            <input ref={searchRef} value={q} onChange={e => setQ(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter' && results[0]) go(results[0]); if (e.key === 'Escape') setQ(''); }}
              onBlur={() => setTimeout(() => setQ(''), 160)}
              placeholder="Search scenes, crew, locations, budget…" className="w-44 bg-transparent text-[12.5px] text-zinc-200 outline-none placeholder:text-zinc-600" />
            <kbd className="rounded border border-line px-1.5 font-mono text-[10px] text-zinc-500">⌘K</kbd>
          </div>
          {results.length > 0 && (
            <div className="fade-up absolute left-0 right-0 z-40 mt-2 overflow-hidden rounded-xl border border-line bg-ink-850 shadow-2xl">
              {results.map((r, i) => (
                <button key={i} onMouseDown={e => e.preventDefault()} onClick={() => go(r)}
                  className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-ink-800">
                  <Icon name={r.icon} size={14} className="shrink-0 text-zinc-500" />
                  <span className="min-w-0 flex-1 truncate text-[12.5px] font-600 text-zinc-200">{r.label}</span>
                  <span className="shrink-0 text-[11px] text-zinc-500">{r.sub}</span>
                </button>
              ))}
            </div>
          )}
        </div>
        <div className="relative">
          <button onClick={() => setBell(b => !b)} className="relative flex h-9 w-9 items-center justify-center rounded-lg border border-line bg-ink-900 text-zinc-400 hover:text-zinc-200">
            <Icon name="Bell" size={16} />
            {unreadCount > 0 && (
              <span className="absolute -right-1.5 -top-1.5 flex h-[17px] min-w-[17px] items-center justify-center rounded-full bg-cta px-1 font-mono text-[9.5px] font-700 text-cta-ink">
                {unreadCount > 9 ? '9+' : unreadCount}
              </span>
            )}
          </button>
          {bell && (
            <React.Fragment>
              <div className="fixed inset-0 z-30" onClick={() => setBell(false)}></div>
              <div className="fade-up absolute right-0 z-40 mt-2 w-[330px] overflow-hidden rounded-xl border border-line bg-ink-850 shadow-2xl">
                <div className="flex items-center justify-between border-b border-line px-3.5 py-2.5">
                  <span className="text-[12px] font-700 text-head">Notifications{unreadCount > 0 ? ` · ${unreadCount} new` : ''}</span>
                  {notifs.length > 0 && <button onClick={markAll} className="text-[11px] font-600 text-electric-soft hover:text-electric">Mark all read</button>}
                </div>
                {notifs.length === 0
                  ? <div className="px-4 py-6 text-center text-[12px] text-zinc-600">All quiet — approvals, bookings and roster moves land here as they happen.</div>
                  : notifs.slice(0, 12).map(n => (
                    <button key={n.id} onClick={() => { markOne(n.id); if (n.moduleId) setModule(n.moduleId); setBell(false); }}
                      className={cx('flex w-full items-start gap-2.5 border-b border-line/40 px-3.5 py-2.5 text-left transition-colors last:border-0 hover:bg-ink-800', !n.read && 'bg-ink-900/70')}>
                      <span className="relative mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-ink-700 text-zinc-400">
                        <Icon name={n.icon} size={14} />
                        {!n.read && <span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-cta"></span>}
                      </span>
                      <span className={cx('min-w-0 flex-1 text-[12px] leading-snug', n.read ? 'text-zinc-500' : 'text-zinc-200')}>{n.text}</span>
                      <span className="shrink-0 font-mono text-[10px] text-zinc-600">{notifAgo(n.ts)}</span>
                    </button>
                  ))}
              </div>
            </React.Fragment>
          )}
        </div>

        {/* Day / Night moved into the account menu */}
        <div className="flex items-center rounded-lg border border-line bg-ink-900 p-0.5">
          {[['desktop','Monitor','Desktop'],['mobile','Smartphone','Crew Phone']].map(([v, ic, lbl]) => (
            <button key={v} onClick={() => setView(v)}
              className={cx('flex items-center gap-1.5 rounded-[7px] px-2.5 py-1.5 font-display text-[12px] transition-colors',
                view === v ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>
              <Icon name={ic} size={14} />{lbl}
            </button>
          ))}
        </div>
        <TopbarUserMenu theme={theme} setTheme={setTheme} lng={lng} setLng={setLng} setModule={setModule} goHome={goHome} />
      </div>
    </header>
  );
}

/* ---------- Reusable phone chrome ---------- */
function PhoneShell({ children, footer }) {
  return (
    <div className="relative w-[392px] overflow-hidden rounded-[40px] border-[10px] border-ink-700 bg-ink-950 shadow-2xl" style={{ height: 784 }}>
      <div className="absolute left-1/2 top-0 z-40 h-6 w-32 -translate-x-1/2 rounded-b-2xl bg-ink-700" />
      <div className="flex items-center justify-between px-6 pb-1 pt-3 text-[11px] font-600 text-zinc-300">
        <span className="font-mono">9:41</span>
        <div className="flex items-center gap-1.5"><Icon name="Signal" size={12} /><Icon name="Wifi" size={12} /><Icon name="BatteryFull" size={14} /></div>
      </div>
      {children}
      {footer}
    </div>
  );
}

/* ---------- Producer phone (full 5-phase app) ---------- */
function ProducerPhone({ module, setModule }) {
  const Mod = MODULES[module];
  const acct = window.useAccount ? window.useAccount() : { name: 'Producer', color: '#22D48A' };
  return (
    <PhoneShell footer={
      <div className="absolute bottom-0 left-0 right-0 z-20 flex items-center justify-around border-t border-line bg-ink-900/95 px-2 pb-5 pt-2.5 backdrop-blur">
        {NAV.map(n => {
          const on = module === n.id;
          return (
            <button key={n.id} onClick={() => setModule(n.id)} className="flex flex-col items-center gap-1">
              <Icon name={n.icon} size={20} className={on ? 'text-electric-soft' : 'text-zinc-500'} />
              <span className={cx('font-display text-[9px]', on ? 'text-electric-soft' : 'text-zinc-600')}>{n.label.split(' ')[0]}</span>
            </button>
          );
        })}
      </div>
    }>
      <div className="flex items-center justify-between border-b border-line px-4 py-3">
        <Logo />
        <Avatar name={acct.name} color={acct.color} size={28} />
      </div>
      <div className="no-scrollbar overflow-y-auto px-4 py-4" style={{ height: 784 - 20 - 56 - 30 - 70 }}>
        <Mod mobile={true} />
      </div>
    </PhoneShell>
  );
}

/* ---------- Crew phone — call sheet & dept board always on; tool revocable ---------- */
const CREW_DEPT_MAP = { 'Lighting':'lighting', 'Art':'art', 'Camera':'camera', 'Production':'direction', 'Hair & Makeup':'mua' };
const CREW_CALL_DEPT = { 'Lighting':'Lighting', 'Art':'Art', 'Camera':'Camera', 'Production':'Prod', 'Hair & Makeup':'MUA' };
const CREW_TOOL_TAB = { floorplan:{ icon:'Lightbulb', label:'Floor Plan' }, purchase:{ icon:'ReceiptText', label:'Purchasing' }, production:{ icon:'CalendarClock', label:'Schedule' } };

function CrewDeptBoard({ person }) {
  const dept = DEPARTMENTS.find(d => d.id === CREW_DEPT_MAP[person.dept]) || DEPARTMENTS[0] || { id: 'none', label: 'Department', icon: 'Users', items: [] };
  const [items, setItems] = useState(dept.items.map(i => [...i]));
  const st = statusFor(items);
  const myCall = CREW_CALL.find(c => c.name === person.member);
  const deptCrew = CREW_CALL.filter(c => c.dept === CREW_CALL_DEPT[person.dept]);
  const loc0 = LOCATIONS[0] || { name: 'Location TBD', kind: 'Set when locations lock' }; // empty production: no location yet
  const toggle = i => setItems(list => list.map((it, j) => j === i ? [it[0], !it[1]] : it));
  return (
    <div className="fade-up space-y-3">
      {/* dept header */}
      <div className="rounded-xl border border-line bg-ink-900 p-3.5">
        <div className="flex items-center gap-3">
          <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl" style={{ background:`${person.color}1f`, color:person.color }}>
            <Icon name={dept.icon} size={19} />
          </span>
          <div className="min-w-0 flex-1">
            <div className="truncate text-[14px] font-700 text-head">{dept.name}</div>
            <div className="truncate text-[11px] text-zinc-500">Head: {dept.head} · {dept.crew} crew</div>
          </div>
          <Chip tone={st.tone}>{st.label}</Chip>
        </div>
      </div>

      {/* your call today */}
      <div className="rounded-xl border border-electric/30 bg-electric/8 p-3.5">
        <div className="flex items-center justify-between gap-3">
          <div>
            <div className="whitespace-nowrap text-[10px] uppercase tracking-[0.14em] text-zinc-500">Your call · {PROJECT.day}</div>
            <div className="font-display text-[30px] font-700 leading-tight text-head">{myCall ? myCall.call : '06:00'}</div>
          </div>
          <div className="min-w-0 text-right">
            <div className="flex items-center justify-end gap-1 truncate text-[12px] font-600 text-zinc-200"><Icon name="MapPin" size={12} className="shrink-0 text-electric-soft" />{loc0.name}</div>
            <div className="mt-0.5 truncate text-[11px] text-zinc-500">{loc0.kind}</div>
          </div>
        </div>
      </div>

      {/* dept checklist */}
      <div className="rounded-xl border border-line bg-ink-900 p-3.5">
        <div className="mb-2 flex items-center justify-between">
          <span className="text-[12px] font-700 text-head">Department board</span>
          <span className="font-mono text-[10.5px] text-zinc-500">{items.filter(i => i[1]).length}/{items.length} done</span>
        </div>
        <div className="space-y-1.5">
          {items.map((it, i) => (
            <button key={i} onClick={() => toggle(i)} className="flex w-full items-center gap-2.5 rounded-lg border border-line bg-ink-850 px-2.5 py-2 text-left transition-colors hover:border-ink-600">
              <span className={cx('flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border transition-colors', it[1] ? 'border-mint bg-mint/20 text-mint' : 'border-ink-600 text-transparent')}>
                <Icon name="Check" size={11} sw={3.5} />
              </span>
              <span className={cx('text-[12px] font-600', it[1] ? 'text-zinc-500 line-through' : 'text-zinc-200')}>{it[0]}</span>
            </button>
          ))}
        </div>
      </div>

      {/* dept gear, prep & return — shared with the producer's Gear & Prep board */}
      <CrewGearPrep deptId={dept.id} />

      {/* dept crew on set today */}
      {deptCrew.length > 0 && (
        <div className="rounded-xl border border-line bg-ink-900 p-3.5">
          <div className="mb-2 text-[12px] font-700 text-head">On set today</div>
          <div className="space-y-1.5">
            {deptCrew.map(c => (
              <div key={c.name} className="flex items-center gap-2.5 rounded-lg px-1 py-1">
                <Avatar name={c.name} color={person.color} size={26} />
                <div className="min-w-0 flex-1">
                  <div className="truncate text-[12px] font-600 text-zinc-100">{c.name}</div>
                  <div className="truncate text-[10.5px] text-zinc-500">{c.role}</div>
                </div>
                <span className="shrink-0 font-mono text-[11.5px] text-zinc-300">{c.call}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function CrewPhone({ person }) {
  const { access } = useContext(AccessContext);
  const granted = !!access[person.id];
  const [tab, setTab] = useState('callsheet');
  useEffect(() => { setTab('callsheet'); }, [person.id]);
  const Tool = MODULES[person.tool];
  const dept = DEPARTMENTS.find(d => d.id === CREW_DEPT_MAP[person.dept]) || DEPARTMENTS[0] || { id: 'none', label: 'Department', icon: 'Users', items: [] };
  const toolTab = CREW_TOOL_TAB[person.tool] || { icon:'Wrench', label:'Tool' };
  const tabs = [
    { id:'callsheet', icon:'ClipboardList', label:'Call Sheet' },
    { id:'dept', icon:dept.icon, label:'My Dept' },
    { id:'tool', icon:toolTab.icon, label:toolTab.label, lock:!granted },
  ];
  const H = 784 - 20 - 64 - 28 - 64;

  return (
    <PhoneShell footer={
      <div className="absolute bottom-0 left-0 right-0 z-20 flex items-center justify-around border-t border-line bg-ink-900/95 px-2 pb-5 pt-2.5 backdrop-blur">
        {tabs.map(t => {
          const on = tab === t.id;
          return (
            <button key={t.id} onClick={() => setTab(t.id)} className="flex flex-col items-center gap-1">
              <span className="relative">
                <Icon name={t.icon} size={20} className={on ? 'text-electric-soft' : 'text-zinc-500'} />
                {t.lock && <span className="absolute -right-2 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-line bg-ink-700 text-zinc-400"><Icon name="Lock" size={8} sw={3} /></span>}
              </span>
              <span className={cx('font-display text-[9px]', on ? 'text-electric-soft' : 'text-zinc-600')}>{t.label}</span>
            </button>
          );
        })}
      </div>
    }>
      {/* crew identity header */}
      <div className="border-b border-line px-4 py-3" style={{ background: `linear-gradient(120deg, ${person.color}1f, transparent)` }}>
        <div className="flex items-center gap-2.5">
          <div className="relative">
            <Avatar name={person.member} color={person.color} size={34} />
            <span className={cx('absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-ink-950', granted ? 'bg-mint' : 'bg-zinc-600')} />
          </div>
          <div className="min-w-0 flex-1">
            <div className="truncate text-[13px] font-700 text-head">{person.member}</div>
            <div className="truncate text-[11px] text-zinc-400">{person.role} · {person.dept}</div>
          </div>
          {granted
            ? <span className="flex items-center gap-1 rounded-full border border-mint/30 bg-mint/10 px-2 py-1 text-[10px] font-700 text-mint"><span className="h-1.5 w-1.5 rounded-full bg-mint proflow-pulse" />LIVE</span>
            : <span className="flex items-center gap-1 whitespace-nowrap rounded-full border border-line bg-ink-800 px-2 py-1 text-[10px] font-700 text-zinc-500"><Icon name="Lock" size={10} />TOOL OFF</span>}
        </div>
      </div>

      <div className="no-scrollbar overflow-y-auto px-3.5 py-3.5" style={{ height: H }}>
        {tab === 'callsheet' && (
          <div className="fade-up">
            <div className="mb-3 flex items-center gap-2 rounded-xl border border-mint/25 bg-mint/8 px-3 py-2">
              <Icon name="BadgeCheck" size={14} className="shrink-0 text-mint" />
              <div className="text-[11px] text-zinc-400">Always available to every crew member — even when tool access is paused</div>
            </div>
            <CallDoc mobile={true} />
          </div>
        )}
        {tab === 'dept' && <CrewDeptBoard person={person} />}
        {tab === 'tool' && (granted ? (
          <div className="fade-up">
            {/* scope banner */}
            <div className="mb-3 flex items-center gap-2 rounded-xl border border-proflow/25 bg-proflow/8 px-3 py-2">
              <Icon name="ShieldCheck" size={14} className="shrink-0 text-proflow-soft" />
              <div className="text-[11px] text-zinc-400">You can access <span className="font-700 text-zinc-200">{person.toolLabel}</span> — scoped by production</div>
            </div>
            <div className="mb-3 flex flex-wrap gap-1.5">
              {person.scope.map(s => (
                <span key={s} className="inline-flex items-center gap-1 rounded-full border border-line bg-ink-800 px-2 py-[3px] text-[10px] font-600 text-zinc-300">
                  <Icon name="Check" size={9} sw={3} />{s}
                </span>
              ))}
            </div>
            <Tool mobile={true} readOnly={person.id === 'dop'} />
          </div>
        ) : (
          <div className="fade-up flex h-full flex-col items-center justify-center px-5 text-center">
            <span className="flex h-16 w-16 items-center justify-center rounded-2xl bg-ink-800 text-zinc-500"><Icon name="Lock" size={30} /></span>
            <div className="mt-4 font-display text-[22px] font-600 text-head">Tool access paused</div>
            <div className="mt-1.5 text-[12.5px] text-zinc-500">Production has paused <span className="font-600 text-zinc-300">{person.toolLabel}</span> on your device. Your call sheet and department board stay available full-time.</div>
            <div className="mt-4 flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-3 py-2 text-[11px] text-zinc-500">
              <Icon name="Smartphone" size={12} />{person.device}
            </div>
          </div>
        ))}
      </div>
    </PhoneShell>
  );
}

/* ---------- Mobile stage: choose who is holding the phone ---------- */
function MobileStage({ module, setModule }) {
  const [who, setWho] = useState('producer');
  const acct = window.useAccount ? window.useAccount() : { name: 'Producer' };
  const person = ACCESS.find(a => a.id === who);
  const roles = [{ id:'producer', member: acct.name, role:'Producer (full)' }, ...ACCESS];

  return (
    <div className="flex flex-col items-center py-7">
      {/* role picker */}
      <div className="mb-5 flex flex-wrap items-center justify-center gap-2 rounded-2xl border border-line bg-ink-900 p-2">
        <span className="px-2 text-[11px] font-600 uppercase tracking-[0.12em] text-zinc-500">On set as</span>
        {roles.map(r => (
          <button key={r.id} onClick={() => setWho(r.id)}
            className={cx('flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 font-display text-[12px] transition-colors',
              who === r.id ? 'bg-ink-700 text-head' : 'text-zinc-400 hover:text-zinc-200')}>
            <span className="h-2 w-2 rounded-full" style={{ background: r.id === 'producer' ? '#22D48A' : r.color }} />
            {r.member.split(' ')[0]}
            <span className="hidden text-zinc-500 sm:inline">· {r.role.replace(' (full)', '')}</span>
          </button>
        ))}
      </div>

      {who === 'producer'
        ? <ProducerPhone module={module} setModule={setModule} />
        : <CrewPhone person={person} />}
    </div>
  );
}

/* mounts once in App; window.createProduction() (from landing or the in-app
   switcher) opens this. On confirm it writes to the registry and opens the
   new production. */
function CreateProductionHost() {
  const [open, setOpen] = useState(false);
  useEffect(() => {
    const h = () => setOpen(true);
    window.addEventListener('sl-new-production', h);
    return () => window.removeEventListener('sl-new-production', h);
  }, []);
  const Modal = window.NewProductionModal;
  if (!Modal) return null;
  return <Modal open={open} onClose={() => setOpen(false)} onCreate={opts => {
    const p = window.slAddProduction ? window.slAddProduction(opts) : null;
    setOpen(false);
    if (p && window.openProject) { window.toast(`${p.name} created`, 'FolderPlus'); window.openProject(p.pid); }
  }} />;
}

/* mounts once in App; window.openAccount() (from the dashboard avatar or the
   in-production Settings) opens the account / base-settings overlay. */
function AccountHost() {
  const [open, setOpen] = useState(false);
  useEffect(() => {
    const h = () => setOpen(true);
    window.addEventListener('sl-open-account', h);
    return () => window.removeEventListener('sl-open-account', h);
  }, []);
  const Modal = window.AccountModal;
  if (!Modal) return null;
  return <Modal open={open} onClose={() => setOpen(false)} />;
}

/* C — role-preview banner + blocked-module notice */
function PreviewBanner() {
  const [v, setV] = useState(() => window.PERMS && window.PERMS.current());
  useEffect(() => { const h = () => setV(window.PERMS && window.PERMS.current()); window.addEventListener('sl-perms', h); return () => window.removeEventListener('sl-perms', h); }, []);
  if (!v) return null;
  const roleLabel = window.PERMS.label ? window.PERMS.label(v.role) : v.role;
  return (
    <div className="fixed left-1/2 top-2 z-[90] flex -translate-x-1/2 items-center gap-3 rounded-full border border-amber/40 bg-amber/15 px-4 py-1.5 text-[12px] font-600 text-amber shadow-lg backdrop-blur">
      <Icon name="Eye" size={13} />
      <span>Previewing as {v.name ? v.name + ' · ' : ''}{roleLabel}{v.dept ? ' (' + v.dept + ')' : ''} — editing limited to this role. Enforced for real when crew sign-in ships.</span>
      <button onClick={() => window.PERMS.viewAs(null)} className="shrink-0 rounded-full bg-amber/25 px-2.5 py-0.5 text-[11px] font-700 transition-colors hover:bg-amber/35">Exit preview</button>
    </div>
  );
}
function RoleBlocked() {
  const v = window.PERMS && window.PERMS.current();
  const roleLabel = v && window.PERMS.label ? window.PERMS.label(v.role) : 'this role';
  return (
    <div className="flex min-h-[50vh] flex-col items-center justify-center text-center">
      <span className="flex h-14 w-14 items-center justify-center rounded-2xl bg-ink-800 text-zinc-500"><Icon name="Lock" size={26} /></span>
      <h3 className="mt-4 font-display text-[18px] text-head">Not available to {roleLabel}</h3>
      <p className="mt-1 max-w-[360px] text-[13px] text-zinc-500">This tool is outside what {roleLabel} can see on this production. Exit preview to return to full access.</p>
      <button onClick={() => window.PERMS.viewAs(null)} className="mt-4 rounded-xl bg-cta px-4 py-2.5 text-[13px] font-700 text-cta-ink hover:bg-cta-hover">Exit preview</button>
    </div>
  );
}

function App() {
  const [home, setHome] = useState(() => {
    try { if (localStorage.getItem('silverlines.autoopen') === '1') { localStorage.removeItem('silverlines.autoopen'); return false; } } catch (e) {}
    return true;
  });
  const [subtabs, setSubtabsState] = useState(() => {
    return window.SLStore.get('silverlines.subtabs', {});
  });
  const setSubtab = (mod, tabId) => setSubtabsState(o => {
    const n = { ...o, [mod]: tabId };
    window.SLStore.set('silverlines.subtabs', n);
    return n;
  });
  const [module, setModule] = useState(() => localStorage.getItem('producer.module') || 'develop');
  const [view, setView] = useState(() => localStorage.getItem('producer.view') || 'desktop');
  const [theme, setTheme] = useState(() => localStorage.getItem('producer.theme') || 'night');
  const [access, setAccess] = useState(() => {
    try { return window.SLStore.get('producer.access', ACCESS.reduce((m, a) => (m[a.id] = true, m), {})); }
    catch (e) { return ACCESS.reduce((m, a) => (m[a.id] = true, m), {}); }
  });
  useEffect(() => { localStorage.setItem('producer.module', module); }, [module]);
  useEffect(() => { localStorage.setItem('producer.view', view); }, [view]);
  useEffect(() => { localStorage.setItem('producer.theme', theme); document.documentElement.dataset.theme = theme; }, [theme]);
  useEffect(() => { window.SLStore.set('producer.access', access); }, [access]);
  /* apply saved appearance prefs (grain/motion) on boot so the dashboard reflects
     them before the Account overlay or in-production Settings ever mount */
  useEffect(() => {
    try {
      const st = window.SLStore.get('silverlines.settings.v1', {}) || {};
      document.documentElement.style.setProperty('--grain-op', st.grain === false ? '0' : '');
      document.documentElement.classList.toggle('reduce-motion', st.motion === false);
    } catch (e) {}
  }, []);

  /* currency / production-name changes anywhere re-render every figure in the app */
  const [, curTick] = useState(0);
  useEffect(() => {
    const h = () => curTick(n => n + 1);
    window.addEventListener('sl-currency', h);
    window.addEventListener('sl-project', h);
    window.addEventListener('sl-perms', h);
    return () => { window.removeEventListener('sl-currency', h); window.removeEventListener('sl-project', h); window.removeEventListener('sl-perms', h); };
  }, []);

  /* beta auth gate — keep the login in front of the app until signed in */
  const [authed, setAuthed] = useState(() => !window.Auth || window.Auth.isAuthed());
  useEffect(() => {
    const h = () => setAuthed(!window.Auth || window.Auth.isAuthed());
    window.addEventListener('sl-account', h);
    return () => window.removeEventListener('sl-account', h);
  }, []);

  const Mod = MODULES[module];

  /* dev hook — lets test harnesses drive navigation programmatically */
  window.__sl = { setModule, setHome, setView, setSubtab, modules: Object.keys(MODULES) };

  if (!authed) {
    return (
      <AccessContext.Provider value={{ access, setAccess, theme, setTheme, setModule, subtabs, setSubtab, goHome: () => setHome(true) }}>
        <Toasts />
        {window.AuthGate ? <window.AuthGate onDone={() => setAuthed(true)} /> : null}
      </AccessContext.Provider>
    );
  }

  return (
    <AccessContext.Provider value={{ access, setAccess, theme, setTheme, setModule, subtabs, setSubtab, goHome: () => setHome(true) }}>
      <Toasts />
      <UndoToasts />
      <CreateProductionHost />
      <AccountHost />
      <PreviewBanner />
      {window.ProductionStatusRoot ? <window.ProductionStatusRoot /> : null}
      {home ? (
        <LandingPage onOpen={pid => { if (pid && pid !== window.PROJ_ID && window.openProject) { window.openProject(pid); } else { setHome(false); } }} />
      ) : (
      <div className="flex h-screen w-full overflow-hidden bg-ink-950 text-zinc-200">
        {view === 'desktop' && <Sidebar module={module} setModule={setModule} />}
        <div className="flex min-w-0 flex-1 flex-col">
          <Topbar view={view} setView={setView} module={module} setModule={setModule} theme={theme} setTheme={setTheme} />
          <main className="grain relative flex-1 overflow-y-auto">
            {view === 'desktop'
              ? <div key={module} className="relative z-10 mx-auto max-w-[1180px] px-7 py-7">
                  {(!window.PERMS || window.PERMS.visibleModules(Object.keys(MODULES)).includes(module))
                    ? <Mod mobile={false} readOnly={window.PERMS ? window.PERMS.readOnly(module) : false} />
                    : <RoleBlocked />}
                </div>
              : <div className="relative z-10"><MobileStage module={module} setModule={setModule} /></div>}
          </main>
        </div>
      </div>
      )}
    </AccessContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

