// Modaler for planleggerens lagre-modell og programbibliotek:
// DraftRecoveryModal (krasjgjenoppretting ved oppstart), UnsavedChangesModal
// (den sentrale ulagret-vakten) og OpenProgramModal (samlet «Åpne …»-dialog
// med fanene Mine programmer / Utforsk / Fra fil — erstatter «Åpne fil» og
// «Mine programmer» som egne knapper). Per-rad-handlingene i «Mine
// programmer» (RenameModal, VisibilityModal, ProgramRowMenu) virker alle
// uten å laste inn programmet i editoren. Lastes med
// <script type="text/babel" src="planner-modals.jsx">, etter
// planner-state.jsx (PC/fmt) og shared/auth-ui.jsx (useAuthState/AuthScreen).

const modalOverlayStyle = { position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' };
const modalBoxStyle = (width) => ({ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width, display:'flex', flexDirection:'column', gap:14 });
const ghostBtnStyle = (busy) => ({ padding:'8px 16px', borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor: busy?'default':'pointer' });
const accentBtnStyle = (busy) => ({ padding:'8px 16px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor: busy?'default':'pointer' });

// ─── DraftRecoveryModal ───────────────────────────────────────────────────
// Vises ubetinget (høyest prioritet av alle modaler) når et krasjgjenopp-
// rettings-utkast finnes ved oppstart. Ingen «avbryt»-vei — det finnes
// ingenting å avbryte til før valget er tatt.
function DraftRecoveryModal({ name, savedAt, onRestore, onDiscard }) {
  const [busy, setBusy] = React.useState(null); // null | 'restore' | 'discard'
  const timeLabel = savedAt
    ? new Date(savedAt).toLocaleTimeString('no-NO', { hour:'2-digit', minute:'2-digit' })
    : 'et ukjent tidspunkt';

  const handle = async (choice, fn) => {
    setBusy(choice);
    try { await fn(); } finally { setBusy(null); }
  };

  return (
    <div style={{ ...modalOverlayStyle, zIndex:300 }}>
      <div style={modalBoxStyle(440)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Ulagret arbeid funnet</div>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink2, lineHeight:1.5 }}>
          Du har ulagrede endringer i «{name}» fra {timeLabel} — trolig fra en fane som ble lukket eller krasjet før de ble lagret.
        </div>
        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button onClick={()=>handle('discard', onDiscard)} disabled={busy!==null} style={ghostBtnStyle(busy!==null)}>
            {busy==='discard' ? 'Forkaster …' : 'Forkast'}
          </button>
          <button onClick={()=>handle('restore', onRestore)} disabled={busy!==null} style={accentBtnStyle(busy!==null)}>
            {busy==='restore' ? 'Gjenoppretter …' : 'Gjenopprett'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── UnsavedChangesModal ────────────────────────────────────────────────
// Den sentrale ulagret-vakten. Samme dialog uansett hvilken handling som
// trigget den (Ny økt / Åpne … / Kjør time) — tre valg, ikke window.confirm.
function UnsavedChangesModal({ programName, onSaveAndContinue, onDiscardAndContinue, onCancel, busy, error }) {
  return (
    <div style={{ ...modalOverlayStyle, zIndex:250 }} onClick={e=>{if(e.target===e.currentTarget && !busy)onCancel();}}>
      <div style={modalBoxStyle(420)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Ulagrede endringer</div>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink2, lineHeight:1.5 }}>
          «{programName}» har endringer som ikke er lagret til skyen ennå.
        </div>
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
          <button onClick={onSaveAndContinue} disabled={busy} style={{ ...accentBtnStyle(busy), width:'100%' }}>
            {busy ? 'Lagrer …' : 'Lagre og fortsett'}
          </button>
          <button onClick={onDiscardAndContinue} disabled={busy} style={{ ...ghostBtnStyle(busy), width:'100%', boxSizing:'border-box' }}>
            Forkast endringene
          </button>
          <button onClick={onCancel} disabled={busy} style={{ background:'none', border:'none', padding:'6px', color:PC.muted, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, cursor: busy?'default':'pointer' }}>
            Avbryt
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── Synlighetsmerke ────────────────────────────────────────────────────
const VISIBILITY_META = {
  private:  { label:'Privat',    color:'#7A7A82' },
  unlisted: { label:'Ulistet',   color:PC.z3 },
  public:   { label:'Offentlig', color:PC.z2 },
};
function VisibilityBadge({ visibility }) {
  const v = VISIBILITY_META[visibility] || VISIBILITY_META.private;
  return (
    <span style={{ fontFamily:"'Archivo',sans-serif", fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:v.color, background:`${v.color}18`, padding:'2px 7px', borderRadius:3, whiteSpace:'nowrap', flexShrink:0 }}>
      {v.label}
    </span>
  );
}

// ─── RenameModal ────────────────────────────────────────────────────────
function RenameModal({ defaultName, onClose, onSave }) {
  const [name, setName] = React.useState(defaultName || '');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const handleSave = async () => {
    if (!name.trim()) { setError('Navn kan ikke være tomt.'); return; }
    setBusy(true); setError(null);
    try { await onSave(name.trim()); onClose(); }
    catch (err) { setError(err.message || 'Kunne ikke gi nytt navn.'); setBusy(false); }
  };

  return (
    <div style={{ ...modalOverlayStyle, zIndex:220 }} onClick={e=>{if(e.target===e.currentTarget && !busy)onClose();}}>
      <div style={modalBoxStyle(360)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Gi nytt navn</div>
        <input value={name} onChange={e=>setName(e.target.value)} autoFocus
          style={{ width:'100%', padding:'9px 12px', borderRadius:6, border:`1.5px solid ${PC.line2}`, background:PC.bg, fontFamily:"'Archivo',sans-serif", fontSize:14, color:PC.ink, outline:'none', boxSizing:'border-box' }} />
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button onClick={onClose} disabled={busy} style={ghostBtnStyle(busy)}>Avbryt</button>
          <button onClick={handleSave} disabled={busy} style={accentBtnStyle(busy)}>{busy ? 'Lagrer …' : 'Lagre'}</button>
        </div>
      </div>
    </div>
  );
}

// ─── VisibilityModal ────────────────────────────────────────────────────
// allow_copy krever ærlig språk — det fjerner kopiknappen, ikke muligheten:
// kan noen åpne programmet, har nettleseren deres hele JSON-en uansett.
function VisibilityModal({ row, onClose, onSave }) {
  const [visibility, setVisibility] = React.useState(row.visibility);
  const [allowCopy, setAllowCopy] = React.useState(row.allowCopy);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const willBreakLink = row.visibility !== 'private' && visibility === 'private';

  const handleSave = async () => {
    setBusy(true); setError(null);
    try { await onSave({ visibility, allowCopy }); onClose(); }
    catch (err) { setError(err.message || 'Kunne ikke oppdatere synlighet.'); setBusy(false); }
  };

  const optRowStyle = (active) => ({ display:'flex', alignItems:'center', gap:10, padding:'9px 10px', borderRadius:6, border:`1px solid ${active?PC.accent:PC.line2}`, cursor:'pointer' });

  return (
    <div style={{ ...modalOverlayStyle, zIndex:220 }} onClick={e=>{if(e.target===e.currentTarget && !busy)onClose();}}>
      <div style={modalBoxStyle(400)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Endre synlighet</div>
        <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
          {[
            { id:'private',  label:'Privat',     desc:'Kun du' },
            { id:'unlisted', label:'Ulistet',    desc:'Alle med delingslenken' },
            { id:'public',   label:'Offentlig',  desc:'Synlig i Utforsk for alle innloggede' },
          ].map(opt => (
            <label key={opt.id} style={optRowStyle(visibility===opt.id)}>
              <input type="radio" name="visibility" checked={visibility===opt.id} onChange={()=>setVisibility(opt.id)} />
              <div>
                <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:13, color:PC.ink }}>{opt.label}</div>
                <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.muted }}>{opt.desc}</div>
              </div>
            </label>
          ))}
        </div>
        <label style={{ display:'flex', alignItems:'center', gap:8, fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink2, cursor:'pointer' }}>
          <input type="checkbox" checked={allowCopy} onChange={e=>setAllowCopy(e.target.checked)} />
          Tillat andre å lage sin egen kopi
        </label>
        {willBreakLink && (
          <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>
            Eksisterende delingslenker slutter å virke når programmet settes til privat.
          </div>
        )}
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button onClick={onClose} disabled={busy} style={ghostBtnStyle(busy)}>Avbryt</button>
          <button onClick={handleSave} disabled={busy} style={accentBtnStyle(busy)}>{busy ? 'Lagrer …' : 'Lagre'}</button>
        </div>
      </div>
    </div>
  );
}

// ─── Per-rad «…»-meny i «Mine programmer» ──────────────────────────────
function ProgramRowMenu({ onRename, onChangeVisibility, onShare, onDuplicate, onDelete }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  const itemStyle = { display:'block', width:'100%', textAlign:'left', padding:'8px 12px', border:'none', background:'transparent', fontFamily:"'Archivo',sans-serif", fontWeight:600, fontSize:12, color:PC.ink2, cursor:'pointer' };
  const item = (label, onClick, danger) => (
    <button onClick={()=>{ setOpen(false); onClick(); }} style={danger ? { ...itemStyle, color:PC.z5 } : itemStyle}>{label}</button>
  );
  return (
    <div ref={ref} style={{ position:'relative', flexShrink:0 }}>
      <button onClick={()=>setOpen(o=>!o)} style={{ width:26, height:26, borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontSize:16, fontWeight:700, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>⋯</button>
      {open && (
        <div style={{ position:'absolute', right:0, top:'calc(100% + 4px)', background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:6, boxShadow:'0 4px 16px rgba(0,0,0,0.12)', minWidth:190, zIndex:70, overflow:'hidden' }}>
          {item('Gi nytt navn', onRename)}
          {item('Endre synlighet', onChangeVisibility)}
          {item('Del', onShare)}
          {item('Dupliser', onDuplicate)}
          <div style={{ borderTop:`1px solid ${PC.line}` }} />
          {item('Slett', onDelete, true)}
        </div>
      )}
    </div>
  );
}

// ─── OpenProgramModal — fane «Mine programmer» ─────────────────────────
// Skiller «ikke innlogget» fra «auth ikke ferdig sjekket ennå» (useAuthState,
// ikke useAuthUser), og gir listehentingen et tidsavbrudd med prøv-igjen —
// slik at fanen aldri kan stå og laste i det uendelige uten forklaring.
// Radhandlingene (gi nytt navn / endre synlighet / del / dupliser / slett)
// virker alle uten å laste inn programmet — det er hele poenget med at
// lista er billig (aldri data i select()).
function MyProgramsTab({ onOpen }) {
  const { user, resolved } = useAuthState();
  const [rows, setRows] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [timedOut, setTimedOut] = React.useState(false);
  const [busyId, setBusyId] = React.useState(null);
  const [search, setSearch] = React.useState('');
  const [rowModal, setRowModal] = React.useState(null); // { type:'rename'|'visibility'|'share', row }

  const refresh = React.useCallback(() => {
    if (!resolved || !user) return;
    setError(null); setTimedOut(false); setRows(null);
    let done = false;
    const timer = setTimeout(() => { if (!done) { done = true; setTimedOut(true); } }, 8000);
    window.TaktCloud.listPrograms()
      .then(list => { if (!done) { done = true; clearTimeout(timer); setRows(list); } })
      .catch(err => { if (!done) { done = true; clearTimeout(timer); setError(err.message || 'Kunne ikke hente programmene dine.'); } });
  }, [user, resolved]);

  React.useEffect(() => { refresh(); }, [refresh]);

  const handleOpen = async (id) => {
    setBusyId(id); setError(null);
    try {
      const { id: openedId, program } = await window.TaktCloud.getProgram(id);
      onOpen(openedId, program);
    } catch (err) {
      setError(err.message || 'Kunne ikke åpne programmet.');
    } finally {
      setBusyId(null);
    }
  };

  const handleDelete = async (id, name) => {
    if (!window.confirm(`Slette «${name}»? Dette kan ikke angres.`)) return;
    setBusyId(id); setError(null);
    try {
      await window.TaktCloud.deleteProgram(id);
      refresh();
    } catch (err) {
      setError(err.message || 'Kunne ikke slette programmet.');
    } finally {
      setBusyId(null);
    }
  };

  const handleDuplicate = async (id) => {
    setBusyId(id); setError(null);
    try {
      await window.TaktCloud.duplicateProgram(id, null);
      refresh();
    } catch (err) {
      setError(err.message || 'Kunne ikke duplisere programmet.');
    } finally {
      setBusyId(null);
    }
  };

  if (!resolved) return <div style={{ fontSize:13, color:PC.muted }}>Sjekker innlogging …</div>;
  if (!user) return <AuthScreen compact />;
  if (timedOut) {
    return (
      <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
        <div style={{ fontSize:13, color:PC.z5, fontWeight:600 }}>Dette tar uvanlig lang tid. Sjekk nettforbindelsen og prøv igjen.</div>
        <button onClick={refresh} style={{ ...ghostBtnStyle(false), alignSelf:'flex-start' }}>Prøv igjen</button>
      </div>
    );
  }
  if (rows === null) return <div style={{ fontSize:13, color:PC.muted }}>Laster …</div>;

  const term = search.trim().toLowerCase();
  const visibleRows = term ? rows.filter(r => r.name.toLowerCase().includes(term)) : rows;

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
      {rows.length > 0 && (
        <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Søk etter navn …"
          style={{ padding:'8px 10px', borderRadius:6, border:`1.5px solid ${PC.line2}`, background:PC.bg, fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink, outline:'none', boxSizing:'border-box' }} />
      )}
      {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
      {rows.length === 0 ? (
        <div style={{ fontSize:13, color:PC.muted }}>Du har ingen lagrede programmer i skyen ennå.</div>
      ) : visibleRows.length === 0 ? (
        <div style={{ fontSize:13, color:PC.muted }}>Ingen programmer matcher «{search.trim()}».</div>
      ) : visibleRows.map(row => (
        <div key={row.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'12px 14px', borderRadius:6, border:`1px solid ${PC.line2}` }}>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ display:'flex', alignItems:'center', gap:8 }}>
              <div style={{ fontWeight:700, fontSize:14, color:PC.ink, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{row.name}</div>
              <VisibilityBadge visibility={row.visibility} />
            </div>
            <div style={{ fontSize:11, color:PC.muted, marginTop:2 }}>{fmt(row.totalSec)} · {row.partCount} deler · sist endret {new Date(row.updatedAt).toLocaleDateString('no-NO')}</div>
          </div>
          <button onClick={()=>handleOpen(row.id)} disabled={busyId===row.id}
            style={{ padding:'6px 12px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, textTransform:'uppercase', cursor:'pointer' }}>
            {busyId===row.id ? '…' : 'Åpne'}
          </button>
          <ProgramRowMenu
            onRename={()=>setRowModal({ type:'rename', row })}
            onChangeVisibility={()=>setRowModal({ type:'visibility', row })}
            onShare={()=>setRowModal({ type:'share', row })}
            onDuplicate={()=>handleDuplicate(row.id)}
            onDelete={()=>handleDelete(row.id, row.name)}
          />
        </div>
      ))}

      {rowModal?.type === 'rename' && (
        <RenameModal
          defaultName={rowModal.row.name}
          onClose={()=>setRowModal(null)}
          onSave={async (name) => { await window.TaktCloud.renameProgram(rowModal.row.id, name); refresh(); }}
        />
      )}
      {rowModal?.type === 'visibility' && (
        <VisibilityModal
          row={rowModal.row}
          onClose={()=>setRowModal(null)}
          onSave={async (opts) => { await window.TaktCloud.setVisibility(rowModal.row.id, opts); refresh(); }}
        />
      )}
      {rowModal?.type === 'share' && (
        <ShareModal cloudId={rowModal.row.id} onClose={()=>{ setRowModal(null); refresh(); }} />
      )}
    </div>
  );
}

// ─── ReadOnlyChoiceModal ────────────────────────────────────────────────
// Åpner du et program du ikke eier, skal du aldri havne rett i en editor du
// ikke kan lagre fra. To valg — er allow_copy false, vises kun det første,
// med én linje som forklarer hvorfor (ikke et løfte om at kopiering er
// umulig — bare at knappen ikke tilbys her).
function ReadOnlyChoiceModal({ row, onOpenReadOnly, onMakeCopy, onClose }) {
  const [busy, setBusy] = React.useState(null); // null | 'readonly' | 'copy'
  const [error, setError] = React.useState(null);

  const handle = async (choice, fn) => {
    setBusy(choice); setError(null);
    try { await fn(); }
    catch (err) { setError(err.message || 'Noe gikk galt.'); setBusy(null); }
  };

  return (
    <div style={{ ...modalOverlayStyle, zIndex:220 }} onClick={e=>{if(e.target===e.currentTarget && !busy)onClose();}}>
      <div style={modalBoxStyle(400)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>«{row.name}»</div>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink2, lineHeight:1.5 }}>
          Dette programmet eies av {row.ownerLabel || 'en annen bruker'}. Du kan åpne det skrivebeskyttet, eller lage din egen kopi å redigere fritt.
        </div>
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
          <button onClick={()=>handle('readonly', onOpenReadOnly)} disabled={busy!==null} style={{ ...ghostBtnStyle(busy!==null), width:'100%', boxSizing:'border-box' }}>
            {busy==='readonly' ? 'Åpner …' : 'Åpne skrivebeskyttet'}
          </button>
          {row.allowCopy ? (
            <button onClick={()=>handle('copy', onMakeCopy)} disabled={busy!==null} style={{ ...accentBtnStyle(busy!==null), width:'100%' }}>
              {busy==='copy' ? 'Lager kopi …' : 'Lag min kopi og rediger'}
            </button>
          ) : (
            <div style={{ fontSize:11, color:PC.dim }}>Eieren har ikke tillatt at andre lager sin egen kopi av dette programmet.</div>
          )}
        </div>
        <div style={{ display:'flex', justifyContent:'flex-end' }}>
          <button onClick={onClose} disabled={busy!==null} style={ghostBtnStyle(busy!==null)}>Avbryt</button>
        </div>
      </div>
    </div>
  );
}

// ─── OpenProgramModal — fane «Utforsk» ─────────────────────────────────
// Andre brukeres offentlige programmer. onOpenReadOnly/onCopyProgram mottar
// det ferdig hentede/dupliserte programmet — kallerens ansvar (App) er å
// kjøre det gjennom guard() før det faktisk erstatter den aktive økten.
function ExploreTab({ onOpenReadOnly, onCopyProgram }) {
  const { user, resolved } = useAuthState();
  const [search, setSearch] = React.useState('');
  const [rows, setRows] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [timedOut, setTimedOut] = React.useState(false);
  const [choiceRow, setChoiceRow] = React.useState(null);

  const refresh = React.useCallback(() => {
    if (!resolved || !user) return;
    setError(null); setTimedOut(false); setRows(null);
    let done = false;
    const timer = setTimeout(() => { if (!done) { done = true; setTimedOut(true); } }, 8000);
    window.TaktCloud.listPublicPrograms({ search })
      .then(list => { if (!done) { done = true; clearTimeout(timer); setRows(list); } })
      .catch(err => { if (!done) { done = true; clearTimeout(timer); setError(err.message || 'Kunne ikke hente Utforsk-lista.'); } });
  }, [user, resolved, search]);

  React.useEffect(() => {
    const t = setTimeout(refresh, 300); // debounce søk
    return () => clearTimeout(t);
  }, [refresh]);

  if (!resolved) return <div style={{ fontSize:13, color:PC.muted }}>Sjekker innlogging …</div>;
  if (!user) return <AuthScreen compact />;

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
      <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Søk etter navn …"
        style={{ padding:'8px 10px', borderRadius:6, border:`1.5px solid ${PC.line2}`, background:PC.bg, fontFamily:"'Archivo',sans-serif", fontSize:13, color:PC.ink, outline:'none', boxSizing:'border-box' }} />
      {timedOut ? (
        <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
          <div style={{ fontSize:13, color:PC.z5, fontWeight:600 }}>Dette tar uvanlig lang tid. Sjekk nettforbindelsen og prøv igjen.</div>
          <button onClick={refresh} style={{ ...ghostBtnStyle(false), alignSelf:'flex-start' }}>Prøv igjen</button>
        </div>
      ) : error ? (
        <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>
      ) : rows === null ? (
        <div style={{ fontSize:13, color:PC.muted }}>Laster …</div>
      ) : rows.length === 0 ? (
        <div style={{ fontSize:13, color:PC.muted }}>Ingen offentlige programmer funnet.</div>
      ) : rows.map(row => (
        <button key={row.id} onClick={()=>setChoiceRow(row)} style={{ display:'block', width:'100%', textAlign:'left', padding:'12px 14px', borderRadius:6, border:`1px solid ${PC.line2}`, background:'transparent', cursor:'pointer' }}>
          <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:14, color:PC.ink, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{row.name}</div>
          <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.muted, marginTop:2 }}>{fmt(row.totalSec)} · {row.partCount} deler · av {row.ownerLabel || 'ukjent'} · sist endret {new Date(row.updatedAt).toLocaleDateString('no-NO')}</div>
        </button>
      ))}

      {choiceRow && (
        <ReadOnlyChoiceModal
          row={choiceRow}
          onClose={()=>setChoiceRow(null)}
          onOpenReadOnly={async () => {
            const full = await window.TaktCloud.getProgram(choiceRow.id);
            setChoiceRow(null);
            onOpenReadOnly(full);
          }}
          onMakeCopy={async () => {
            const copy = await window.TaktCloud.duplicateProgram(choiceRow.id, null);
            setChoiceRow(null);
            onCopyProgram(copy);
          }}
        />
      )}
    </div>
  );
}

// ─── OpenProgramModal — fane «Fra fil» ─────────────────────────────────
function FromFileTab({ onImportProgram }) {
  const [error, setError] = React.useState(null);

  const handleFile = (e) => {
    const f = e.target.files[0];
    if (!f) return;
    if (f.size > 512*1024) { setError('Filen er for stor (maks 512 KB).'); e.target.value = ''; return; }
    const r = new FileReader();
    r.onload = ev => {
      let raw;
      try { raw = JSON.parse(ev.target.result); }
      catch { setError('Ugyldig JSON-fil'); return; }
      const result = window.TaktProgram.validateProgram(raw);
      if (!result.ok) { setError('Programmet er ugyldig:\n' + result.errors.join('\n')); return; }
      setError(null);
      onImportProgram(window.TaktProgram.normalizeProgram(result.program));
    };
    r.readAsText(f);
    e.target.value = '';
  };

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
      <div style={{ fontSize:13, color:PC.ink2, lineHeight:1.5 }}>Åpne en tidligere nedlastet .json-fil.</div>
      <input type="file" accept=".json" onChange={handleFile} style={{ fontFamily:"'Archivo',sans-serif", fontSize:13 }} />
      {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600, whiteSpace:'pre-wrap' }}>{error}</div>}
    </div>
  );
}

