// ─── Doc-kit: edición + adjuntos (4/4) ───────────────────────────────
// useDocEdit/buildDocActions: el patrón de edición in-place (editing/draft/
// enterEdit/discardEdit/saveEdit/patch) que usan las 7 pantallas de documento
// del expediente + Finanzas + Compras/Logística/Inventario. ConfirmDeleteModal:
// modal de confirmación de borrado, reusado por 10+ archivos. FileUploadCard
// (+ sus 3 piezas internas UploadNameModal/FilePreviewModal/AIExtractStrip,
// más AISpark que solo usa AIExtractStrip): tarjeta de archivos adjuntos con
// extracción IA opcional. Extraído de screens-docs.jsx en P4 (FileUploadCard
// + useDocEdit/buildDocActions/ConfirmDeleteModal) y en P7 (UploadNameModal/
// FilePreviewModal/AIExtractStrip/AISpark, H2 de la review P4: vivían en
// screens-docs.jsx sin ser de uso interno a esa pantalla — su ÚNICO
// consumidor real siempre fue FileUploadCard, acá mismo). Expone a window:
// useDocEdit, buildDocActions, ConfirmDeleteModal, FileUploadCard. Consume
// window.DocCard (ui/doc-kit-shell.jsx), window.MicosAIExtract
// (print/ai-extract.js), window.openPrintDoc/MicosDocArchivo
// (print/print-viewer.jsx / print/doc-archivo.js), window.MicosCurrentUser
// (dominio/sesion.js), formatDate (dominio/formato.js).
//
// EXCEPCIÓN AL TECHO DE LÍNEAS (regla nueva de la compuerta, P7 §3): este
// archivo queda muy por encima de las ~250 líneas de referencia (y del techo
// duro de 400) tras sumar las 4 piezas de arriba. Se documenta a propósito en
// vez de partirlo: es UN SOLO TEMA cohesivo — "infraestructura de edición +
// adjuntos de documento" — y la instrucción explícita de la review P4/P7
// (H2) fue consolidar acá, junto a su consumidor real, en vez de dejarlas
// huérfanas en una pantalla que no las usa. Partir esto en un 5º archivo
// doc-kit no lo pidió ninguna spec de este paquete; si a futuro crece más,
// separar "FileUploadCard + su sub-flujo de adjuntos" (UploadNameModal/
// FilePreviewModal/AIExtractStrip/AISpark) de "useDocEdit/buildDocActions/
// ConfirmDeleteModal" (edición genérica, sin relación con archivos) es el
// corte natural.
//
// P9 (review rechazada) — saveEdit admite `onSave` ASYNC: si devuelve `false`
// (ej. la resolución de un FK por publicId contra el backend no encontró nada,
// ver resolveExpedienteIdForSave en screens/compras/combobox.jsx) el guardado se
// aborta y la pantalla se queda en edición con el error visible en el campo, en
// vez de cerrar la edición como si hubiese guardado. Los `onSave` síncronos
// existentes (no devuelven nada) no cambian de comportamiento: `undefined !==
// false` sigue cerrando la edición igual que antes.
//
// P4 — 3 fixes de FileUploadCard (bugs del mapa):
// 1. startEdit(idx) leía `files[idx]` pero el botón "Editar nombre" itera
//    `allFiles` (= files.concat(sessionFiles)) y pasa el índice de allFiles;
//    para cualquier archivo subido EN LA SESIÓN (idx >= files.length),
//    files[idx] era undefined y el guard `if (!f) return` cortaba en
//    silencio. Fix: lee de `allFiles[idx]`.
// 2. confirmUpload() era no-op (solo `setPending(null)`): el nombre tipeado
//    en el modal de "Editar nombre" nunca se aplicaba. Fix: para un archivo
//    de la SESIÓN (idx >= files.length) — el caso normal de uso, ya que las
//    9 instancias reales de <FileUploadCard> usan realUpload — renombra
//    directo en `sessionFiles` (estado propio del componente, sin tocar
//    ningún caller). Para un archivo PERSISTIDO (idx < files.length, solo
//    alcanzable en el modo demo sin realUpload, hoy sin callers reales) se
//    agregó el prop `onRename(idx, nuevoNombre)`, simétrico a `onRemove`,
//    para que un futuro caller con archivos persistidos pueda cablearlo.
// 3. El affordance de "Editar nombre" (lápiz) ya estaba en el render — con
//    los dos fixes de arriba queda funcional de punta a punta para el caso
//    real (archivos de sesión); no se quitó.
//
// DECISIÓN 9/10 (persistencia real de adjuntos + IA honesta) — props NUEVAS, todas
// OPT-IN y retrocompatibles (una <FileUploadCard> que no las pasa se comporta EXACTO
// como antes: archivos solo en memoria):
//   - `persistTarget` + `onUploaderReady`: los archivos de sesión ya no mueren en memoria.
//     El parent captura los pendientes (getPending) y, al GUARDAR el documento padre, los
//     sube al bucket `erp-adjuntos` + registra en `documentos` (window.MicosAdjuntos.subir),
//     y limpia los pendientes (clearPending). Los ya persistidos (prop `files`, filas reales
//     de `documentos`) se ven/borran/renombran de verdad: openFile abre los bytes del bucket
//     (/api/documentos/<id>/download), onRemove/onRename los delega el parent a MicosStore
//     (soft-delete / PATCH del metadato). La descarga del FilePreviewModal usa el mismo
//     endpoint. `subidoPorLabel` resuelve el uuid `subido_por` al nombre real.
//   - `extractDisabled` (+ `extractDisabledReason`): DECISIÓN 10 — en vez de las franjas de
//     extracción por-archivo, muestra UNA tarjeta "Extraer datos con IA" deshabilitada-honesta
//     (patrón P24: gris plano, botón disabled, "Disponible al pasar a producción"). El
//     componente NO se borra, solo se apaga con su motivo.
//
// OJO (archivo COMPARTIDO): screens/docs/{presupuesto,orden-compra,remito}.jsx y las de
// Finanzas/Inventario también consumen FileUploadCard — todos los cambios de acá son aditivos
// para no romperlas.

// ─── Puente ERP → CRM: "Enviar por mail" desde un documento (carril B-erp) ──
// Navega al CRM (/crm, otro entry point) con el composer prellenado por deep-link.
// CONTRATO de query params — lo lee crm/composer.jsx (_crmLeerParamsCompose):
//   compose=1  → fuerza abrir el composer
//   to         → mail de la parte del documento (cliente/comprador)
//   subject    → "<Tipo> <publicId> — <empresa>"  (ej. "Presupuesto PRE-00000148 — Micos S.R.L.")
//   exp        → publicId del expediente (EXP-…), para vincular el hilo
//   doc        → publicId/código del comprobante (PRE-…/A-0001-123/REC-…): el CRM
//                lo matchea contra los comprobantes del expediente y adjunta ESE
//                PDF (lo genera el CRM server-side; el PDF NO viaja en la URL).
// El mail del cliente se saca de los `fields` que las pantallas de documento ya
// arman con buildClienteFields (label 'Email') — sin duplicar la resolución.
function crmEmailFromDocFields(fields) {
  const f = (fields || []).find((x) => x && x.label === 'Email');
  const v = f && f.value != null ? String(f.value).trim() : '';
  return v;
}
function openCrmCompose({ to, subject, exp, doc }) {
  const p = new URLSearchParams({ compose: '1' });
  if (to) p.set('to', to);
  if (subject) p.set('subject', subject);
  if (exp) p.set('exp', exp);
  if (doc) p.set('doc', doc);
  window.location.href = '/crm?' + p.toString();
}
// Devuelve el handler listo para el prop `onEmail` de buildDocActions.
// `tipo` = "Presupuesto"/"Factura"/"Recibo"/… ; `docId` = publicId/código del doc.
function buildDocEmailAction({ tipo, docId, expPublicId, fields }) {
  // Gate por permiso: si el usuario no puede ver el CRM, no ofrecemos el botón
  // (llevaría a "no autorizado"). Mismo criterio que el sidebar/topbar. Devolver
  // null hace que buildDocActions (`onEmail && …`) directamente no lo renderice.
  if (window.MicosPuedeVer && !window.MicosPuedeVer('crm')) return null;
  const to = crmEmailFromDocFields(fields);
  const empresa = (window.COMPANY_CONFIG && window.COMPANY_CONFIG.nombre) || 'Micos S.R.L.';
  const subject = `${tipo}${docId ? ' ' + docId : ''} — ${empresa}`;
  return () => openCrmCompose({ to, subject, exp: expPublicId || '', doc: docId || '' });
}

// ─── Helpers: edit state hook + actions builder ──
function useDocEdit(buildDraft, opts = {}) {
  const startEditing = !!opts.startEditing;
  const [editing, setEditing] = React.useState(startEditing);
  const [draft, setDraft] = React.useState(() => (startEditing ? buildDraft() : null));
  const enterEdit = () => { setDraft(buildDraft()); setEditing(true); };
  const discardEdit = () => {
    setEditing(false);
    setDraft(null);
    if (opts.onCancel) opts.onCancel(); // en modo creación, vuelve al expediente
  };
  const saveEdit = async () => {
    if (opts.onSave) {
      const result = await opts.onSave(draft);
      if (result === false) return; // guardado abortado (error de validación) — sigue en edición
    }
    setEditing(false);
  };
  const patch = (k, v) => setDraft((d) => ({ ...d, [k]: v }));
  return { editing, draft, setDraft, enterEdit, discardEdit, saveEdit, patch };
}

function buildDocActions({ editing, enterEdit, discardEdit, saveEdit, onPrint, onEmail, onDelete, printBlocked, printBlockedTitle }) {
  const printDisabled = printBlocked || !onPrint;
  return editing ? (
    <>
      {onDelete && (
        <button onClick={onDelete} className="btn btn-outline"
          style={{ color: 'var(--destructive)', borderColor: 'color-mix(in oklab, var(--destructive) 50%, var(--border))' }}>
          <IconTrash size={14} />
          <span className="hidden sm:inline">Borrar</span>
        </button>
      )}
      <button onClick={discardEdit} className="btn btn-outline">
        <IconX size={14} />
        <span className="hidden sm:inline">Descartar</span>
      </button>
      <button onClick={saveEdit} className="btn btn-primary">
        <IconCheck size={14} />
        <span className="hidden sm:inline">Guardar</span>
      </button>
    </>
  ) : (
    <>
      <button className="btn btn-outline" onClick={printDisabled ? undefined : onPrint} disabled={printDisabled}
        title={printBlocked ? (printBlockedTitle || 'Pendiente de aprobación en Ajustes de inventario') : (onPrint ? 'Imprimir / Guardar PDF' : 'Disponible próximamente')}>
        <IconPrinter size={14} />
        <span className="hidden sm:inline">Imprimir</span>
      </button>
      {/* Puente ERP→CRM (carril B-erp): abre el correo con este documento adjunto. */}
      {onEmail && (
        <button className="btn btn-outline" onClick={onEmail}
          title="Redactar un correo con este documento adjunto">
          <IconMail size={14} />
          <span className="hidden sm:inline">Enviar por mail</span>
        </button>
      )}
      <button onClick={enterEdit} className="btn btn-primary">
        <IconPencil size={14} />
        <span className="hidden sm:inline">Editar</span>
      </button>
    </>
  );
}