// ─── OpenProgramModal ───────────────────────────────────────────────────
// Samlet «Åpne …»-dialog: Mine programmer / Utforsk / Fra fil. Selve
// øktbyttet (og guard-sjekken for ulagrede endringer) er kallerens ansvar:
// onOpenCloud/onImportProgram/onOpenForeign/onCopyProgram mottar det ferdig
// hentede/validerte/dupliserte programmet og avgjør selv om — og når — det
// faktisk skal erstatte økten. onOpenForeign setter skrivebeskyttet modus;
// onCopyProgram gir en helt vanlig, redigerbar, egeneid økt.
function OpenProgramModal({ onClose, onOpenCloud, onImportProgram, onOpenForeign, onCopyProgram }) {
  const [tab, setTab] = React.useState('cloud'); // cloud | explore | file

  const tabBtn = (key, label) => (
    <button onClick={()=>setTab(key)} style={{
      padding:'8px 4px', border:'none', background:'none', cursor:'pointer',
      fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em',
      color: tab===key ? PC.ink : PC.muted,
      borderBottom: tab===key ? `2px solid ${PC.accent}` : '2px solid transparent',
    }}>{label}</button>
  );

  return (
    <div style={modalOverlayStyle} onClick={e=>{if(e.target===e.currentTarget)onClose();}}>
      <div style={modalBoxStyle(560)}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Åpne program</div>
        <div style={{ display:'flex', gap:16, borderBottom:`1px solid ${PC.line}` }}>
          {tabBtn('cloud', 'Mine programmer')}
          {tabBtn('explore', 'Utforsk')}
          {tabBtn('file', 'Fra fil')}
        </div>
        <div style={{ maxHeight:'55vh', overflowY:'auto' }}>
          {tab === 'cloud'
            ? <MyProgramsTab onOpen={onOpenCloud} />
            : tab === 'explore'
            ? <ExploreTab onOpenReadOnly={onOpenForeign} onCopyProgram={onCopyProgram} />
            : <FromFileTab onImportProgram={onImportProgram} />}
        </div>
        <div style={{ display:'flex', justifyContent:'flex-end' }}>
          <button onClick={onClose} style={ghostBtnStyle(false)}>Lukk</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { DraftRecoveryModal, UnsavedChangesModal, OpenProgramModal });