// ─── Reusable: confirm delete modal ──
function ConfirmDeleteModal({
  title = '¿Eliminar elemento?',
  itemName,
  description,
  confirmLabel = 'Sí, eliminar',
  cancelLabel = 'Cancelar',
  onCancel,
  onConfirm,
}) {
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    function onKey(e) {
      if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
      if (e.key === 'Enter') { e.preventDefault(); onConfirm(); }
    }
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onCancel, onConfirm]);

  const node = (
    <div
      className="fixed inset-0 z-[1000] flex items-center justify-center px-4"
      style={{
        background: 'color-mix(in oklab, var(--background) 55%, transparent)',
        backdropFilter: 'blur(14px) saturate(140%)',
        WebkitBackdropFilter: 'blur(14px) saturate(140%)',
        animation: 'fade-in 160ms ease-out',
      }}
      onMouseDown={(e) => { if (e.target === e.currentTarget) onCancel(); }}
    >
      <div
        className="w-full max-w-md rounded-2xl overflow-hidden"
        style={{
          background: 'var(--card)',
          border: '1px solid color-mix(in oklab, var(--border) 75%, transparent)',
          boxShadow: '0 30px 80px -20px rgba(0,0,0,0.45), 0 0 0 1px color-mix(in oklab, var(--destructive) 12%, transparent)',
          animation: 'scale-in 200ms cubic-bezier(.2,.9,.3,1.2)',
        }}
      >
        <div className="px-6 pt-6 pb-4">
          <p className="text-[10.5px] font-bold uppercase tracking-[0.22em] mb-2" style={{ color: 'var(--destructive)' }}>
            Eliminar
          </p>
          <p className="text-[18px] font-extrabold leading-tight" style={{ color: 'var(--foreground)' }}>
            {title}
          </p>
          <p className="text-[12.5px] mt-1.5 leading-snug" style={{ color: 'var(--muted-foreground)' }}>
            {description || 'Esta acción no se puede deshacer.'}
          </p>
        </div>

        {itemName && (
          <div className="px-6 pb-6">
            <div className="flex items-center gap-3 px-4 py-3.5 rounded-xl"
              style={{
                background: 'color-mix(in oklab, var(--destructive) 7%, transparent)',
                border: '1px solid color-mix(in oklab, var(--destructive) 25%, transparent)',
              }}
            >
              <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
                style={{
                  width: 38, height: 38,
                  background: 'color-mix(in oklab, var(--destructive) 14%, transparent)',
                  color: 'var(--destructive)',
                }}>
                <IconTrash size={17} />
              </span>
              <div className="min-w-0 flex-1">
                <p className="text-[10.5px] font-bold uppercase tracking-[0.18em]" style={{ color: 'var(--destructive)' }}>
                  Se va a eliminar
                </p>
                <p className="text-[13px] font-extrabold truncate mt-0.5" style={{ color: 'var(--foreground)' }}>
                  {itemName}
                </p>
              </div>
            </div>
          </div>
        )}

        <div className="flex items-center justify-end gap-2 px-6 py-4"
          style={{
            background: 'color-mix(in oklab, var(--surface) 40%, transparent)',
            borderTop: '1px solid color-mix(in oklab, var(--border) 50%, transparent)',
          }}
        >
          <button type="button" onClick={onCancel} className="btn btn-outline">
            <IconX size={14} />
            <span>{cancelLabel}</span>
          </button>
          <button
            type="button"
            onClick={onConfirm}
            className="btn"
            style={{
              background: 'var(--destructive)',
              color: 'white',
              border: '1px solid var(--destructive)',
            }}
            onMouseEnter={(e) => { e.currentTarget.style.filter = 'brightness(0.92)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.filter = 'none'; }}
          >
            <IconTrash size={14} />
            <span>{confirmLabel}</span>
          </button>
        </div>
      </div>
    </div>
  );

  return ReactDOM.createPortal(node, document.body);
}

// ─── Sub-pieza de FileUploadCard: modal de nombre (subir/editar) ──
// Movido de screens-docs.jsx en P7 (H2 de la review P4) — su único consumidor
// siempre fue FileUploadCard, acá mismo.
function UploadNameModal({ pending, mode = 'create', onChange, onCancel, onConfirm, inputRef }) {
  // Lock body scroll while modal is open + close on Escape
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    function onKey(e) {
      if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
    }
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onCancel]);

  const isEdit = mode === 'edit';

  const node = (
    <div
      className="fixed inset-0 z-[1000] flex items-center justify-center px-4"
      style={{
        background: 'color-mix(in oklab, var(--background) 55%, transparent)',
        backdropFilter: 'blur(14px) saturate(140%)',
        WebkitBackdropFilter: 'blur(14px) saturate(140%)',
        animation: 'fade-in 160ms ease-out',
      }}
      onMouseDown={(e) => { if (e.target === e.currentTarget) onCancel(); }}
    >
      <div
        className="w-full max-w-md rounded-2xl overflow-hidden"
        style={{
          background: 'var(--card)',
          border: '1px solid color-mix(in oklab, var(--border) 75%, transparent)',
          boxShadow: '0 30px 80px -20px rgba(0,0,0,0.45), 0 0 0 1px color-mix(in oklab, var(--accent) 10%, transparent)',
          animation: 'scale-in 200ms cubic-bezier(.2,.9,.3,1.2)',
        }}
      >
        <div className="px-6 pt-6 pb-4">
          <p className="text-[10.5px] font-bold uppercase tracking-[0.22em] mb-2" style={{ color: 'var(--accent)' }}>
            {isEdit ? 'Editar archivo' : 'Subir archivo'}
          </p>
          <p className="text-[18px] font-extrabold leading-tight" style={{ color: 'var(--foreground)' }}>
            {isEdit ? 'Cambiar el nombre del archivo' : 'Agregar un archivo al expediente'}
          </p>
          <p className="text-[12.5px] mt-1.5" style={{ color: 'var(--muted-foreground)' }}>
            {isEdit
              ? 'Actualizá la información del archivo adjunto.'
              : 'Arrastrá un archivo o seleccioná uno desde tu equipo.'}
          </p>
        </div>

        <div className="px-6 pb-4">
          {isEdit ? (
            <div className="flex items-center gap-3 px-4 py-3.5 rounded-xl"
              style={{
                background: 'color-mix(in oklab, var(--surface) 55%, transparent)',
                border: '1px solid color-mix(in oklab, var(--border) 50%, transparent)',
              }}
            >
              <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
                style={{
                  width: 38, height: 38,
                  background: 'color-mix(in oklab, var(--accent) 14%, transparent)',
                  color: 'var(--accent)',
                }}>
                <IconFileText size={18} />
              </span>
              <div className="min-w-0 flex-1">
                <p className="text-[12.5px] font-mono font-bold truncate" style={{ color: 'var(--foreground)' }}>
                  {pending.fileName || pending.originalName || 'archivo.pdf'}
                </p>
                <p className="text-[11px] tabular-nums" style={{ color: 'var(--muted-foreground)' }}>
                  {pending.tamano || '—'}
                </p>
              </div>
            </div>
          ) : (
            <button
              type="button"
              className="file-drop w-full flex flex-col items-center justify-center gap-2 px-6 py-8 rounded-xl text-center transition-all cursor-pointer"
            >
              <span className="inline-flex items-center justify-center rounded-xl"
                style={{
                  width: 48, height: 48,
                  background: 'color-mix(in oklab, var(--accent) 12%, transparent)',
                  color: 'var(--accent)',
                }}>
                <IconUploadCloud size={22} />
              </span>
              <p className="text-[13.5px] font-bold">
                <span style={{ color: 'var(--accent)' }}>Hacé click para elegir</span>
                <span style={{ color: 'var(--muted-foreground)' }}> o arrastrá un archivo</span>
              </p>
              <p className="text-[11.5px]" style={{ color: 'var(--muted-foreground)' }}>
                PDF, JPG, PNG · máx. 10 MB
              </p>
            </button>
          )}
        </div>

        <div className="px-6 pb-6">
          <label className="block text-[10.5px] font-bold uppercase tracking-[0.2em] mb-1.5" style={{ color: 'var(--muted-foreground)' }}>
            Nombre del archivo
          </label>
          <input
            ref={inputRef}
            value={pending.name}
            onChange={(e) => onChange(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter') { e.preventDefault(); if (pending.name.trim()) onConfirm(); }
            }}
            placeholder="Ej. Orden de compra firmada — Hospital Italiano"
            className="edit-input w-full text-[14px] font-bold"
            style={{ color: 'var(--foreground)' }}
            autoFocus
          />
          <p className="text-[11px] mt-2" style={{ color: 'var(--muted-foreground)' }}>
            Tip: usá un nombre descriptivo para encontrarlo rápido.
          </p>
        </div>

        <div className="flex items-center justify-end gap-2 px-6 py-4"
          style={{
            background: 'color-mix(in oklab, var(--surface) 40%, transparent)',
            borderTop: '1px solid color-mix(in oklab, var(--border) 50%, transparent)',
          }}
        >
          <button type="button" onClick={onCancel} className="btn btn-outline">
            <IconX size={14} />
            <span>Cancelar</span>
          </button>
          <button
            type="button"
            onClick={onConfirm}
            disabled={!pending.name.trim()}
            className="btn btn-primary"
            style={!pending.name.trim() ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}
          >
            <IconCheck size={14} />
            <span>{isEdit ? 'Guardar cambios' : 'Subir archivo'}</span>
          </button>
        </div>
      </div>
    </div>
  );

  return ReactDOM.createPortal(node, document.body);
}

// "Subido por": una fila persistida trae el uuid del usuario (FK); lo resolvemos al nombre
// real (window.USUARIOS) vía MicosAdjuntos. Un archivo de sesión ya trae el nombre → tal cual.
function subidoPorLabel(v) {
  return (window.MicosAdjuntos && window.MicosAdjuntos.nombreUsuario) ? window.MicosAdjuntos.nombreUsuario(v) : (v || '—');
}

// ─── Sub-pieza de FileUploadCard: modal de vista previa ──
// Movido de screens-docs.jsx en P7 (H2 de la review P4). Casi-muerto en la
// práctica (solo se renderiza como fallback en FileUploadCard.openFile cuando
// `window.openPrintDoc && window.MicosDocArchivo` es falso, condición
// sistemáticamente falsa hoy porque Micos ERP.html siempre carga
// print/print-viewer.jsx + print/doc-archivo.js) — se conserva como fallback honesto, no
// se borra.
function FilePreviewModal({ file, onClose }) {
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); onClose(); } }
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onClose]);

  // Descarga real: prioridad al blob de sesión (`_url`) o una `url`/`href` explícita; si el
  // archivo YA está persistido (fila de `documentos` con `id` + `storagePath`) se baja del
  // bucket vía /api/documentos/<id>/download?dl=1 (DECISIÓN 9). Solo queda sin fuente un
  // metadato sin bytes (caso legacy improbable): ahí el botón queda deshabilitado, honesto.
  const descargaSrc = (file && (file._url || file.url || file.href))
    || (file && file.id && file.storagePath && window.MicosAdjuntos ? window.MicosAdjuntos.descargarHref(file) : null)
    || null;
  const descargar = () => {
    if (!descargaSrc) return;
    const a = document.createElement('a');
    a.href = descargaSrc;
    a.download = file.nombre || 'documento';
    a.rel = 'noopener';
    document.body.appendChild(a);
    a.click();
    a.remove();
  };

  const node = (
    <div
      className="fixed inset-0 z-[1000] flex items-center justify-center px-4 py-6"
      style={{
        background: 'color-mix(in oklab, var(--background) 60%, transparent)',
        backdropFilter: 'blur(14px) saturate(140%)',
        WebkitBackdropFilter: 'blur(14px) saturate(140%)',
        animation: 'fade-in 160ms ease-out',
      }}
      onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div
        className="w-full max-w-3xl rounded-2xl overflow-hidden flex flex-col"
        style={{
          background: 'var(--card)',
          border: '1px solid color-mix(in oklab, var(--border) 75%, transparent)',
          boxShadow: '0 30px 80px -20px rgba(0,0,0,0.5)',
          animation: 'scale-in 200ms cubic-bezier(.2,.9,.3,1.2)',
          maxHeight: 'calc(100vh - 48px)',
        }}
      >
        {/* Header */}
        <div className="flex items-center gap-3 px-5 py-4"
          style={{ borderBottom: '1px solid color-mix(in oklab, var(--border) 50%, transparent)' }}
        >
          <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
            style={{
              width: 36, height: 36,
              background: 'color-mix(in oklab, var(--accent) 14%, transparent)',
              color: 'var(--accent)',
            }}>
            <IconFileText size={17} />
          </span>
          <div className="min-w-0 flex-1">
            <p className="text-[13.5px] font-extrabold truncate" style={{ color: 'var(--foreground)' }}>
              {file.nombre}
            </p>
            <p className="text-[11px] tabular-nums" style={{ color: 'var(--muted-foreground)' }}>
              {file.tamano || '—'}
              {file.subidoPor && (<><span className="mx-1.5 opacity-50">·</span>Subido por {subidoPorLabel(file.subidoPor)}</>)}
              {file.subidoAt && (<><span className="mx-1.5 opacity-50">·</span>{formatDate(file.subidoAt)}</>)}
            </p>
          </div>
          <button
            type="button"
            onClick={descargar}
            disabled={!descargaSrc}
            aria-disabled={!descargaSrc}
            className={`inline-flex items-center justify-center rounded-md transition-colors ${descargaSrc ? 'cursor-pointer' : 'is-disabled'}`}
            style={{ width: 32, height: 32, color: descargaSrc ? 'var(--muted-foreground)' : 'color-mix(in oklab, var(--muted-foreground) 45%, transparent)' }}
            onMouseEnter={descargaSrc ? (e) => { e.currentTarget.style.color = 'var(--accent)'; e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 10%, transparent)'; } : undefined}
            onMouseLeave={descargaSrc ? (e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'transparent'; } : undefined}
            title={descargaSrc ? 'Descargar' : 'Sin archivo descargable (solo metadatos)'}
          >
            <IconDownload size={15} />
          </button>
          <button
            type="button"
            onClick={onClose}
            className="inline-flex items-center justify-center rounded-md cursor-pointer transition-colors"
            style={{ width: 32, height: 32, color: 'var(--muted-foreground)' }}
            onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--foreground)'; e.currentTarget.style.background = 'color-mix(in oklab, var(--surface) 60%, transparent)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'transparent'; }}
            title="Cerrar"
          >
            <IconX size={16} />
          </button>
        </div>

        {/* Body — faux document preview */}
        <div className="flex-1 overflow-auto px-6 py-8"
          style={{ background: 'color-mix(in oklab, var(--background) 65%, transparent)' }}
        >
          <div className="mx-auto rounded-md"
            style={{
              maxWidth: 560,
              background: '#ffffff',
              color: '#1a1a1a',
              boxShadow: '0 24px 60px -20px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.06)',
              aspectRatio: '1 / 1.294', /* A4-ish */
              padding: '44px 44px',
              fontFamily: 'inherit',
            }}
          >
            {/* Page header */}
            <div className="flex items-start justify-between" style={{ marginBottom: 28 }}>
              <div>
                <div style={{
                  width: 110, height: 18, borderRadius: 3,
                  background: 'linear-gradient(90deg, #d9d9d9 0%, #ececec 100%)',
                }} />
                <div style={{
                  marginTop: 8, width: 78, height: 8, borderRadius: 2,
                  background: '#ececec',
                }} />
              </div>
              <div className="text-right">
                <p style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#888' }}>
                  Documento
                </p>
                <p style={{ marginTop: 4, fontSize: 13, fontWeight: 800, color: '#1a1a1a', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' }}>
                  {(file.nombre || '').replace(/\.[^.]+$/, '').slice(0, 24)}
                </p>
              </div>
            </div>

            <div style={{ height: 1, background: '#ececec', marginBottom: 24 }} />

            {/* Title block */}
            <p style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#888' }}>
              {file.nombre && file.nombre.toLowerCase().includes('oc') ? 'Orden de Compra' : 'Documento adjunto'}
            </p>
            <p style={{ marginTop: 6, fontSize: 20, fontWeight: 800, lineHeight: 1.15, color: '#1a1a1a', letterSpacing: '-0.01em' }}>
              {file.nombre}
            </p>

            {/* Meta strip */}
            <div style={{
              marginTop: 22, padding: '12px 14px', borderRadius: 6,
              background: '#f5f5f5',
              display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12,
            }}>
              {[
                ['Subido por', subidoPorLabel(file.subidoPor)],
                ['Fecha', file.subidoAt ? formatDate(file.subidoAt) : '—'],
                ['Tamaño', file.tamano || '—'],
              ].map(([label, val], k) => (
                <div key={k}>
                  <p style={{ fontSize: 8.5, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#999' }}>{label}</p>
                  <p style={{ marginTop: 3, fontSize: 11, fontWeight: 700, color: '#222' }}>{val}</p>
                </div>
              ))}
            </div>

            {/* Body text lines */}
            <div style={{ marginTop: 28 }}>
              {[100, 96, 88, 94, 70].map((w, k) => (
                <div key={k} style={{
                  height: 8, borderRadius: 2, marginBottom: 10,
                  background: k === 4 ? '#f0f0f0' : '#ececec',
                  width: w + '%',
                }} />
              ))}
            </div>

            <div style={{ marginTop: 22 }}>
              {[92, 100, 78].map((w, k) => (
                <div key={k} style={{
                  height: 8, borderRadius: 2, marginBottom: 10,
                  background: '#ececec', width: w + '%',
                }} />
              ))}
            </div>

            {/* Faux table */}
            <div style={{
              marginTop: 26, borderRadius: 6, overflow: 'hidden', border: '1px solid #ececec',
            }}>
              <div style={{ display: 'grid', gridTemplateColumns: '1.6fr 0.7fr 1fr', background: '#f5f5f5', padding: '8px 12px' }}>
                {['Descripción', 'Cant.', 'Importe'].map((h) => (
                  <p key={h} style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#888' }}>{h}</p>
                ))}
              </div>
              {[0, 1, 2].map((r) => (
                <div key={r} style={{
                  display: 'grid', gridTemplateColumns: '1.6fr 0.7fr 1fr',
                  padding: '11px 12px',
                  borderTop: r === 0 ? 'none' : '1px solid #f0f0f0',
                  alignItems: 'center',
                }}>
                  <div style={{ height: 8, borderRadius: 2, background: '#ececec', width: ['78%', '62%', '70%'][r] }} />
                  <div style={{ height: 8, borderRadius: 2, background: '#ececec', width: '40%' }} />
                  <div style={{ height: 8, borderRadius: 2, background: '#ececec', width: '60%' }} />
                </div>
              ))}
            </div>

            {/* Footer */}
            <div style={{ marginTop: 32, paddingTop: 14, borderTop: '1px solid #ececec', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <p style={{ fontSize: 9, color: '#aaa', letterSpacing: '0.1em', textTransform: 'uppercase' }}>
                Vista previa
              </p>
              <p style={{ fontSize: 9, color: '#aaa', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' }}>
                1 / 1
              </p>
            </div>
          </div>

          <p className="text-center text-[11px] mt-5" style={{ color: 'var(--muted-foreground)' }}>
            Vista previa simulada — el contenido real del archivo se renderiza al integrar el visor.
          </p>
        </div>
      </div>
    </div>
  );

  return ReactDOM.createPortal(node, document.body);
}

// Sparkle (4 puntas) para acciones de IA — solo lo usa AIExtractStrip, acá mismo.
// Movido de screens-docs.jsx en P7 (H2 de la review P4).
function AISpark({ size = 14 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}>
      <path d="M12 1.5l1.7 7.3 7.3 1.7-7.3 1.7L12 19.5l-1.7-7.3L3 10.5l7.3-1.7z" />
    </svg>
  );
}

// Franja "Autocompletar con IA" por archivo (estados: idle / loading / done).
// Movido de screens-docs.jsx en P7 (H2 de la review P4) — su único consumidor
// siempre fue FileUploadCard, acá mismo.
function AIExtractStrip({ state, onRun, count, error, warning }) {
  const topBorder = '1px solid color-mix(in oklab, var(--border) 50%, transparent)';
  if (state === 'loading') {
    return (
      <div style={{ padding: '10px 14px', borderTop: topBorder, display: 'flex', flexDirection: 'column', gap: 8 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span className="ai-spin" style={{ color: 'var(--accent)', display: 'inline-flex' }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <line x1="12" y1="2" x2="12" y2="6" /><line x1="12" y1="18" x2="12" y2="22" /><line x1="4.9" y1="4.9" x2="7.8" y2="7.8" /><line x1="16.2" y1="16.2" x2="19.1" y2="19.1" /><line x1="2" y1="12" x2="6" y2="12" /><line x1="18" y1="12" x2="22" y2="12" /><line x1="4.9" y1="19.1" x2="7.8" y2="16.2" /><line x1="16.2" y1="7.8" x2="19.1" y2="4.9" />
            </svg>
          </span>
          <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--accent)' }}>Analizando documento…</span>
        </div>
        <span style={{ display: 'block', height: 4, borderRadius: 999, background: 'color-mix(in oklab, var(--accent) 18%, transparent)', overflow: 'hidden' }}>
          <span className="ai-prog" style={{ display: 'block', height: '100%', borderRadius: 999, background: 'var(--accent)' }} />
        </span>
      </div>
    );
  }
  if (state === 'done') {
    if (warning) {
      return (
        <div style={{ padding: '10px 14px', borderTop: '1px solid color-mix(in oklab, var(--warning-soft) 35%, var(--border))', background: 'color-mix(in oklab, var(--warning-soft) 10%, transparent)', display: 'flex', alignItems: 'flex-start', gap: 8 }}>
          <span style={{ color: 'var(--warning-soft)', display: 'inline-flex', flexShrink: 0, marginTop: 1 }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /><line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /></svg>
          </span>
          <span style={{ flex: 1, minWidth: 0, fontSize: 11.5, fontWeight: 600, color: '#fbbf24' }}>{warning}</span>
          <button type="button" onClick={onRun} className="ai-extract-link" title="Reintentar">Reintentar</button>
        </div>
      );
    }
    return (
      <div style={{ padding: '10px 14px', borderTop: '1px solid color-mix(in oklab, #10b981 30%, var(--border))', background: 'color-mix(in oklab, #10b981 8%, transparent)', display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ color: '#34d399', display: 'inline-flex' }}><IconCheck size={15} /></span>
        <span key={count} className="animate-name-pop" style={{ fontSize: 12, fontWeight: 700, color: '#34d399' }}>{(count || 0)} {count === 1 ? 'campo detectado' : 'campos detectados'} · revisá antes de guardar</span>
      </div>
    );
  }
  if (state === 'error') {
    return (
      <div style={{ padding: '10px 14px', borderTop: '1px solid color-mix(in oklab, var(--destructive) 30%, var(--border))', background: 'color-mix(in oklab, var(--destructive) 8%, transparent)', display: 'flex', alignItems: 'center', gap: 10 }}>
        <span style={{ color: 'var(--destructive)', display: 'inline-flex', flexShrink: 0 }}><IconX size={15} /></span>
        <span style={{ flex: 1, minWidth: 0, fontSize: 11.5, fontWeight: 600, color: 'var(--destructive)' }}>{error || 'No se pudo leer el documento'}</span>
        <button type="button" onClick={onRun} className="ai-extract-link" title="Reintentar">Reintentar</button>
      </div>
    );
  }
  return (
    <div style={{ padding: '10px 14px', borderTop: topBorder, display: 'flex', alignItems: 'center', gap: 12 }}>
      <span className="inline-flex items-center justify-center flex-shrink-0" style={{ width: 30, height: 30, borderRadius: 8, background: 'color-mix(in oklab, var(--accent) 14%, transparent)', color: 'var(--accent)' }}>
        <AISpark size={15} />
      </span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <p style={{ margin: 0, fontSize: 12.5, fontWeight: 700, color: 'var(--foreground)' }}>Autocompletar con IA</p>
        <p style={{ margin: '1px 0 0', fontSize: 11, color: 'var(--muted-foreground)' }}>Detecta fechas, ítems y montos del PDF · solo PDF con texto por ahora (sin fotos ni escaneos)</p>
      </div>
      <button type="button" onClick={onRun} className="ai-extract-link" title="Detectar fechas, ítems y montos del PDF (con texto)">
        Extraer datos <IconChevronRight size={14} />
      </button>
    </div>
  );
}

// DECISIÓN 10 — "Extraer datos IA" deshabilitado HONESTO (patrón P24): no se borra el
// componente, se muestra la promesa apagada con su motivo (gris plano, botón disabled, sin
// feedback). Reemplaza a las franjas por-archivo cuando `extractDisabled` (doc. médica/extra):
// la extracción real necesita el modelo de visión de producción, que en el ERP exportado no
// está. Una sola tarjeta a nivel documento (no por archivo) para que se vea siempre.
function AIExtractDisabledNotice({ reason = 'Disponible al pasar a producción' }) {
  return (
    <div className="mt-4" style={{
      padding: '10px 14px', borderRadius: 10,
      border: '1px dashed color-mix(in oklab, var(--border) 70%, transparent)',
      background: 'color-mix(in oklab, var(--muted) 30%, transparent)',
      display: 'flex', alignItems: 'center', gap: 12,
    }}>
      <span className="inline-flex items-center justify-center flex-shrink-0" style={{
        width: 30, height: 30, borderRadius: 8,
        background: 'color-mix(in oklab, var(--muted) 55%, transparent)', color: 'var(--muted-foreground)',
      }}>
        <AISpark size={15} />
      </span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <p style={{ margin: 0, fontSize: 12.5, fontWeight: 700, color: 'var(--muted-foreground)' }}>Extraer datos con IA</p>
        <p style={{ margin: '1px 0 0', fontSize: 11, color: 'var(--muted-foreground)' }}>{reason}</p>
      </div>
      <button type="button" className="btn is-disabled" disabled aria-disabled="true"
        title={reason} style={{ pointerEvents: 'none' }}>
        Extraer datos
      </button>
    </div>
  );
}

function FileUploadCard({
  eyebrow = 'Documentos adjuntos',
  title = 'Archivos del documento',
  description = 'Adjuntá la orden de compra firmada que envió el cliente.',
  files = [],
  editing = false,
  onUpload, onRemove, onRename,
  extractable = false,  // si true: cada archivo (PDF/imagen) muestra la franja "Autocompletar con IA"
  extractDisabled = false,        // DECISIÓN 10: muestra la extracción IA deshabilitada-honesta (P24) en vez de las franjas activas
  extractDisabledReason = 'Disponible al pasar a producción',
  onExtract,            // (fileIndex) => void — hook opcional al disparar la extracción
  realUpload = false,   // si true: "Subir archivo" abre el selector real del SO y la extracción lee el archivo de verdad (en sesión)
  onExtracted,          // (data) => void — recibe los campos extraídos del archivo para autocompletar
  extractor,            // (file) => Promise<{data,campos}> — extractor IA a usar (default: orden de compra)
  persistTarget,        // DECISIÓN 9: { tipo, expedienteId? , compraId? } — destino de persistencia de los archivos de sesión
  onUploaderReady,      // (api:{getPending,clearPending}) => void — el parent captura los archivos de sesión para subirlos al guardar
}) {
  // Modals: create/edit upload, delete confirm, preview.
  const [pending, setPending] = React.useState(null); // { mode, name, fileName?, tamano?, fileIndex? }
  const [confirmIdx, setConfirmIdx] = React.useState(null);
  const [previewIdx, setPreviewIdx] = React.useState(null);
  const [exState, setExState] = React.useState({}); // { [fileIndex]: 'loading' | 'done' | 'error' }
  const [exMeta, setExMeta] = React.useState({});    // { [fileIndex]: { count, error } }
  const [sessionFiles, setSessionFiles] = React.useState([]); // archivos reales subidos en la sesión (aún sin persistir)
  const nameInputRef = React.useRef(null);
  const fileInputRef = React.useRef(null);

  // Lista mostrada = persistidos (filas reales de `documentos`) + subidos en sesión (aún en memoria).
  const allFiles = files.concat(sessionFiles);

  // DECISIÓN 9 — el parent captura los archivos de sesión para subirlos al bucket al GUARDAR
  // el documento padre (no al elegirlos: así "Descartar" no deja adjuntos huérfanos, y el alta
  // de compra puede resolver el id real recién al crearse). `getPending` lee el ref (siempre el
  // valor vivo), `clearPending` los vacía tras subirlos. Backward-compatible: sin
  // `onUploaderReady`, la card sigue guardando en memoria como antes.
  const sessionFilesRef = React.useRef(sessionFiles);
  React.useEffect(() => { sessionFilesRef.current = sessionFiles; }, [sessionFiles]);
  React.useEffect(() => {
    if (!onUploaderReady) return;
    onUploaderReady({
      getPending: () => sessionFilesRef.current.slice(),
      clearPending: () => setSessionFiles([]),
    });
  }, [onUploaderReady]);

  // Extracción REAL (solo archivos subidos en sesión, con realUpload): lee el
  // archivo y completa el formulario vía onExtracted. Si no hay archivo real,
  // cae a la demo (estado done con conteo fijo).
  function runExtract(idx) {
    if (exState[idx] === 'loading') return;
    const f = allFiles[idx];
    setExState((s) => ({ ...s, [idx]: 'loading' }));
    setExMeta((m) => ({ ...m, [idx]: {} }));
    if (onExtract) onExtract(idx);
    if (realUpload && f && f._file && window.MicosAIExtract) {
      const runIA = extractor || window.MicosAIExtract.ordenCompra;
      runIA(f._file).then((res) => {
        if (onExtracted) onExtracted(res.data);
        setExMeta((m) => ({ ...m, [idx]: { count: res.campos, warning: onExtracted ? (res.warning || '') : '' } }));
        setExState((s) => ({ ...s, [idx]: 'done' }));
      }).catch((err) => {
        setExMeta((m) => ({ ...m, [idx]: { error: (err && err.message) || 'No se pudo leer el documento' } }));
        setExState((s) => ({ ...s, [idx]: 'error' }));
      });
      return;
    }
    // Demo (archivo no real): mantiene el comportamiento anterior.
    setTimeout(() => { setExMeta((m) => ({ ...m, [idx]: { count: 6 } })); setExState((s) => ({ ...s, [idx]: 'done' })); }, 1700);
  }

  function startUpload() {
    if (realUpload) { if (fileInputRef.current) fileInputRef.current.click(); return; }
    setPending({ mode: 'create', name: '' });
    if (onUpload) onUpload();
    setTimeout(() => nameInputRef.current && nameInputRef.current.focus(), 0);
  }

  // Selección de archivo real (en sesión): se guarda el File + un blob URL.
  function onFilePicked(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = ''; // permite re-subir el mismo archivo
    if (!file) return;
    const kb = file.size / 1024;
    const tamano = kb >= 1024 ? (kb / 1024).toFixed(1) + ' MB' : Math.max(1, Math.round(kb)) + ' KB';
    const entry = {
      nombre: file.name,
      tamano: tamano,
      subidoPor: (window.MicosCurrentUser && window.MicosCurrentUser() && window.MicosCurrentUser().name) || 'Vos',
      subidoAt: new Date().toISOString(),
      _file: file,
      _url: URL.createObjectURL(file),
    };
    setSessionFiles((arr) => [...arr, entry]);
  }

  // Fix P4 (bug del mapa): leía `files[idx]`, pero "Editar nombre" itera
  // `allFiles` y pasa SU índice — para un archivo de sesión (idx >= files.length)
  // eso devolvía undefined y cortaba en el guard sin hacer nada.
  function startEdit(idx) {
    const f = allFiles[idx];
    if (!f) return;
    setPending({
      mode: 'edit',
      fileIndex: idx,
      fileName: f.nombre,
      tamano: f.tamano,
      name: f.nombre,
    });
    setTimeout(() => nameInputRef.current && nameInputRef.current.select(), 0);
  }

  // Fix P4 (bug del mapa): era no-op (`setPending(null)` a secas) — el nombre
  // tipeado en el modal nunca se aplicaba. Ahora, en modo edición, renombra de
  // verdad: si el índice cae en `sessionFiles` (caso normal de uso: las 9
  // instancias reales de FileUploadCard usan realUpload) lo hace acá mismo;
  // si cae en `files` (persistido) delega a `onRename`, si el caller lo pasa.
  function confirmUpload() {
    if (pending && pending.mode === 'edit') {
      const nuevoNombre = pending.name.trim();
      if (nuevoNombre) {
        const idx = pending.fileIndex;
        if (idx >= files.length) {
          const si = idx - files.length;
          setSessionFiles((arr) => arr.map((f, j) => (j === si ? { ...f, nombre: nuevoNombre } : f)));
        } else if (onRename) {
          onRename(idx, nuevoNombre);
        }
      }
    }
    setPending(null);
  }

  // "Ver archivo": abre el MISMO visor de impresión que presupuesto/remito/etc
  // (overlay con barra superior, zoom, imprimir, guardar PDF). Fallback al modal
  // propio si el renderer genérico no está cargado en esta página.
  function openFile(i) {
    const f = allFiles[i];
    if (!f) return;
    // Archivo real subido en sesión: abrir el MISMO visor (overlay) mostrando el
    // contenido real (imagen o PDF), con la misma experiencia que el de ejemplo.
    if (f._url) {
      const esPdf = /\.pdf$/i.test(f.nombre || '') || (f._file && f._file.type === 'application/pdf');
      const meta = [f.tamano, f.subidoPor && ('Subido por ' + subidoPorLabel(f.subidoPor)), f.subidoAt && formatDate(f.subidoAt)]
        .filter(Boolean).join('  ·  ');
      if (window.openPrintDoc) {
        window.openPrintDoc(null, { realUrl: f._url, realKind: esPdf ? 'pdf' : 'image', title: f.nombre, subtitle: meta });
      } else {
        try { window.open(f._url, '_blank'); } catch (e) {}
      }
      return;
    }
    // DECISIÓN 9 — adjunto YA persistido (fila de `documentos` con id + storagePath): abrir el
    // MISMO visor con los bytes reales del bucket (inline), vía /api/documentos/<id>/download.
    if (f.id && f.storagePath && window.MicosAdjuntos) {
      const esPdf = /pdf/i.test(f.mime || '') || /\.pdf$/i.test(f.nombre || '');
      const meta = [f.tamano, f.subidoPor && ('Subido por ' + subidoPorLabel(f.subidoPor)), f.subidoAt && formatDate(f.subidoAt)]
        .filter(Boolean).join('  ·  ');
      const url = window.MicosAdjuntos.verHref(f);
      if (window.openPrintDoc) {
        window.openPrintDoc(null, { realUrl: url, realKind: esPdf ? 'pdf' : 'image', title: f.nombre, subtitle: meta });
      } else {
        try { window.open(url, '_blank'); } catch (e) {}
      }
      return;
    }
    if (window.openPrintDoc && window.MicosDocArchivo) {
      const meta = [f.tamano, f.subidoPor && ('Subido por ' + f.subidoPor), f.subidoAt && formatDate(f.subidoAt)]
        .filter(Boolean).join('  ·  ');
      window.openPrintDoc(f, { mod: window.MicosDocArchivo, title: f.nombre, subtitle: meta });
    } else {
      setPreviewIdx(i);
    }
  }

  return (
    <DocCard editing={editing}>
      <div className="flex flex-wrap items-start justify-between gap-3 mb-5">
        <div className="min-w-0">
          <p className="text-[11px] font-bold uppercase tracking-[0.2em]" style={{ color: 'var(--accent)' }}>
            {eyebrow}
          </p>
          <p className="text-[15px] font-extrabold mt-1.5" style={{ color: 'var(--foreground)' }}>
            {title}
          </p>
          <p className="text-[12.5px] mt-1" style={{ color: 'var(--muted-foreground)' }}>
            {description}
          </p>
        </div>
        <p className="text-[11.5px] font-bold uppercase tracking-[0.1em]" style={{ color: 'var(--muted-foreground)' }}>
          {allFiles.length} {allFiles.length === 1 ? 'archivo' : 'archivos'}
        </p>
      </div>

      {/* Input real oculto (solo realUpload) */}
      {realUpload && (
        <input ref={fileInputRef} type="file" accept=".pdf,image/*" onChange={onFilePicked} style={{ display: 'none' }} />
      )}

      {/* Trigger row — subir archivo (la extracción IA vive por archivo, abajo) */}
      <button
        onClick={startUpload}
        className={`file-drop w-full flex items-center justify-center gap-3 px-5 py-4 rounded-xl text-center transition-all cursor-pointer${allFiles.length > 0 ? ' mb-4' : ''}`}
        type="button"
      >
        <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
          style={{
            width: 32, height: 32,
            background: 'color-mix(in oklab, var(--accent) 12%, transparent)',
            color: 'var(--accent)',
          }}>
          <IconUploadCloud size={16} />
        </span>
        <p className="text-[13.5px] font-bold">
          <span style={{ color: 'var(--accent)' }}>Subir archivo</span>
          <span className="ml-1.5 font-medium" style={{ color: 'var(--muted-foreground)' }}>· PDF / JPG / PNG</span>
        </p>
      </button>

      {/* Naming modal (create or edit) */}
      {pending && (
        <UploadNameModal
          pending={pending}
          mode={pending.mode}
          onChange={(name) => setPending({ ...pending, name })}
          onCancel={() => setPending(null)}
          onConfirm={confirmUpload}
          inputRef={nameInputRef}
        />
      )}

      {/* Delete confirmation modal */}
      {confirmIdx !== null && allFiles[confirmIdx] && (
        <ConfirmDeleteModal
          title="¿Eliminar archivo?"
          itemName={allFiles[confirmIdx].nombre}
          onCancel={() => setConfirmIdx(null)}
          onConfirm={() => {
            if (confirmIdx >= files.length) {
              const si = confirmIdx - files.length;
              setSessionFiles((arr) => arr.filter((_, j) => j !== si));
            } else if (onRemove) {
              onRemove(confirmIdx);
            }
            setExState((s) => { const n = { ...s }; delete n[confirmIdx]; return n; });
            setConfirmIdx(null);
          }}
        />
      )}

      {/* Preview modal */}
      {previewIdx !== null && allFiles[previewIdx] && (
        <FilePreviewModal
          file={allFiles[previewIdx]}
          onClose={() => setPreviewIdx(null)}
        />
      )}

      {/* File list */}
      {allFiles.length > 0 && (
        <div className="space-y-2">
          {allFiles.map((f, i) => (
            <div
              key={i}
              className="rounded-xl overflow-hidden transition-colors"
              style={{
                background: 'color-mix(in oklab, var(--surface) 50%, transparent)',
                border: '1px solid color-mix(in oklab, var(--border) 45%, transparent)',
              }}
            >
            <div
              className="flex items-center gap-3 px-4 py-3 cursor-pointer file-row"
              onClick={() => openFile(i)}
              role="button"
              tabIndex={0}
              onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFile(i); } }}
              onMouseEnter={(e) => { e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 6%, transparent)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
            >
              <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
                style={{
                  width: 36, height: 36,
                  background: 'color-mix(in oklab, var(--accent) 12%, transparent)',
                  color: 'var(--accent)',
                }}>
                <IconFileText size={17} />
              </span>
              <div className="flex-1 min-w-0">
                <p className="text-[13px] font-bold truncate" style={{ color: 'var(--foreground)' }}>
                  {f.nombre}
                </p>
                <p className="text-[11px] tabular-nums" style={{ color: 'var(--muted-foreground)' }}>
                  {f.tamano}
                  <span className="mx-1.5 opacity-50">·</span>
                  Subido por {subidoPorLabel(f.subidoPor)}
                  <span className="mx-1.5 opacity-50">·</span>
                  {formatDate(f.subidoAt)}
                </p>
              </div>
              <div className="flex items-center gap-1 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
                <button
                  onClick={() => openFile(i)}
                  className="inline-flex items-center justify-center rounded-md transition-colors cursor-pointer"
                  style={{ width: 32, height: 32, color: 'var(--muted-foreground)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--accent)'; e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 10%, transparent)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'transparent'; }}
                  title="Ver archivo"
                  type="button"
                >
                  <IconEye size={15} />
                </button>
                <button
                  onClick={() => startEdit(i)}
                  className="inline-flex items-center justify-center rounded-md transition-colors cursor-pointer"
                  style={{ width: 32, height: 32, color: 'var(--muted-foreground)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--accent)'; e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 10%, transparent)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'transparent'; }}
                  title="Editar nombre"
                  type="button"
                >
                  <IconPencil size={14} />
                </button>
                <button
                  onClick={() => setConfirmIdx(i)}
                  className="inline-flex items-center justify-center rounded-md transition-colors cursor-pointer"
                  style={{ width: 32, height: 32, color: 'var(--destructive)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = 'color-mix(in oklab, var(--destructive) 14%, transparent)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
                  title="Eliminar"
                  type="button"
                >
                  <IconTrash size={15} />
                </button>
              </div>
            </div>
            {/* DeepSeek (deepseek-chat) es texto-only: la franja de extracción solo se ofrece
                para PDF (pdf.js lee la capa de texto real). Fotos jpg/png quedan afuera por
                ahora — sin visión no hay forma de leerlas (ver print/ai-extract.js). */}
            {extractable && !extractDisabled && /\.pdf$/i.test(f.nombre || '') && (
              <AIExtractStrip state={exState[i]} onRun={() => runExtract(i)} count={(exMeta[i] || {}).count} error={(exMeta[i] || {}).error} warning={(exMeta[i] || {}).warning} />
            )}
            </div>
          ))}
        </div>
      )}

      {/* DECISIÓN 10 — extracción IA deshabilitada-honesta (una vez, a nivel documento) */}
      {extractable && extractDisabled && (
        <AIExtractDisabledNotice reason={extractDisabledReason} />
      )}
    </DocCard>
  );
}

Object.assign(window, {
  useDocEdit, buildDocActions, ConfirmDeleteModal,
  openCrmCompose, buildDocEmailAction, crmEmailFromDocFields,
  UploadNameModal, FilePreviewModal, AISpark, AIExtractStrip, AIExtractDisabledNotice, FileUploadCard,
});
