// src/screen-nube.jsx — la nube DEL AGENTE (épica #1424), 2026-10-18.
//  · «Sincronizar por tu nube» (#1432): la principal elige SU nube (una por despacho) y comparte la
//    carpeta; el miembro entra a esa misma desde su computadora. Ruta /ajustes/nube.
//  · «Respaldo automático» (#1439, rebanada de #508): carpeta de tu nube + contraseña guardada en el
//    llavero (#1438). Ruta /ajustes/respaldo.
// Estado vivo: AxNube y AxResp (abajo) los leen Ajustes, Tu cuenta, Equipo › sincronización, el indicador
// de la barra de arriba y el panel «Simular». Lo que haces en una pantalla se ve en las otras.
// Datos sintéticos: Despacho Ejemplo, Laura Ejemplo (principal), Sofía Ejemplo y Marco Ejemplo (miembros).

const NB_DESPACHO = 'Despacho Ejemplo';
const NB_CARPETA = 'Piixi · Despacho Ejemplo';
const NB_PRINCIPAL = 'Laura Ejemplo';
const NB_RESP_CARPETA = 'Piixi respaldos';

const NB_NUBES = {
  gdrive: {
    id: 'gdrive', nombre: 'Google Drive', app: 'Google Drive para escritorio', total: 15, piixi: 1.2,
    gratis: '15 GB gratuitos, compartidos con Gmail',
    llena: 'Si se llena, también se frena el envío de correos por Gmail, que Piixi usa para mandar tus avisos.',
    rutaMac: '~/Library/CloudStorage/GoogleDrive-despacho.ejemplo@gmail.com/Mi unidad',
    rutaWin: 'G:\\Mi unidad', detectaWin: false,
    cuenta: 'despacho.ejemplo@gmail.com', sitio: 'drive.google.com',
    compartir: [
      'Abre drive.google.com y entra con la cuenta del despacho.',
      'Busca la carpeta «' + NB_CARPETA + '» en Mi unidad.',
      'Clic derecho › Compartir › Compartir.',
      'Escribe el correo de cada miembro, déjalo como «Editor» y da Enviar.',
    ],
    miembroExtra: 'En drive.google.com › Compartidos conmigo, clic derecho en «' + NB_CARPETA + '» › Organizar › Agregar acceso directo a Mi unidad. Sin ese acceso directo, la carpeta no baja a tu computadora.',
  },
  onedrive: {
    id: 'onedrive', nombre: 'OneDrive', app: 'OneDrive', total: 5, piixi: 0.9,
    gratis: '5 GB gratuitos',
    llena: 'Si se llena, también se frena el correo de Outlook.com.',
    rutaMac: '~/Library/CloudStorage/OneDrive-Personal',
    rutaWin: 'C:\\Users\\Despacho\\OneDrive', detectaWin: true,
    cuenta: 'despacho.ejemplo@outlook.com', sitio: 'onedrive.live.com',
    compartir: [
      'Abre la carpeta de OneDrive en tu computadora.',
      'Clic derecho en «' + NB_CARPETA + '» › Compartir.',
      'Escribe el correo de cada miembro y deja «Puede editar».',
      'Da Enviar.',
    ],
    miembroExtra: 'En onedrive.live.com › Compartido, abre «' + NB_CARPETA + '» y elige «Agregar acceso directo a Mis archivos».',
  },
  icloud: {
    id: 'icloud', nombre: 'iCloud Drive', app: 'iCloud para Windows (11.1 o más nuevo)', total: 5, piixi: 0.9,
    gratis: '5 GB gratuitos, compartidos con tus fotos',
    llena: 'Si se llena, también dejan de subirse tus fotos y los respaldos del iPhone.',
    rutaMac: '~/Library/Mobile Documents/com~apple~CloudDocs',
    rutaWin: 'C:\\Users\\Despacho\\iCloud Drive', detectaWin: true,
    cuenta: 'cuenta de Apple del despacho', sitio: 'icloud.com',
    compartir: [
      'En el Finder, abre iCloud Drive.',
      'Clic derecho en «' + NB_CARPETA + '» › Compartir › Colaborar.',
      'Elige «Sólo las personas que invites» y «Pueden hacer cambios».',
      'Escribe el correo de la cuenta de Apple de cada miembro y da Compartir.',
    ],
    miembroExtra: 'Abre la invitación que te llegó por correo y acepta la carpeta: aparece sola en tu iCloud Drive.',
  },
};

const nbGB = (n) => (Math.round(n * 10) / 10).toLocaleString('es-MX', { minimumFractionDigits: 1, maximumFractionDigits: 1 });

// ─────────────────────────────────────────────────────────────
// ESTADO · Sincronizar por tu nube (#1432)
// ─────────────────────────────────────────────────────────────
const AxNube = (function () {
  const subs = new Set();
  let s = null;
  let reloj = null;
  let seq = 0;

  function inicial() {
    return {
      nube: null,              // la nube del despacho, la de la principal: null | gdrive | onedrive | icloud
      paso: 'elegir',          // principal: elegir | carpeta | creando | compartir | lista
      elegida: null,           // la que va seleccionando en el paso «elegir»
      detecta: true,           // Piixi encuentra la carpeta de la app de la nube (si no, diálogo nativo)
      soP: 'mac',              // la computadora de la principal
      mixto: true,             // el equipo tiene Mac y Windows
      permisoMac: true,        // macOS pide permiso la primera vez
      permiso: null,           // null | 'pidiendo' | 'negado'
      creandoPct: 0,
      dialogo: null,           // null | 'principal' | 'miembro' — diálogo nativo simulado
      carpeta: '',
      miembros: [
        { id: 'sofia', persona: 'Sofía Ejemplo', compu: 'Escritorio de Sofía', so: 'Windows', estado: 'invitada' },
        { id: 'marco', persona: 'Marco Ejemplo', compu: 'Laptop de Marco', so: 'Mac', estado: 'invitada' },
      ],
      espacio: 'normal',       // normal | casi | lleno
      conexion: 'ok',          // ok | desconectada — la app de la nube en ESTA computadora
      reconectando: false,
      pendientes: [],          // archivos que todavía no llegan
      // miembro (Sofía, Windows)
      mPaso: 'cuenta',         // cuenta | app | carpeta | verificando | unida
      mCuenta: null,           // null | 'si' | 'no'
      mAcceso: false,          // Google: acceso directo agregado
      mRechazo: null,          // null | 'otra' | 'ajena'
      mCarpeta: '',
      // preguntas A/B
      qIcloud: 'A',            // A avisa y deja elegir iCloud · B no lo ofrece con Windows en el equipo
      qNombre: 'A',            // A «Por Google Drive» · B «Por la nube del despacho»
      qEspacio: 'A',           // A sólo la principal ve el aviso · B también los miembros
    };
  }
  function emit() { s = Object.assign({}, s); subs.forEach((f) => f()); }
  function asegurar() { if (!s) s = inicial(); }
  function relojar() {
    if (reloj || !s.pendientes.length) return;
    reloj = setInterval(() => {
      if (!s.pendientes.length) { clearInterval(reloj); reloj = null; return; }
      s.pendientes = s.pendientes.map((p) => {
        if (p.buscando) return p;
        if (p.seg > 1) return Object.assign({}, p, { seg: p.seg - 1 });
        return Object.assign({}, p, { intento: p.intento + 1, seg: Math.min(900, p.espera * 2), espera: Math.min(900, p.espera * 2) });
      });
      emit();
    }, 1000);
  }

  const api = {
    get() { asegurar(); return s; },
    subscribe(f) { subs.add(f); return () => subs.delete(f); },
    set(p) { asegurar(); Object.assign(s, p); emit(); },
    info(id) { asegurar(); return NB_NUBES[id || s.nube] || null; },
    esMiembro() { return !!(window.AxEquipo && AxEquipo.get().ver === 'miembro'); },
    configurada() { asegurar(); return !!s.nube && s.paso === 'lista'; },
    ruta(id, so) {
      const n = NB_NUBES[id]; if (!n) return '';
      return so === 'win' ? n.rutaWin : n.rutaMac;
    },
    // ¿hay vía por la nube para ESTA computadora?
    viaDisponible() {
      asegurar();
      if (!api.configurada() || s.conexion !== 'ok') return false;
      if (api.esMiembro()) return s.mPaso === 'unida';
      return true;
    },
    nombreVia(largo) {
      asegurar();
      const n = api.info();
      if (!n) return 'la nube';
      if (s.qNombre === 'B') return 'la nube del despacho';
      return largo ? 'el ' + n.nombre + ' del despacho' : n.nombre;
    },
    // indicador de la barra (cuando la red no deja la directa)
    viaIndicador() {
      asegurar();
      const n = api.info();
      if (!api.configurada()) return { color: 'ambar', texto: 'Sin conexión directa' };
      if (s.conexion !== 'ok') return { color: 'rojo', texto: (n ? n.nombre : 'La nube') + ' no responde' };
      if (api.esMiembro() && s.mPaso !== 'unida') return { color: 'ambar', texto: 'Sin conexión directa' };
      if (s.pendientes.length) return { color: 'azul', texto: s.qNombre === 'B' ? 'Por la nube del despacho · 1 por llegar' : 'Por ' + n.nombre + ' · 1 por llegar' };
      return { color: 'gris', texto: s.qNombre === 'B' ? 'Por la nube del despacho' : 'Por ' + n.nombre };
    },
    espacioDatos() {
      asegurar();
      const n = api.info() || NB_NUBES.gdrive;
      const r = s.espacio === 'lleno' ? 1 : s.espacio === 'casi' ? 0.93 : 0.43;
      return { usado: n.total * r, total: n.total, piixi: n.piixi, ratio: r, libre: n.total * (1 - r) };
    },

    // ── principal ──
    elegir(id) { api.set({ elegida: id }); },
    continuarCon(id) { api.set({ elegida: id, paso: 'carpeta', permiso: null }); },
    crearCarpeta(ruta) {
      asegurar();
      const id = s.elegida;
      const r = ruta || api.ruta(id, s.soP);
      s.carpeta = r.replace(/[\\/]$/, '') + (s.soP === 'win' ? '\\' : '/') + NB_CARPETA;
      if (s.soP === 'mac' && s.permisoMac && s.permiso !== 'concedido') { s.permiso = 'pidiendo'; emit(); return; }
      api._crear();
    },
    responderPermiso(si) {
      asegurar();
      if (si) { s.permiso = 'concedido'; emit(); api._crear(); }
      else { s.permiso = 'negado'; emit(); }
    },
    _crear() {
      s.paso = 'creando'; s.creandoPct = 0; emit();
      const id = setInterval(() => {
        s.creandoPct = Math.min(1, s.creandoPct + 0.12);
        if (s.creandoPct >= 1) { clearInterval(id); s.nube = s.elegida; s.paso = 'compartir'; }
        emit();
      }, 160);
    },
    marcarCompartida(idM) {
      asegurar();
      s.miembros = s.miembros.map((m) => (m.id === idM && m.estado === 'invitada' ? Object.assign({}, m, { estado: 'compartida' }) : m));
      emit();
    },
    terminar() { api.set({ paso: 'lista' }); },
    // deja la nube sin avisos (para que cada recorrido empiece limpio)
    limpiar() { api.set({ espacio: 'normal', conexion: 'ok', pendientes: [], dialogo: null, reconectando: false }); },
    reiniciarPrincipal() {
      asegurar();
      const q = { qIcloud: s.qIcloud, qNombre: s.qNombre, qEspacio: s.qEspacio, soP: s.soP, mixto: s.mixto, detecta: s.detecta, permisoMac: s.permisoMac };
      s = Object.assign(inicial(), q); emit();
    },
    configurarYa(id) {
      asegurar();
      const n = id || s.nube || 'gdrive';
      Object.assign(s, {
        nube: n, elegida: n, paso: 'lista', permiso: 'concedido',
        carpeta: api.ruta(n, s.soP) + (s.soP === 'win' ? '\\' : '/') + NB_CARPETA,
      });
      emit();
    },

    // ── miembro ──
    mElegirCarpeta(opcion) {
      asegurar();
      s.dialogo = null;
      if (opcion !== 'buena') { s.mRechazo = opcion; if (s.mPaso === 'unida') s.mPaso = 'carpeta'; emit(); return; }
      s.mRechazo = null; s.mPaso = 'verificando'; s.conexion = 'ok';
      const n = api.info();
      s.mCarpeta = (n && n.id === 'gdrive' ? 'G:\\Mi unidad\\' : n && n.id === 'onedrive' ? 'C:\\Users\\Sofia\\OneDrive\\' : 'C:\\Users\\Sofia\\iCloud Drive\\') + NB_CARPETA;
      emit();
      setTimeout(() => {
        s.mPaso = 'unida';
        s.miembros = s.miembros.map((m) => (m.id === 'sofia' ? Object.assign({}, m, { estado: 'unida' }) : m));
        emit();
      }, 1400);
    },
    reiniciarMiembro() { api.set({ mPaso: 'cuenta', mCuenta: null, mAcceso: false, mRechazo: null, mCarpeta: '' }); },
    unirYa() {
      asegurar();
      s.mPaso = 'unida'; s.mCuenta = 'si'; s.mAcceso = true; s.mRechazo = null;
      const n = api.info();
      s.mCarpeta = (n && n.id === 'gdrive' ? 'G:\\Mi unidad\\' : 'C:\\Users\\Sofia\\OneDrive\\') + NB_CARPETA;
      s.miembros = s.miembros.map((m) => (m.id === 'sofia' ? Object.assign({}, m, { estado: 'unida' }) : m));
      emit();
    },

    // ── archivos que todavía no llegan (#1425) ──
    simularPendiente() {
      asegurar();
      if (s.pendientes.length) return;
      s.pendientes = [{ id: 'f' + (++seq), nombre: 'PDF de la póliza de Ana Torres Ruiz', detalle: '2.4 MB · lo subió la computadora principal a las 10:31 a.m.', intento: 2, seg: 95, espera: 120, buscando: false }];
      emit(); relojar();
    },
    reintentar(idF) {
      asegurar();
      s.pendientes = s.pendientes.map((p) => (p.id === idF ? Object.assign({}, p, { buscando: true }) : p)); emit();
      setTimeout(() => {
        s.pendientes = s.pendientes.map((p) => (p.id === idF ? Object.assign({}, p, { buscando: false, intento: p.intento + 1, seg: p.espera, }) : p));
        emit();
      }, 1300);
    },
    llego() {
      asegurar();
      if (!s.pendientes.length) return;
      const p = s.pendientes[0];
      s.pendientes = []; emit();
      api._toast({ kind: 'success', title: 'Llegó el archivo', message: p.nombre + ' bajó de ' + (api.info() ? api.info().nombre : 'la nube') + ' y ya se aplicó. No se perdió nada.' });
    },
    reconectar() {
      asegurar();
      s.reconectando = true; emit();
      setTimeout(() => {
        s.reconectando = false; s.conexion = 'ok'; emit();
        api._toast({ kind: 'success', title: (api.info() ? api.info().nombre : 'Tu nube') + ' volvió', message: 'Lo que estaba en espera viaja solo.' });
      }, 1300);
    },
    _toast(t) { if (window.__nbToast) window.__nbToast(t); },
  };
  return api;
})();

function useNube() {
  const [, force] = useState(0);
  useEffect(() => AxNube.subscribe(() => force((x) => x + 1)), []);
  return AxNube.get();
}

// ─────────────────────────────────────────────────────────────
// ESTADO · Respaldo automático (#1439 + #1438 llavero)
// ─────────────────────────────────────────────────────────────
// Los días se cuentan desde el «hoy» de la maqueta (AdmixPersonas.TODAY): 0 = hoy, -1 = ayer…
const NB_MESES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
function nbDia(offset) {
  const d = new Date(typeof AdmixPersonas !== 'undefined' && AdmixPersonas.TODAY ? AdmixPersonas.TODAY : '2026-05-18T00:00:00');
  d.setDate(d.getDate() + offset);
  return d;
}
function nbYMD(offset, sep) { const d = nbDia(offset); const s = sep == null ? '-' : sep; return d.getFullYear() + s + String(d.getMonth() + 1).padStart(2, '0') + s + String(d.getDate()).padStart(2, '0'); }
function rpFecha(dia, hoy) {
  if (dia === hoy) return 'Hoy';
  if (dia === hoy - 1) return 'Ayer';
  const d = nbDia(dia);
  return d.getDate() + ' ' + NB_MESES[d.getMonth()];
}
function rpArchivo(dia) { return 'piixi-respaldo-' + nbYMD(dia) + '.piixi'; }
function rpCopia(dia) { const k = ((dia % 7) + 7) % 7; return { dia, hora: '2:0' + k + ' a.m.', mb: 46 + (k % 4), archivo: rpArchivo(dia), subida: true }; }

const AxResp = (function () {
  const subs = new Set();
  let s = null;
  function inicial() {
    return {
      estado: 'apagado',      // apagado | configurando | encendido
      paso: 'carpeta',        // configurando: carpeta | contrasena
      nubeCarpeta: null,      // la nube donde vive la carpeta del respaldo
      carpeta: '',
      carpetaExiste: true,    // simular «la carpeta ya no existe»
      hoy: 0,
      copias: [],             // las últimas 7
      intento: null,          // { dia, ok, motivo: 'carpeta' | 'nocabe' }
      corriendo: false, pct: 0,
      comprobar: false,       // A: pasaron 30 días, Piixi pide la contraseña para comprobar que la recuerdas
      donde: 'esta',          // restaurar en: esta computadora | otra computadora
      abre: true,             // la contraseña tecleada abre el respaldo
      detectada: 'gdrive',    // la nube que Piixi encuentra en esta computadora (para sugerir la carpeta)
      qRestaurar: 'A',        // A en esta computadora usa el llavero · B siempre la tecleas
      qRecordar: 'A',         // A cada 30 días la pide para comprobar · B nunca la vuelve a pedir
    };
  }
  function emit() { s = Object.assign({}, s); subs.forEach((f) => f()); }
  function asegurar() { if (!s) s = inicial(); }
  function mismaNube() { return !!(s.nubeCarpeta && AxNube.get().nube === s.nubeCarpeta); }

  const api = {
    get() { asegurar(); return s; },
    subscribe(f) { subs.add(f); return () => subs.delete(f); },
    set(p) { asegurar(); Object.assign(s, p); emit(); },
    nube() { asegurar(); return NB_NUBES[s.nubeCarpeta] || null; },
    // ¿la app de esa nube no responde en esta computadora?
    nubeCaida() { asegurar(); return mismaNube() && AxNube.get().conexion !== 'ok'; },
    espacio() { asegurar(); return mismaNube() ? AxNube.get().espacio : 'normal'; },
    sugerida() {
      asegurar();
      const n = AxNube.get().nube || (s.detectada !== 'ninguna' ? s.detectada : null);
      return n ? { nube: n, ruta: AxNube.ruta(n, AxNube.get().soP) + (AxNube.get().soP === 'win' ? '\\' : '/') + NB_RESP_CARPETA } : null;
    },
    ultimo() { asegurar(); return s.copias.length ? s.copias[s.copias.length - 1] : null; },
    fallo() { asegurar(); return s.intento && !s.intento.ok ? s.intento : null; },

    empezar() { api.set({ estado: 'configurando', paso: 'carpeta' }); },
    usarCarpeta(nube, ruta) { api.set({ nubeCarpeta: nube, carpeta: ruta, carpetaExiste: true, paso: 'contrasena' }); },
    encender() { api.set({ estado: 'encendido', intento: null, comprobar: false }); },
    apagar() { api.set({ estado: 'apagado', paso: 'carpeta', intento: null, comprobar: false }); },
    encendidoYa() {
      asegurar();
      const sug = api.sugerida() || { nube: 'gdrive', ruta: AxNube.ruta('gdrive', 'mac') + '/' + NB_RESP_CARPETA };
      s.hoy = 0;
      const copias = []; for (let d = -6; d <= 0; d++) copias.push(rpCopia(d));
      Object.assign(s, { estado: 'encendido', nubeCarpeta: sug.nube, carpeta: sug.ruta, carpetaExiste: true, copias, intento: { dia: s.hoy, ok: true }, comprobar: false, corriendo: false, cambiandoClave: false });
      emit();
    },
    elegirCarpetaOtraVez(ruta) { api.set({ carpeta: ruta || s.carpeta, carpetaExiste: true }); },
    // corre el respaldo (a mano o la noche simulada)
    correr(etiqueta) {
      asegurar();
      if (s.corriendo || s.estado !== 'encendido') return;
      if (!s.carpetaExiste) { s.intento = { dia: s.hoy, ok: false, motivo: 'carpeta' }; emit(); return; }
      if (api.espacio() === 'lleno') { s.intento = { dia: s.hoy, ok: false, motivo: 'nocabe' }; emit(); return; }
      s.corriendo = true; s.pct = 0; emit();
      const id = setInterval(() => {
        s.pct = Math.min(1, s.pct + 0.08);
        if (s.pct >= 1) {
          clearInterval(id);
          const c = rpCopia(s.hoy);
          if (etiqueta === 'ahora') { const d = new Date(); const h = d.getHours(); c.hora = (h % 12 === 0 ? 12 : h % 12) + ':' + String(d.getMinutes()).padStart(2, '0') + (h < 12 ? ' a.m.' : ' p.m.'); }
          c.subida = !api.nubeCaida();
          s.copias = s.copias.filter((x) => x.dia !== s.hoy).concat([c]).slice(-7);
          s.intento = { dia: s.hoy, ok: true };
          s.corriendo = false;
        }
        emit();
      }, 140);
    },
    pasaNoche() { asegurar(); s.hoy += 1; emit(); api.correr('noche'); },
    pasan30() { asegurar(); if (s.qRecordar === 'A' && s.estado === 'encendido') api.set({ comprobar: true }); },
  };
  return api;
})();

function useResp() {
  const [, force] = useState(0);
  useEffect(() => AxResp.subscribe(() => force((x) => x + 1)), []);
  return AxResp.get();
}

// ─────────────────────────────────────────────────────────────
// PIEZAS
// ─────────────────────────────────────────────────────────────
function NbNubeIcono({ id, size = 18 }) {
  if (id === 'icloud') {
    return <svg width={size} height={size} viewBox="0 0 24 24" style={{ display: 'block' }}><path d="M18.5 19H7a5 5 0 0 1-.6-9.96A6.5 6.5 0 0 1 18.9 9.6 4.75 4.75 0 0 1 18.5 19z" fill="#3693F3"/></svg>;
  }
  if (window.ConnectorIcon) return <ConnectorIcon id={id} size={size}/>;
  return <AxIcon name="cloud" size={size}/>;
}

function NbCandado({ size = 15 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{ display: 'block', flex: '0 0 ' + size + 'px' }}>
      <rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
    </svg>
  );
}

function NbBarra({ valor, tono }) {
  const c = tono === 'bad' ? 'var(--ax-danger)' : tono === 'warm' ? 'var(--ax-warm)' : tono === 'info' ? 'var(--ax-info)' : 'var(--ax-accent)';
  return (
    <div style={{ height: 8, borderRadius: 8, background: 'var(--ax-bg-4)', overflow: 'hidden' }}>
      <div style={{ height: '100%', width: Math.round(Math.min(1, valor || 0) * 100) + '%', background: c, borderRadius: 8, transition: 'width 200ms linear' }}/>
    </div>
  );
}

function NbAviso({ tono, icono, titulo, children, acciones, dataNb }) {
  const t = tono === 'bad' ? { c: 'var(--ax-danger)', b: 'var(--ax-danger)', bg: 'var(--ax-danger-soft)' }
    : tono === 'info' ? { c: 'var(--ax-info)', b: 'var(--ax-info-line)', bg: 'var(--ax-info-soft)' }
    : tono === 'ok' ? { c: 'var(--ax-accent)', b: 'var(--ax-accent-line)', bg: 'var(--ax-accent-soft)' }
    : { c: 'var(--ax-warm)', b: 'var(--ax-warm-line)', bg: 'var(--ax-warm-soft)' };
  return (
    <div role="status" data-nb={dataNb || 'aviso'} style={{ display: 'flex', gap: 12, alignItems: 'flex-start', padding: '12px 14px', borderRadius: 8, border: '1px solid ' + t.b, background: t.bg }}>
      <span style={{ color: t.c, display: 'flex', marginTop: 1 }}>{icono === 'candado' ? <NbCandado size={16}/> : <AxIcon name={icono || 'info'} size={16}/>}</span>
      <div style={{ flex: 1, minWidth: 0, fontSize: 13, color: 'var(--ax-fg)', lineHeight: 1.55 }}>
        {titulo ? <div style={{ color: 'var(--ax-fg-strong)', fontWeight: 600, marginBottom: 2 }}>{titulo}</div> : null}
        {children}
        {acciones ? <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 10 }}>{acciones}</div> : null}
      </div>
    </div>
  );
}

function NbTarjeta({ titulo, icono, derecha, children, dataNb, id }) {
  return (
    <div className="ax-card" data-nb={dataNb} id={id}>
      <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--ax-border)', display: 'flex', alignItems: 'center', gap: 10 }}>
        {icono === 'candado' ? <span style={{ color: 'var(--ax-fg-muted)' }}><NbCandado/></span> : <AxIcon name={icono || 'cloud'} size={15} style={{ color: 'var(--ax-fg-muted)' }}/>}
        <h3 style={{ margin: 0, fontSize: 14, fontWeight: 500, color: 'var(--ax-fg-strong)' }}>{titulo}</h3>
        {derecha ? <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>{derecha}</div> : null}
      </div>
      <div style={{ padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 14 }}>{children}</div>
    </div>
  );
}

function NbFila({ etiqueta, children }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '170px 1fr', gap: 14, alignItems: 'baseline', fontSize: 13 }}>
      <span style={{ color: 'var(--ax-fg-muted)' }}>{etiqueta}</span>
      <span style={{ color: 'var(--ax-fg-strong)', minWidth: 0, overflowWrap: 'anywhere' }}>{children}</span>
    </div>
  );
}

function NbNota({ children }) {
  return <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)', lineHeight: 1.55 }}>{children}</div>;
}

// Paso numerado: hecho · activo · pendiente
function NbPaso({ n, titulo, estado, resumen, children }) {
  const hecho = estado === 'hecho', activo = estado === 'activo';
  return (
    <div data-nb={'paso-' + n} data-estado={estado} style={{ display: 'flex', gap: 14, padding: '14px 0', borderTop: n > 1 ? '1px solid var(--ax-border)' : 'none', opacity: estado === 'pendiente' ? 0.55 : 1 }}>
      <span style={{
        width: 26, height: 26, borderRadius: '50%', flex: '0 0 26px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        fontSize: 12, fontWeight: 600,
        background: hecho ? 'var(--ax-accent-soft)' : activo ? 'var(--ax-fg-strong)' : 'var(--ax-bg-3)',
        color: hecho ? 'var(--ax-accent)' : activo ? 'var(--ax-bg-2)' : 'var(--ax-fg-faint)',
        border: '1px solid ' + (hecho ? 'var(--ax-accent-line)' : 'var(--ax-border-strong)'),
      }}>{hecho ? <AxIcon name="check" size={13}/> : n}</span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14, color: 'var(--ax-fg-strong)', fontWeight: activo ? 600 : 500 }}>{titulo}</div>
        {hecho && resumen ? <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)', marginTop: 3 }}>{resumen}</div> : null}
        {activo ? <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>{children}</div> : null}
      </div>
    </div>
  );
}

// ── Diálogos del sistema (simulados) ─────────────────────────
function NbSistema({ open, titulo, children, botones, ancho }) {
  if (!open) return null;
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 1100, background: 'rgba(0,0,0,.35)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div className="ax-fade-in" data-nb="dialogo-sistema" style={{
        width: ancho || 420, maxWidth: 'calc(100vw - 40px)', background: '#ECECEC', color: '#1d1d1f', borderRadius: 12,
        boxShadow: '0 24px 60px rgba(0,0,0,.45)', border: '1px solid rgba(0,0,0,.2)', font: '13px/1.45 -apple-system, "Segoe UI", system-ui, sans-serif', overflow: 'hidden',
      }}>
        <div style={{ padding: '8px 12px', fontSize: 11, color: '#6e6e73', borderBottom: '1px solid rgba(0,0,0,.08)', background: '#E3E3E3' }}>Ventana del sistema operativo · no es de Piixi</div>
        <div style={{ padding: 18 }}>
          {titulo ? <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 8 }}>{titulo}</div> : null}
          {children}
        </div>
        <div style={{ padding: '10px 14px', display: 'flex', justifyContent: 'flex-end', gap: 8, background: '#E3E3E3', borderTop: '1px solid rgba(0,0,0,.08)' }}>{botones}</div>
      </div>
    </div>
  );
}
function NbSisBoton({ primario, onClick, children, disabled }) {
  return (
    <button type="button" onClick={onClick} disabled={disabled} style={{
      appearance: 'none', borderRadius: 6, padding: '5px 14px', font: 'inherit', fontSize: 13, cursor: 'default',
      border: primario ? '1px solid #0063E1' : '1px solid rgba(0,0,0,.2)', background: primario ? (disabled ? '#9cbcf0' : '#0A7AFF') : '#fff', color: primario ? '#fff' : '#1d1d1f',
    }}>{children}</button>
  );
}

// Selector de carpeta nativo (simulado): la principal elige dónde crearla; el miembro elige la del despacho.
function NbDialogoCarpeta() {
  const n = useNube();
  const [sel, setSel] = useState(null);
  useEffect(() => { setSel(null); }, [n.dialogo]);
  if (!n.dialogo) return null;
  const nube = AxNube.info(n.dialogo === 'principal' ? n.elegida : n.nube) || NB_NUBES.gdrive;
  const miembro = n.dialogo === 'miembro';
  const compartida = n.miembros.find((m) => m.id === 'sofia').estado !== 'invitada';
  const opciones = miembro
    ? [
        compartida ? { k: 'buena', l: NB_CARPETA, sub: nube.id === 'gdrive' ? 'Mi unidad › acceso directo' : 'compartida por ' + NB_PRINCIPAL } : null,
        { k: 'ajena', l: 'Piixi · Seguros del Norte', sub: 'de otra instalación de Piixi' },
        { k: 'otra', l: 'Documentos de la oficina', sub: 'carpeta normal' },
      ].filter(Boolean)
    : [
        { k: 'raiz', l: nube.id === 'gdrive' ? 'Mi unidad' : nube.nombre, sub: AxNube.ruta(nube.id, n.soP) },
        { k: 'docs', l: 'Documentos', sub: (n.soP === 'win' ? 'C:\\Users\\Despacho\\Documents' : '~/Documents') + ' · no es de la nube' },
      ];
  const elegir = () => {
    if (!sel) return;
    if (miembro) { AxNube.mElegirCarpeta(sel); return; }
    const o = opciones.find((x) => x.k === sel);
    AxNube.set({ dialogo: null });
    if (sel === 'docs') { AxNube.set({ avisoNoNube: true }); return; }
    AxNube.set({ avisoNoNube: false });
    AxNube.crearCarpeta(o.sub);
  };
  return (
    <NbSistema open titulo={miembro ? 'Elige la carpeta del despacho' : 'Elige dónde crear la carpeta del despacho'} ancho={520}
      botones={<><NbSisBoton onClick={() => AxNube.set({ dialogo: null })}>Cancelar</NbSisBoton><NbSisBoton primario disabled={!sel} onClick={elegir}>Elegir</NbSisBoton></>}>
      <div style={{ display: 'flex', gap: 12 }}>
        <div style={{ width: 130, flex: '0 0 130px', fontSize: 12, color: '#6e6e73', display: 'flex', flexDirection: 'column', gap: 6 }}>
          <b style={{ color: '#1d1d1f', fontSize: 11, textTransform: 'uppercase', letterSpacing: '.04em' }}>Ubicaciones</b>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#1d1d1f' }}><NbNubeIcono id={nube.id} size={14}/>{nube.nombre}</span>
          <span>Documentos</span><span>Escritorio</span><span>Descargas</span>
        </div>
        <div style={{ flex: 1, background: '#fff', border: '1px solid rgba(0,0,0,.12)', borderRadius: 6, minHeight: 150 }}>
          {opciones.map((o) => (
            <button key={o.k} type="button" data-nb={'carpeta-' + o.k} onClick={() => setSel(o.k)} style={{
              appearance: 'none', display: 'flex', alignItems: 'center', gap: 10, width: '100%', textAlign: 'left', border: 0, cursor: 'default',
              padding: '8px 10px', font: 'inherit', background: sel === o.k ? '#0A7AFF' : 'transparent', color: sel === o.k ? '#fff' : '#1d1d1f',
            }}>
              <AxIcon name="folder" size={16}/>
              <span style={{ display: 'flex', flexDirection: 'column' }}>
                <span>{o.l}</span>
                <span style={{ fontSize: 11, opacity: 0.7 }}>{o.sub}</span>
              </span>
            </button>
          ))}
          {miembro && !compartida ? (
            <div style={{ padding: '10px', fontSize: 11.5, color: '#86868b', borderTop: '1px solid rgba(0,0,0,.08)' }}>
              No aparece «{NB_CARPETA}»: {NB_PRINCIPAL} todavía no te la comparte.
            </div>
          ) : null}
        </div>
      </div>
    </NbSistema>
  );
}

function NbPermisoMac() {
  const n = useNube();
  const nube = AxNube.info(n.elegida) || NB_NUBES.gdrive;
  return (
    <NbSistema open={n.permiso === 'pidiendo'} titulo={'«Piixi» quiere acceder a archivos de ' + nube.nombre + '.'}
      botones={<><NbSisBoton onClick={() => AxNube.responderPermiso(false)}>No permitir</NbSisBoton><NbSisBoton primario onClick={() => AxNube.responderPermiso(true)}>Permitir</NbSisBoton></>}>
      <div style={{ color: '#3a3a3c' }}>Puedes cambiarlo después en Configuración del Sistema › Privacidad y seguridad › Archivos y carpetas.</div>
    </NbSistema>
  );
}

// ─────────────────────────────────────────────────────────────
// SINCRONIZAR POR TU NUBE — piezas compartidas
// ─────────────────────────────────────────────────────────────
function NbEspacio({ compacto }) {
  const n = useNube();
  const nube = AxNube.info();
  if (!nube) return null;
  const e = AxNube.espacioDatos();
  const tono = n.espacio === 'lleno' ? 'bad' : n.espacio === 'casi' ? 'warm' : 'ok';
  return (
    <div data-nb="espacio" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, color: 'var(--ax-fg-strong)' }}>
        <span>Espacio de {nube.nombre}</span>
        <span className="ax-mono" style={{ color: 'var(--ax-fg-muted)' }}>{nbGB(e.usado)} GB de {e.total} GB · Piixi ocupa {nbGB(e.piixi)} GB</span>
      </div>
      <NbBarra valor={e.ratio} tono={tono === 'ok' ? 'info' : tono}/>
      {!compacto && n.espacio !== 'normal' ? (
        <NbAviso tono={tono} icono="alert" dataNb={'espacio-' + n.espacio}
          titulo={n.espacio === 'lleno' ? 'Tu ' + nube.nombre + ' está lleno' : 'Tu ' + nube.nombre + ' está casi lleno'}
          acciones={<>
            <AxButton size="sm" variant="secondary" icon="arrow-right" onClick={() => AxNube._toast({ kind: 'info', message: 'En la app real se abre ' + nube.sitio + ' para liberar espacio.' })}>Liberar espacio en {nube.nombre}</AxButton>
          </>}>
          {n.espacio === 'lleno'
            ? 'Ya no cabe nada nuevo: los cambios del equipo esperan aquí y viajan directo cuando las computadoras se alcancen. '
            : 'Quedan ' + nbGB(e.libre) + ' GB. Cuando se llene, lo que viaja por tu nube se queda esperando. '}
          {nube.llena} Piixi ya borra solo lo que no se usa: lo que queda es de la cartera vigente.
        </NbAviso>
      ) : null}
    </div>
  );
}

function NbPendientes() {
  const n = useNube();
  if (!n.pendientes.length) return null;
  const nube = AxNube.info();
  return (
    <div data-nb="pendientes" style={{ border: '1px solid var(--ax-border)', borderRadius: 8 }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--ax-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
        <AxEyebrow>{n.pendientes.length === 1 ? '1 archivo todavía no llega' : n.pendientes.length + ' archivos todavía no llegan'}</AxEyebrow>
        <AxPill tone="info" dot>No se perdió</AxPill>
      </div>
      {n.pendientes.map((p) => {
        const m = Math.floor(p.seg / 60), sg = String(p.seg % 60).padStart(2, '0');
        return (
          <div key={p.id} data-nb="pendiente" style={{ padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
            <AxIcon name="pdf" size={16} style={{ color: 'var(--ax-fg-muted)' }}/>
            <div style={{ flex: '1 1 260px', minWidth: 0 }}>
              <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>{p.nombre}</div>
              <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)', marginTop: 2 }}>{p.detalle}</div>
              <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)', marginTop: 2 }}>
                {p.buscando ? 'Buscándolo en la carpeta…' : nube.nombre + ' todavía lo está bajando · intento ' + p.intento + ' · siguiente en ' + m + ':' + sg}
              </div>
            </div>
            <AxButton size="sm" variant="secondary" icon="refresh" loading={p.buscando} onClick={() => AxNube.reintentar(p.id)}>{p.buscando ? 'Buscando…' : 'Reintentar ahora'}</AxButton>
          </div>
        );
      })}
      <div style={{ padding: '0 14px 12px' }}>
        <NbNota>El aviso llegó antes que el archivo. Piixi lo vuelve a buscar solo, cada vez con más calma, y lo aplica en cuanto {nube.nombre} termine de bajarlo. Sólo si en 24 horas no aparece se avisa como falla.</NbNota>
      </div>
    </div>
  );
}

function NbDesconectada() {
  const n = useNube();
  const nube = AxNube.info();
  if (!nube || n.conexion === 'ok') return null;
  return (
    <NbAviso tono="bad" icono="alert" dataNb="desconectada" titulo={nube.nombre + ' no responde en esta computadora'}
      acciones={<>
        <AxButton size="sm" variant="primary" icon="refresh" loading={n.reconectando} onClick={() => AxNube.reconectar()}>{n.reconectando ? 'Buscando…' : 'Reintentar'}</AxButton>
        <AxButton size="sm" variant="secondary" onClick={() => AxNube._toast({ kind: 'info', message: 'En la app real se abre ' + nube.app + '.' })}>Abrir {nube.nombre}</AxButton>
        <AxButton size="sm" variant="tertiary" icon="folder" onClick={() => (AxNube.esMiembro() ? AxNube.set({ dialogo: 'miembro' }) : AxNube.reconectar())}>Elegir la carpeta otra vez</AxButton>
      </>}>
      La app de {nube.nombre} está cerrada, cerró sesión o la carpeta «{NB_CARPETA}» se movió. Nada se pierde: lo que cambies se guarda aquí, sigue viajando directo cuando las computadoras se alcancen, y por la nube en cuanto vuelva.
    </NbAviso>
  );
}

// ─────────────────────────────────────────────────────────────
// SINCRONIZAR POR TU NUBE — principal
// ─────────────────────────────────────────────────────────────
function NbElegirNube() {
  const n = useNube();
  const ids = ['gdrive', 'onedrive', 'icloud'];
  const sel = n.elegida;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 10 }}>
        {ids.map((id) => {
          const x = NB_NUBES[id];
          const bloqueada = id === 'icloud' && n.mixto && n.qIcloud === 'B';
          const avisar = id === 'icloud' && n.mixto && n.qIcloud === 'A';
          const on = sel === id;
          return (
            <button key={id} type="button" data-nb={'nube-' + id} disabled={bloqueada} onClick={() => AxNube.elegir(id)} style={{
              appearance: 'none', textAlign: 'left', cursor: 'default', font: 'inherit', padding: 14, borderRadius: 10,
              background: on ? 'var(--ax-accent-soft)' : 'var(--ax-bg-1)', color: 'var(--ax-fg)',
              border: '1px solid ' + (on ? 'var(--ax-accent-line)' : 'var(--ax-border)'), opacity: bloqueada ? 0.5 : 1,
              display: 'flex', flexDirection: 'column', gap: 8,
            }}>
              <span style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <span style={{ width: 30, height: 30, borderRadius: 7, background: 'var(--ax-bg-3)', border: '1px solid var(--ax-border-strong)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}><NbNubeIcono id={id} size={17}/></span>
                <span style={{ fontSize: 14, color: 'var(--ax-fg-strong)', fontWeight: 600 }}>{x.nombre}</span>
                {on ? <span style={{ marginLeft: 'auto', color: 'var(--ax-accent)' }}><AxIcon name="check" size={15}/></span> : null}
              </span>
              <span style={{ fontSize: 12, color: 'var(--ax-fg-muted)' }}>{x.gratis}</span>
              {bloqueada ? <span style={{ fontSize: 12, color: 'var(--ax-fg-muted)' }}>No disponible: en tu equipo hay computadoras Windows, y ahí iCloud se traba.</span> : null}
              {avisar ? <span style={{ fontSize: 12, color: 'var(--ax-warm)' }}>En tu equipo hay computadoras Windows: ahí iCloud se traba seguido. Te sugerimos Google Drive u OneDrive.</span> : null}
            </button>
          );
        })}
      </div>
      <NbNota>Una nube por despacho: la tuya. Tus miembros entran a esa misma carpeta; si usan otra nube, abren una cuenta gratuita en la tuya y lo compartido no gasta su espacio.</NbNota>
      <div><AxButton variant="primary" icon="arrow-right" disabled={!sel} onClick={() => AxNube.continuarCon(sel)}>{sel ? 'Continuar con ' + NB_NUBES[sel].nombre : 'Elige una nube'}</AxButton></div>
    </div>
  );
}

function NbPasoCarpeta() {
  const n = useNube();
  const nube = AxNube.info(n.elegida);
  const win = n.soP === 'win';
  const encontrada = n.detecta && (!win || nube.detectaWin);
  const ruta = AxNube.ruta(nube.id, n.soP);
  return (
    <>
      {n.permiso === 'negado' ? (
        <NbAviso tono="bad" icono="alert" titulo="Piixi no puede usar la carpeta sin ese permiso"
          acciones={<AxButton size="sm" variant="primary" onClick={() => AxNube.set({ permiso: 'pidiendo' })}>Volver a pedir el permiso</AxButton>}>
          macOS pregunta una sola vez. Si contestaste «No permitir», ábrelo en Configuración del Sistema › Privacidad y seguridad › Archivos y carpetas › Piixi.
        </NbAviso>
      ) : null}
      {n.avisoNoNube ? (
        <NbAviso tono="warm" icono="alert" titulo="Esa carpeta no es de tu nube">
          Elegiste una carpeta de esta computadora que no sincroniza {nube.nombre}: los miembros no la verían. Elige una dentro de {nube.nombre}.
        </NbAviso>
      ) : null}
      {encontrada ? (
        <>
          <NbAviso tono="ok" icono="check" titulo={'Encontramos ' + nube.nombre + ' en esta computadora'}>
            <span className="ax-mono" style={{ fontSize: 12 }}>{ruta}</span>
          </NbAviso>
          <NbNota>Piixi crea ahí la carpeta «{NB_CARPETA}» con un archivo marcador sellado, para que cada miembro compruebe que eligió la buena. Lo que viaja por ella va sellado: ni Piixi ni {nube.nombre} pueden leerlo.</NbNota>
          {!win && n.permisoMac && n.permiso !== 'concedido' ? <NbNota>La primera vez, macOS te va a preguntar si Piixi puede usar archivos de {nube.nombre}: contesta «Permitir».</NbNota> : null}
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            <AxButton variant="primary" icon="folder" onClick={() => AxNube.crearCarpeta(ruta)}>Crear la carpeta aquí</AxButton>
            <AxButton variant="secondary" onClick={() => AxNube.set({ dialogo: 'principal' })}>Elegir otra ubicación…</AxButton>
          </div>
        </>
      ) : (
        <>
          <NbAviso tono="warm" icono="search" titulo={'No encontramos la carpeta de ' + nube.nombre}>
            {win && nube.id === 'gdrive'
              ? 'En Windows, Google Drive aparece como una unidad aparte (casi siempre G:) y no hay forma segura de detectarla. Elígela tú: busca «Mi unidad».'
              : '¿Ya instalaste ' + nube.app + ' e iniciaste sesión? Si sí, elige su carpeta tú.'}
          </NbAviso>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            <AxButton variant="primary" icon="folder" onClick={() => AxNube.set({ dialogo: 'principal' })}>Elegir la carpeta de {nube.nombre}…</AxButton>
            <AxButton variant="tertiary" onClick={() => AxNube._toast({ kind: 'info', message: 'En la app real se abre la página de descarga de ' + nube.app + '.' })}>No tengo la app: descargarla</AxButton>
          </div>
        </>
      )}
      <div><AxButton size="sm" variant="tertiary" icon="arrow-left" onClick={() => AxNube.set({ paso: 'elegir', permiso: null, avisoNoNube: false })}>Elegir otra nube</AxButton></div>
    </>
  );
}

function NbMiembroFila({ m, conAccion }) {
  const pill = m.estado === 'unida' ? <AxPill tone="accent" dot>Ya entró</AxPill>
    : m.estado === 'compartida' ? <AxPill tone="warm">Compartida · falta que la elija</AxPill>
    : <AxPill tone="ghost">Falta compartírsela</AxPill>;
  return (
    <div data-nb={'miembro-' + m.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderTop: '1px solid var(--ax-border)', flexWrap: 'wrap' }}>
      <AxAvatar name={m.persona} size={28}/>
      <div style={{ flex: '1 1 180px', minWidth: 0 }}>
        <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>{m.persona}</div>
        <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)' }}>{m.compu} · {m.so}</div>
      </div>
      {pill}
      {conAccion && m.estado === 'invitada' ? <AxButton size="sm" variant="secondary" icon="check" onClick={() => AxNube.marcarCompartida(m.id)}>Ya se la compartí</AxButton> : null}
    </div>
  );
}

function NbPasoCompartir() {
  const n = useNube();
  const nube = AxNube.info();
  const [copiado, setCopiado] = useState(false);
  const copiar = () => {
    const t = 'Para unirte a la nube de ' + NB_DESPACHO + ' en Piixi:\n1. Si no tienes cuenta de ' + nube.nombre + ', abre una gratuita.\n2. Instala ' + nube.app + '.\n3. ' + nube.miembroExtra + '\n4. En Piixi: Ajustes › Equipo › Nube del despacho › Elegir la carpeta.';
    if (window.pregCopiar) pregCopiar(t);
    setCopiado(true); setTimeout(() => setCopiado(false), 3000);
  };
  return (
    <>
      <NbAviso tono="ok" icono="check" titulo={'Carpeta creada en tu ' + nube.nombre}>
        <span className="ax-mono" style={{ fontSize: 12 }}>{n.carpeta}</span>
      </NbAviso>
      <div>
        <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)', marginBottom: 6 }}>Compártela con tu equipo, desde {nube.nombre}:</div>
        <ol style={{ margin: 0, paddingLeft: 20, fontSize: 13, color: 'var(--ax-fg)', lineHeight: 1.7 }}>
          {nube.compartir.map((p, i) => <li key={i}>{p}</li>)}
        </ol>
        <NbNota>Piixi no la puede compartir por ti: {nube.id === 'icloud' ? 'iCloud no lo permite a otras apps.' : 'se hace desde tu cuenta de ' + nube.nombre + ', con tu permiso.'}</NbNota>
      </div>
      <div>
        {n.miembros.map((m) => <NbMiembroFila key={m.id} m={m} conAccion/>)}
      </div>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        <AxButton variant="secondary" icon="copy" onClick={copiar}>{copiado ? 'Copiado' : 'Copiar los pasos para tus miembros'}</AxButton>
        <AxButton variant="primary" icon="check" onClick={() => AxNube.terminar()}>Listo</AxButton>
      </div>
    </>
  );
}

function NbPrincipalLista() {
  const n = useNube();
  const nube = AxNube.info();
  const unidos = n.miembros.filter((m) => m.estado === 'unida').length;
  return (
    <>
      <NbDesconectada/>
      <NbTarjeta titulo={'Nube del despacho · ' + nube.nombre} icono="cloud" dataNb="resumen-principal"
        derecha={n.conexion === 'ok' ? <AxPill tone="accent" dot>Conectada</AxPill> : <AxPill tone="danger">No responde</AxPill>}>
        <NbFila etiqueta="Carpeta"><span className="ax-mono" style={{ fontSize: 12 }}>{n.carpeta}</span></NbFila>
        <NbFila etiqueta="Cuenta">{nube.cuenta}</NbFila>
        <NbFila etiqueta="Cuándo se usa">Cuando las computadoras no se alcanzan directo (la principal apagada, o una red que no deja). Directo sigue siendo lo preferido.</NbFila>
        <NbEspacio/>
        <NbPendientes/>
      </NbTarjeta>
      <NbTarjeta titulo={'Miembros · ' + unidos + ' de ' + n.miembros.length + ' ya entraron'} icono="user" dataNb="miembros">
        <div>{n.miembros.map((m) => <NbMiembroFila key={m.id} m={m} conAccion/>)}</div>
        <NbNota>Para sacar a alguien del equipo son dos pasos: dejar de compartirle la carpeta en {nube.nombre} y rotar la llave del despacho en Ajustes › Equipo.</NbNota>
      </NbTarjeta>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        <AxButton variant="tertiary" icon="refresh" onClick={() => AxNube.reiniciarPrincipal()}>Cambiar de nube</AxButton>
      </div>
    </>
  );
}

function NbPrincipal() {
  const n = useNube();
  const pasoN = { elegir: 1, carpeta: 2, creando: 2, compartir: 3, lista: 4 }[n.paso];
  if (n.paso === 'lista') return <NbPrincipalLista/>;
  const est = (k) => (pasoN > k ? 'hecho' : pasoN === k ? 'activo' : 'pendiente');
  const nube = AxNube.info(n.elegida);
  return (
    <NbTarjeta titulo="Configura la nube del despacho" icono="cloud" dataNb="asistente-principal" derecha={<AxPill tone="ghost">Paso {Math.min(3, pasoN)} de 3</AxPill>}>
      <NbNota>Cuando las computadoras del equipo no se alcanzan directo, los cambios y los PDF viajan por una carpeta en tu nube, sellados en tu computadora. Ningún dato del despacho pasa por los servidores de Piixi.</NbNota>
      <div>
        <NbPaso n={1} titulo="Elige tu nube" estado={est(1)} resumen={nube ? nube.nombre : ''}><NbElegirNube/></NbPaso>
        <NbPaso n={2} titulo="La carpeta del despacho" estado={est(2)} resumen={n.carpeta}>
          {n.paso === 'creando' ? (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>Creando la carpeta y su marcador sellado… {Math.round(n.creandoPct * 100)} %</div>
              <NbBarra valor={n.creandoPct}/>
            </div>
          ) : <NbPasoCarpeta/>}
        </NbPaso>
        <NbPaso n={3} titulo="Compártela con tu equipo" estado={est(3)}><NbPasoCompartir/></NbPaso>
      </div>
    </NbTarjeta>
  );
}

// ─────────────────────────────────────────────────────────────
// SINCRONIZAR POR TU NUBE — miembro (Sofía, Windows)
// ─────────────────────────────────────────────────────────────
function NbMiembro() {
  const n = useNube();
  const nube = AxNube.info();
  if (!AxNube.configurada()) {
    return (
      <NbTarjeta titulo="Nube del despacho" icono="cloud" dataNb="miembro-sin-nube">
        <NbAviso tono="info" icono="info" titulo="La computadora principal todavía no elige la nube del despacho">
          Cuando {NB_PRINCIPAL} la configure, aquí verás los pasos para entrar. Mientras, todo viaja directo cuando las computadoras se alcanzan.
        </NbAviso>
      </NbTarjeta>
    );
  }
  const compartida = n.miembros.find((m) => m.id === 'sofia').estado !== 'invitada';
  const orden = { cuenta: 1, app: 2, carpeta: 3, verificando: 3, unida: 4 };
  const k = orden[n.mPaso];
  const est = (i) => (k > i ? 'hecho' : k === i ? 'activo' : 'pendiente');
  const cabecera = (
    <NbAviso tono="info" icono="cloud" dataNb="miembro-cabecera" titulo={'Tu despacho sincroniza por el ' + nube.nombre + ' de ' + NB_PRINCIPAL}>
      Es la nube de la computadora principal: una por despacho. Lo que te comparte no gasta tu espacio.
    </NbAviso>
  );
  if (n.mPaso === 'unida') {
    return (
      <>
        <NbDesconectada/>
        <NbTarjeta titulo="Nube del despacho" icono="cloud" dataNb="miembro-unida"
          derecha={n.conexion === 'ok' ? <AxPill tone="accent" dot>Esta computadora ya entró</AxPill> : <AxPill tone="danger">No responde</AxPill>}>
          {cabecera}
          <NbFila etiqueta="Carpeta en esta computadora"><span className="ax-mono" style={{ fontSize: 12 }}>{n.mCarpeta}</span></NbFila>
          <NbFila etiqueta="Cuándo se usa">Cuando no te alcanzas directo con la computadora principal. Tus cambios suben a tu subcarpeta y la principal los aplica al leerlos.</NbFila>
          {n.qEspacio === 'B' && n.espacio !== 'normal' ? (
            <NbAviso tono={n.espacio === 'lleno' ? 'bad' : 'warm'} icono="alert" dataNb="espacio-miembro" titulo={'El ' + nube.nombre + ' del despacho está ' + (n.espacio === 'lleno' ? 'lleno' : 'casi lleno')}>
              {NB_PRINCIPAL} ya tiene el aviso; no tienes que hacer nada. Si se llena, tus cambios esperan aquí y viajan directo cuando se alcancen.
            </NbAviso>
          ) : null}
          <NbPendientes/>
        </NbTarjeta>
      </>
    );
  }
  return (
    <NbTarjeta titulo="Entra a la nube del despacho" icono="cloud" dataNb="asistente-miembro" derecha={<AxPill tone="ghost">Paso {Math.min(3, k)} de 3</AxPill>}>
      {cabecera}
      <div>
        <NbPaso n={1} titulo={'Tu cuenta de ' + nube.nombre} estado={est(1)} resumen={n.mCuenta === 'no' ? 'Abriste una cuenta gratuita' : 'Ya tenías cuenta'}>
          {n.mCuenta === 'no' ? (
            <>
              <NbNota>Abre una cuenta gratuita de {nube.nombre} en {nube.sitio}. No necesitas pagar nada: lo que {NB_PRINCIPAL} comparte no gasta tu espacio.</NbNota>
              <div style={{ display: 'flex', gap: 8 }}>
                <AxButton size="sm" variant="secondary" onClick={() => AxNube._toast({ kind: 'info', message: 'En la app real se abre ' + nube.sitio + '.' })}>Abrir {nube.sitio}</AxButton>
                <AxButton size="sm" variant="primary" icon="check" onClick={() => AxNube.set({ mPaso: 'app' })}>Ya la abrí</AxButton>
              </div>
            </>
          ) : (
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
              <AxButton size="sm" variant="primary" onClick={() => AxNube.set({ mCuenta: 'si', mPaso: 'app' })}>Sí, ya tengo</AxButton>
              <AxButton size="sm" variant="secondary" onClick={() => AxNube.set({ mCuenta: 'no' })}>No, todavía no</AxButton>
            </div>
          )}
        </NbPaso>
        <NbPaso n={2} titulo={'Instala ' + nube.app} estado={est(2)} resumen={nube.id === 'gdrive' ? 'Instalada, con el acceso directo en Mi unidad' : 'Instalada'}>
          <NbNota>Instálala e inicia sesión con tu cuenta. Es la que baja la carpeta del despacho a esta computadora.</NbNota>
          {nube.id === 'gdrive' || nube.id === 'onedrive' ? (
            <NbAviso tono="info" icono="info" titulo={nube.id === 'gdrive' ? 'Con Google Drive, un paso más' : 'Con OneDrive, un paso más'}>
              {nube.miembroExtra}
            </NbAviso>
          ) : <NbNota>{nube.miembroExtra}</NbNota>}
          <div><AxButton size="sm" variant="primary" icon="check" onClick={() => AxNube.set({ mPaso: 'carpeta', mAcceso: true })}>{nube.id === 'icloud' ? 'Ya la instalé y acepté la carpeta' : 'Ya la instalé y agregué el acceso directo'}</AxButton></div>
        </NbPaso>
        <NbPaso n={3} titulo="Elige la carpeta del despacho" estado={est(3)}>
          {n.mPaso === 'verificando' ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, color: 'var(--ax-fg-strong)' }}>
              <span className="ax-spinner" style={{ width: 13, height: 13 }}/> Comprobando el marcador de la carpeta…
            </div>
          ) : (
            <>
              {n.mRechazo ? (
                <NbAviso tono="bad" icono="alert" dataNb="rechazo" titulo={n.mRechazo === 'ajena' ? 'Esa carpeta es de otro despacho' : 'Esa carpeta no es la del despacho'}>
                  {n.mRechazo === 'ajena'
                    ? 'Tiene el marcador de otra instalación de Piixi, no el de ' + NB_DESPACHO + '. '
                    : 'No tiene el marcador de ' + NB_DESPACHO + '. '}
                  Busca «{NB_CARPETA}»{nube.id === 'gdrive' ? ' en Mi unidad (es el acceso directo que agregaste)' : ''} y elígela. Si no aparece, pídele a {NB_PRINCIPAL} que te la comparta otra vez.
                </NbAviso>
              ) : null}
              {!compartida ? <NbNota>Ojo: {NB_PRINCIPAL} todavía no marca que te la compartió. Si no la ves, pídesela.</NbNota> : null}
              <div><AxButton variant="primary" icon="folder" onClick={() => AxNube.set({ dialogo: 'miembro' })}>Elegir la carpeta…</AxButton></div>
            </>
          )}
        </NbPaso>
      </div>
    </NbTarjeta>
  );
}

// ─────────────────────────────────────────────────────────────
// PANTALLA · /ajustes/nube
// ─────────────────────────────────────────────────────────────
function NubeScreen() {
  useNube();
  const eq = useEquipo();
  const { pushToast } = useAxStore();
  const { navigate } = useHashRoute();
  window.__nbToast = pushToast;
  const miembro = eq.ver === 'miembro';
  return (
    <div style={{ flex: 1, overflow: 'auto' }} className="ax-fade-in">
      <AxPageHeader eyebrow="Ajustes › Equipo"
        title={miembro ? 'Nube del despacho' : 'Sincronizar por tu nube'}
        subtitle={miembro ? 'Cómo entra esta computadora a la carpeta que comparte la computadora principal.' : 'Tu Google Drive, OneDrive o iCloud como camino de respaldo entre las computadoras del equipo.'}
        actions={<AxButton variant="tertiary" icon="arrow-left" onClick={() => navigate('/equipo-sincronizacion')}>Ver la sincronización del equipo</AxButton>}/>
      <div style={{ padding: '24px 32px 48px', maxWidth: 900, display: 'flex', flexDirection: 'column', gap: 18 }}>
        {miembro ? <NbMiembro/> : <NbPrincipal/>}
      </div>
      <NbDialogoCarpeta/>
      <NbPermisoMac/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// RESPALDO AUTOMÁTICO · /ajustes/respaldo
// ─────────────────────────────────────────────────────────────
function RpContrasenaForm({ titulo, boton, onListo, nota, cancelar }) {
  const [a, setA] = useState('');
  const [b, setB] = useState('');
  const [apunte, setApunte] = useState(false);
  const err = a && b && a !== b ? 'Las contraseñas no coinciden.' : a && a.length < 8 ? 'Usa al menos 8 caracteres.' : null;
  const listo = a && b && !err && apunte;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }} data-nb="form-contrasena">
      {titulo ? <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>{titulo}</div> : null}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <AxField label="Contraseña del respaldo"><AxInput type="password" value={a} onChange={(e) => setA(e.target.value)} placeholder="Al menos 8 caracteres"/></AxField>
        <AxField label="Repítela" error={err}><AxInput type="password" value={b} onChange={(e) => setB(e.target.value)}/></AxField>
      </div>
      <NbNota>{nota}</NbNota>
      <label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 13, color: 'var(--ax-fg)' }}>
        <input type="checkbox" checked={apunte} onChange={(e) => setApunte(e.target.checked)} style={{ marginTop: 3 }}/>
        <span>La apunté en un lugar seguro, fuera de esta computadora.</span>
      </label>
      <div style={{ display: 'flex', gap: 8 }}>
        <AxButton variant="primary" icon="check" disabled={!listo} onClick={() => onListo()}>{boton}</AxButton>
        {cancelar ? <AxButton variant="tertiary" onClick={cancelar}>Cancelar</AxButton> : null}
      </div>
    </div>
  );
}

const RP_NOTA_LLAVERO = 'Piixi la guarda en el llavero de esta computadora para respaldar cada noche sin pedírtela. Si esta computadora se descompone, el llavero se va con ella: para recuperar tu cartera en otra vas a necesitar esta contraseña. Piixi no la puede recuperar por ti.';

function RpConfigurar() {
  const r = useResp();
  useNube();
  const sug = AxResp.sugerida();
  const [eligiendo, setEligiendo] = useState(false);
  const est = (k) => ({ carpeta: 1, contrasena: 2 }[r.paso] > k ? 'hecho' : { carpeta: 1, contrasena: 2 }[r.paso] === k ? 'activo' : 'pendiente');
  return (
    <div>
      <NbPaso n={1} titulo="Dónde se guardan las copias" estado={est(1)} resumen={r.carpeta}>
        {sug ? (
          <NbAviso tono="ok" icono="check" titulo={'Te sugerimos una carpeta en tu ' + NB_NUBES[sug.nube].nombre}>
            <span className="ax-mono" style={{ fontSize: 12 }}>{sug.ruta}</span>
            <div style={{ marginTop: 4 }}>Así la copia sobrevive si esta computadora se descompone. Es una carpeta tuya, aparte de la del equipo: no se comparte con nadie.</div>
          </NbAviso>
        ) : (
          <NbAviso tono="warm" icono="search" titulo="No encontramos Google Drive, OneDrive ni iCloud en esta computadora">
            Puedes elegir cualquier carpeta, pero si es de esta misma computadora, la copia se pierde con ella. Mejor instala la app de tu nube y elige su carpeta.
          </NbAviso>
        )}
        {eligiendo ? (
          <div style={{ border: '1px dashed var(--ax-border-strong)', borderRadius: 8, padding: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
            <AxEyebrow>Diálogo del sistema (simulado) · elige una carpeta</AxEyebrow>
            {['gdrive', 'onedrive', 'icloud'].map((id) => (
              <AxButton key={id} size="sm" variant="secondary" onClick={() => { setEligiendo(false); AxResp.usarCarpeta(id, AxNube.ruta(id, AxNube.get().soP) + '/' + NB_RESP_CARPETA); }}>
                {NB_NUBES[id].nombre} › {NB_RESP_CARPETA}
              </AxButton>
            ))}
            <AxButton size="sm" variant="tertiary" onClick={() => { setEligiendo(false); AxResp.usarCarpeta(null, '~/Documents/' + NB_RESP_CARPETA); }}>Documentos › {NB_RESP_CARPETA} (en esta computadora)</AxButton>
          </div>
        ) : null}
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {sug ? <AxButton variant="primary" icon="check" onClick={() => AxResp.usarCarpeta(sug.nube, sug.ruta)}>Usar esta carpeta</AxButton> : null}
          <AxButton variant="secondary" icon="folder" onClick={() => setEligiendo(!eligiendo)}>Elegir otra carpeta…</AxButton>
        </div>
      </NbPaso>
      <NbPaso n={2} titulo="La contraseña del respaldo" estado={est(2)}>
        {r.nubeCarpeta === null && r.carpeta ? (
          <NbAviso tono="warm" icono="alert" titulo="Esa carpeta está en esta computadora">Si se descompone, la copia se pierde con ella. Puedes seguir, pero te lo vamos a recordar en Ajustes.</NbAviso>
        ) : null}
        <RpContrasenaForm boton="Encender el respaldo automático" nota={RP_NOTA_LLAVERO} onListo={() => AxResp.encender()}
          cancelar={() => AxResp.set({ paso: 'carpeta' })}/>
      </NbPaso>
    </div>
  );
}

function RpEstado() {
  const r = useResp();
  useNube();
  const nube = AxResp.nube();
  const ult = AxResp.ultimo();
  const fallo = AxResp.fallo();
  const caida = AxResp.nubeCaida();
  const esp = AxResp.espacio();
  const [cambiando, setCambiando] = useState(false);
  const [apagando, setApagando] = useState(false);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {r.corriendo ? (
        <div data-nb="respaldando" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>Respaldando… {Math.round(r.pct * 100)} %</div>
          <NbBarra valor={r.pct}/>
          <NbNota>Puedes seguir trabajando: la copia se toma sin detener Piixi.</NbNota>
        </div>
      ) : fallo ? (
        <NbAviso tono="bad" icono="alert" dataNb={'falla-' + fallo.motivo}
          titulo={(fallo.dia === r.hoy ? 'El respaldo de anoche' : 'El último respaldo') + ' no se hizo: ' + (fallo.motivo === 'carpeta' ? 'la carpeta ya no existe' : 'no cabe')}
          acciones={fallo.motivo === 'carpeta' ? <>
            <AxButton size="sm" variant="primary" icon="folder" onClick={() => AxResp.elegirCarpetaOtraVez()}>Elegir la carpeta otra vez</AxButton>
            <AxButton size="sm" variant="secondary" icon="refresh" onClick={() => AxResp.correr('ahora')}>Reintentar</AxButton>
          </> : <>
            <AxButton size="sm" variant="primary" onClick={() => AxNube._toast({ kind: 'info', message: 'En la app real se abre ' + (nube ? nube.sitio : 'tu nube') + ' para liberar espacio.' })}>Liberar espacio en {nube ? nube.nombre : 'tu nube'}</AxButton>
            <AxButton size="sm" variant="secondary" icon="folder" onClick={() => setCambiando(true)}>Elegir otra carpeta</AxButton>
          </>}>
          {fallo.motivo === 'carpeta'
            ? 'No encontramos «' + NB_RESP_CARPETA + '» en ' + (nube ? 'tu ' + nube.nombre : 'la ruta de siempre') + ': ¿la moviste, le cambiaste el nombre o la borraste? Elígela de nuevo o elige otra. '
            : 'Faltan unos ' + (ult ? ult.mb : 48) + ' MB en tu ' + (nube ? nube.nombre : 'carpeta') + '. ' + (nube ? nube.llena + ' ' : '')}
          {ult ? 'Tu copia más reciente sigue siendo la de ' + rpFecha(ult.dia, r.hoy).toLowerCase() + ', ' + ult.hora : ''}
        </NbAviso>
      ) : ult ? (
        <NbAviso tono="ok" icono="check" dataNb="ultimo" titulo={'Último respaldo: ' + rpFecha(ult.dia, r.hoy) + ', ' + ult.hora}>
          {ult.mb} MB · <span className="ax-mono" style={{ fontSize: 12 }}>{ult.archivo}</span>
        </NbAviso>
      ) : (
        <NbAviso tono="info" icono="info" dataNb="sin-copias" titulo="Encendido. El primero corre esta noche.">
          Si quieres comprobarlo ya, haz uno ahora.
        </NbAviso>
      )}
      {caida && !r.corriendo ? (
        <NbAviso tono="warm" icono="alert" dataNb="respaldo-sin-subir" titulo={nube.nombre + ' no responde en esta computadora'}
          acciones={<AxButton size="sm" variant="secondary" icon="refresh" onClick={() => AxNube.reconectar()}>Reintentar</AxButton>}>
          {ult && !ult.subida ? 'La copia de ' + rpFecha(ult.dia, r.hoy).toLowerCase() + ' quedó en la carpeta, pero todavía no sube a la nube: mientras, sólo existe en esta computadora. ' : 'Las copias se siguen escribiendo en la carpeta, pero no suben a la nube hasta que ' + nube.nombre + ' vuelva. '}
          Abre {nube.app} o vuelve a iniciar sesión.
        </NbAviso>
      ) : null}
      {esp === 'casi' && !fallo && nube ? (
        <NbAviso tono="warm" icono="alert" dataNb="respaldo-espacio" titulo={'Tu ' + nube.nombre + ' está casi lleno'}>
          El próximo respaldo ({ult ? ult.mb : 48} MB) todavía cabe. {nube.llena}
        </NbAviso>
      ) : null}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <NbFila etiqueta="Cuándo corre">Cada noche, sólo en esta computadora (la principal). Si estuvo apagada, corre en cuanto vuelvas a abrir Piixi.</NbFila>
        <NbFila etiqueta="Carpeta">
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
            {nube ? <NbNubeIcono id={nube.id} size={14}/> : null}
            <span className="ax-mono" style={{ fontSize: 12 }}>{r.carpeta}</span>
          </span>
        </NbFila>
        <NbFila etiqueta="Copias">Se guardan las últimas 7; la más vieja se borra sola. {r.copias.length ? 'Hoy hay ' + r.copias.length + '.' : ''}</NbFila>
        <NbFila etiqueta="Contraseña">
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><span style={{ color: 'var(--ax-accent)' }}><NbCandado size={14}/></span>Guardada en el llavero de esta computadora</span>
        </NbFila>
      </div>
      {cambiando ? (
        <div style={{ border: '1px dashed var(--ax-border-strong)', borderRadius: 8, padding: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
          <AxEyebrow>Diálogo del sistema (simulado) · elige una carpeta</AxEyebrow>
          {['gdrive', 'onedrive', 'icloud'].map((id) => (
            <AxButton key={id} size="sm" variant="secondary" onClick={() => { setCambiando(false); AxResp.set({ nubeCarpeta: id, carpeta: AxNube.ruta(id, AxNube.get().soP) + '/' + NB_RESP_CARPETA, carpetaExiste: true, intento: null }); }}>
              {NB_NUBES[id].nombre} › {NB_RESP_CARPETA}
            </AxButton>
          ))}
        </div>
      ) : null}
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        <AxButton variant="secondary" icon="refresh" disabled={r.corriendo} onClick={() => AxResp.correr('ahora')}>Respaldar ahora</AxButton>
        <AxButton variant="tertiary" icon="folder" onClick={() => setCambiando(!cambiando)}>Cambiar carpeta</AxButton>
        <AxButton variant="tertiary" onClick={() => AxResp.set({ cambiandoClave: true })}>Cambiar contraseña</AxButton>
        <AxButton variant="tertiary" icon="x" onClick={() => setApagando(true)}>Apagar</AxButton>
      </div>
      {r.cambiandoClave ? (
        <div style={{ border: '1px solid var(--ax-border)', borderRadius: 8, padding: 14 }}>
          <RpContrasenaForm titulo="Nueva contraseña del respaldo" boton="Guardar la nueva" nota="Los respaldos siguientes se sellan con la nueva. Las copias que ya están en la carpeta siguen abriendo con la anterior."
            onListo={() => { AxResp.set({ cambiandoClave: false, comprobar: false }); AxNube._toast({ kind: 'success', title: 'Contraseña cambiada', message: 'Quedó en el llavero. El próximo respaldo ya se sella con ella.' }); }}
            cancelar={() => AxResp.set({ cambiandoClave: false })}/>
        </div>
      ) : null}
      <AxModal open={apagando} onClose={() => setApagando(false)} title="Apagar el respaldo automático" width={460}
        footer={<><AxButton variant="secondary" onClick={() => setApagando(false)}>Cancelar</AxButton><AxButton variant="primary" onClick={() => { setApagando(false); AxResp.apagar(); }}>Apagar</AxButton></>}>
        <div style={{ fontSize: 13, color: 'var(--ax-fg)', lineHeight: 1.55 }}>
          Piixi deja de respaldar cada noche y borra la contraseña del llavero de esta computadora. Las copias que ya están en tu carpeta se quedan ahí y siguen abriendo con su contraseña.
        </div>
      </AxModal>
    </div>
  );
}

function RpComprobar() {
  const r = useResp();
  const [t, setT] = useState('');
  const [olvido, setOlvido] = useState(false);
  if (!r.comprobar || r.estado !== 'encendido') return null;
  return (
    <NbAviso tono="info" icono="candado" dataNb="comprobar" titulo="¿Todavía recuerdas la contraseña del respaldo?">
      Pasaron 30 días. Si esta computadora se descompone, esa contraseña es lo único que te deja recuperar tu cartera en otra. Escríbela para comprobarlo.
      {olvido ? (
        <div style={{ marginTop: 10 }}>
          <RpContrasenaForm boton="Guardar la nueva" nota="Los respaldos siguientes se sellan con la nueva. Las copias viejas siguen abriendo con la anterior."
            onListo={() => { setOlvido(false); AxResp.set({ comprobar: false }); AxNube._toast({ kind: 'success', title: 'Contraseña cambiada', message: 'El respaldo de esta noche ya se sella con la nueva.' }); }}
            cancelar={() => setOlvido(false)}/>
        </div>
      ) : (
        <div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' }}>
          <div style={{ width: 240 }}><AxInput type="password" value={t} onChange={(e) => setT(e.target.value)} placeholder="Contraseña del respaldo"/></div>
          <AxButton size="sm" variant="primary" disabled={!t} onClick={() => { AxResp.set({ comprobar: false }); setT(''); AxNube._toast({ kind: 'success', title: 'Es correcta', message: 'Te volvemos a preguntar en 30 días.' }); }}>Comprobar</AxButton>
          <AxButton size="sm" variant="tertiary" onClick={() => setOlvido(true)}>La olvidé: elegir otra</AxButton>
        </div>
      )}
    </NbAviso>
  );
}

function RpRestaurar() {
  const r = useResp();
  const [sel, setSel] = useState(null);
  const [modal, setModal] = useState(null); // null | confirmar | llavero | aplicando | listo
  const [pct, setPct] = useState(0);
  const [clave, setClave] = useState('');
  const [error, setError] = useState(null);
  const [archivo, setArchivo] = useState(null);
  const copias = r.copias.slice().reverse();
  const copia = copias.find((c) => c.dia === sel) || copias[0] || null;
  const aplicar = () => {
    setModal('aplicando'); setPct(0);
    let v = 0;
    const id = setInterval(() => { v = Math.min(1, v + 0.1); setPct(v); if (v >= 1) { clearInterval(id); setModal('listo'); } }, 160);
  };
  const conClave = () => { if (!r.abre) { setError('La contraseña no abre este respaldo.'); return; } setError(null); aplicar(); };
  const cerrar = () => { setModal(null); setClave(''); setError(null); };
  const otra = r.donde === 'otra';
  const usaLlavero = !otra && r.qRestaurar === 'A' && r.estado === 'encendido';
  return (
    <NbTarjeta titulo="Restaurar" icono="refresh" dataNb="restaurar" id="restaurar"
      derecha={<AxPill tone="ghost">{otra ? 'En otra computadora' : 'En esta computadora'}</AxPill>}>
      {otra ? (
        <>
          <NbNota>En una computadora nueva no está tu llavero. Instala Piixi, elige el archivo del respaldo desde tu nube y escribe la contraseña que elegiste al encender el respaldo automático.</NbNota>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            <AxButton variant="secondary" icon="folder" onClick={() => setArchivo((copias[0] || rpCopia(r.hoy)).archivo)}>Elegir el archivo…</AxButton>
            {archivo ? <span className="ax-mono" style={{ fontSize: 12, color: 'var(--ax-fg-strong)' }}>{archivo} · {AxResp.nube() ? AxResp.nube().nombre : 'tu nube'} › {NB_RESP_CARPETA}</span> : null}
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
            <div style={{ width: 260 }}><AxField label="Contraseña del respaldo" error={error}><AxInput type="password" value={clave} onChange={(e) => { setClave(e.target.value); setError(null); }}/></AxField></div>
            <AxButton variant="primary" disabled={!archivo || !clave} onClick={conClave}>Restaurar</AxButton>
          </div>
        </>
      ) : copias.length ? (
        <>
          <div style={{ border: '1px solid var(--ax-border)', borderRadius: 8 }}>
            {copias.map((c, i) => (
              <label key={c.dia} data-nb="copia" style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '9px 14px', borderTop: i ? '1px solid var(--ax-border)' : 'none', fontSize: 13, color: 'var(--ax-fg-strong)' }}>
                <input type="radio" name="rp-copia" checked={copia && copia.dia === c.dia} onChange={() => setSel(c.dia)}/>
                <span style={{ flex: 1 }}>{rpFecha(c.dia, r.hoy)}, {c.hora}</span>
                {!c.subida ? <AxPill tone="warm">Sin subir</AxPill> : null}
                <span className="ax-mono" style={{ fontSize: 11, color: 'var(--ax-fg-muted)' }}>{c.mb} MB</span>
              </label>
            ))}
          </div>
          <div><AxButton variant="primary" icon="refresh" disabled={!copia} onClick={() => setModal('confirmar')}>Restaurar esta copia</AxButton></div>
        </>
      ) : (
        <NbNota>Todavía no hay copias en la carpeta. Cuando corra el primer respaldo, aquí las verás.</NbNota>
      )}
      <AxModal open={!!modal} onClose={modal === 'aplicando' ? () => {} : cerrar} width={480}
        title={modal === 'listo' ? 'Cartera restaurada' : 'Restaurar la copia de ' + (copia ? rpFecha(copia.dia, r.hoy).toLowerCase() + ', ' + copia.hora : '')}
        footer={modal === 'confirmar' ? <>
          <AxButton variant="secondary" onClick={cerrar}>Cancelar</AxButton>
          {usaLlavero
            ? <AxButton variant="primary" onClick={() => setModal('llavero')}>Restaurar</AxButton>
            : <AxButton variant="primary" disabled={!clave} onClick={conClave}>Restaurar</AxButton>}
        </> : modal === 'listo' ? <AxButton variant="primary" onClick={cerrar}>Listo</AxButton> : null}>
        {modal === 'confirmar' ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12, fontSize: 13, color: 'var(--ax-fg)', lineHeight: 1.55 }}>
            <div>Restaurar reemplaza la cartera de esta computadora por la de esa copia. La cartera de hoy se guarda aparte por si necesitas regresar.</div>
            {usaLlavero ? (
              <NbAviso tono="info" icono="candado">Piixi usa la contraseña guardada en el llavero de esta computadora: no tienes que escribirla. El sistema te va a pedir permiso.</NbAviso>
            ) : (
              <AxField label="Contraseña del respaldo" error={error}><AxInput type="password" value={clave} onChange={(e) => { setClave(e.target.value); setError(null); }}/></AxField>
            )}
          </div>
        ) : modal === 'aplicando' ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>Restaurando… Piixi se va a reiniciar. {Math.round(pct * 100)} %</div>
            <NbBarra valor={pct}/>
          </div>
        ) : modal === 'listo' ? (
          <div style={{ fontSize: 13, color: 'var(--ax-fg)', lineHeight: 1.55 }}>Piixi se reinició con la cartera de esa copia. La de antes quedó guardada aparte.</div>
        ) : null}
      </AxModal>
      <NbSistema open={modal === 'llavero'} titulo="Piixi quiere usar tu información confidencial guardada en «piixi-cartera» en tu llavero."
        botones={<><NbSisBoton onClick={() => setModal('confirmar')}>Denegar</NbSisBoton><NbSisBoton primario onClick={aplicar}>Permitir</NbSisBoton></>}>
        <div style={{ color: '#3a3a3c' }}>Para permitirlo, escribe la contraseña de esta computadora o usa Touch ID. (En Windows: «Confirma que eres tú» con tu PIN.)</div>
        <div style={{ marginTop: 10, background: '#fff', border: '1px solid rgba(0,0,0,.2)', borderRadius: 6, padding: '6px 10px', color: '#86868b' }}>••••••••</div>
      </NbSistema>
    </NbTarjeta>
  );
}

function RpManual() {
  const { pushToast } = useAxStore();
  const r = useResp();
  const [a, setA] = useState('');
  const [b, setB] = useState('');
  const err = a && b && a !== b ? 'Las contraseñas no coinciden.' : null;
  return (
    <NbTarjeta titulo="Guardar un respaldo a mano" icono="upload" dataNb="manual" id="manual">
      <NbNota>Un solo archivo .piixi, protegido con una contraseña que eliges ahora. Piixi no guarda esta contraseña ni la puede recuperar.</NbNota>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <AxField label="Contraseña"><AxInput type="password" value={a} onChange={(e) => setA(e.target.value)}/></AxField>
        <AxField label="Repite la contraseña" error={err}><AxInput type="password" value={b} onChange={(e) => setB(e.target.value)}/></AxField>
      </div>
      <div><AxButton variant="secondary" disabled={!a || !b || !!err} onClick={() => { setA(''); setB(''); pushToast({ kind: 'success', title: 'Respaldo guardado', message: rpArchivo(r.hoy) + ' quedó en Descargas.' }); }}>Guardar respaldo</AxButton></div>
    </NbTarjeta>
  );
}

function RespaldoScreen() {
  const r = useResp();
  useNube();
  const eq = useEquipo();
  const { pushToast } = useAxStore();
  const { path } = useHashRoute();
  window.__nbToast = pushToast;
  const miembro = eq.ver === 'miembro';
  useEffect(() => {
    const q = path.split('?')[1] || '';
    const ir = (q.match(/ir=([a-z]+)/) || [])[1];
    if (ir) { const el = document.getElementById(ir); if (el) el.scrollIntoView({ block: 'start' }); }
  }, [path]);
  const pill = r.estado === 'encendido'
    ? (AxResp.fallo() ? <AxPill tone="danger">Falló</AxPill> : <AxPill tone="accent" dot>Encendido</AxPill>)
    : r.estado === 'configurando' ? <AxPill tone="warm">Configurando</AxPill> : <AxPill tone="ghost">Apagado</AxPill>;
  return (
    <div style={{ flex: 1, overflow: 'auto' }} className="ax-fade-in">
      <AxPageHeader eyebrow="Ajustes" title="Respaldo de tu cartera"
        subtitle="Una copia sellada de tu cartera. Con el archivo y la contraseña la recuperas en otra computadora."/>
      <div style={{ padding: '24px 32px 48px', maxWidth: 900, display: 'flex', flexDirection: 'column', gap: 18 }}>
        <RpComprobar/>
        {miembro ? (
          <NbTarjeta titulo="Respaldo automático" icono="candado" dataNb="respaldo-miembro" derecha={<AxPill tone="ghost">Lo hace la principal</AxPill>}>
            <NbAviso tono="info" icono="info" titulo={'El respaldo automático corre en la computadora principal (' + NB_PRINCIPAL + ')'}>
              Es la que tiene la cartera completa del despacho. Desde aquí puedes guardar un respaldo a mano cuando quieras.
            </NbAviso>
          </NbTarjeta>
        ) : (
          <NbTarjeta titulo="Respaldo automático" icono="candado" dataNb="respaldo-auto" derecha={pill}>
            {r.estado === 'apagado' ? (
              <>
                <NbNota>Cada noche, Piixi guarda una copia sellada de tu cartera en una carpeta de tu nube (Google Drive, OneDrive o iCloud). Así, si esta computadora se descompone o la cambias, tu trabajo no se pierde.</NbNota>
                <div><AxButton variant="primary" icon="candado" onClick={() => AxResp.empezar()}>Encender el respaldo automático</AxButton></div>
              </>
            ) : r.estado === 'configurando' ? <RpConfigurar/> : <RpEstado/>}
          </NbTarjeta>
        )}
        {!miembro ? <RpRestaurar/> : null}
        <RpManual/>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SUPERFICIES EN OTRAS PANTALLAS (estado vivo)
// ─────────────────────────────────────────────────────────────

// Ajustes › Equipo: la fila de la nube (principal) o de la nube del despacho (miembro)
function NubeFilaAjustes(miembro) {
  const n = AxNube.get();
  const nube = AxNube.info();
  if (miembro) {
    if (!AxNube.configurada()) return { label: 'Nube del despacho', value: 'La principal todavía no la elige', pill: { tone: 'ghost', label: 'sin nube' }, linkTo: '/ajustes/nube' };
    if (n.mPaso !== 'unida') return { label: 'Nube del despacho', value: nube.nombre + ' de ' + NB_PRINCIPAL + ' · falta que esta computadora entre', pill: { tone: 'warm', label: 'falta entrar' }, linkTo: '/ajustes/nube' };
    if (n.conexion !== 'ok') return { label: 'Nube del despacho', value: nube.nombre + ' no responde en esta computadora', pill: { tone: 'danger', label: 'revisar' }, linkTo: '/ajustes/nube' };
    return { label: 'Nube del despacho', value: nube.nombre + ' de ' + NB_PRINCIPAL + ' · esta computadora ya entró', pill: { tone: 'accent', label: 'on' }, linkTo: '/ajustes/nube' };
  }
  if (!AxNube.configurada()) {
    return { label: 'Sincronizar por tu nube', value: n.nube ? 'A medias: falta ' + (n.paso === 'compartir' ? 'compartir la carpeta' : 'terminar') : 'Sin configurar · tu Google Drive, OneDrive o iCloud cuando no se alcanzan directo', pill: { tone: n.nube ? 'warm' : 'ghost', label: n.nube ? 'a medias' : 'off' }, linkTo: '/ajustes/nube' };
  }
  const unidos = n.miembros.filter((m) => m.estado === 'unida').length;
  const base = nube.nombre + ' · ' + unidos + ' de ' + n.miembros.length + ' miembros ya entraron';
  if (n.conexion !== 'ok') return { label: 'Sincronizar por tu nube', value: nube.nombre + ' no responde en esta computadora', pill: { tone: 'danger', label: 'revisar' }, linkTo: '/ajustes/nube' };
  if (n.espacio !== 'normal') return { label: 'Sincronizar por tu nube', value: base + ' · ' + (n.espacio === 'lleno' ? 'lleno' : 'casi lleno'), pill: { tone: n.espacio === 'lleno' ? 'danger' : 'warm', label: 'espacio' }, linkTo: '/ajustes/nube' };
  return { label: 'Sincronizar por tu nube', value: base, pill: { tone: 'accent', label: 'on' }, linkTo: '/ajustes/nube' };
}

// Ajustes: tarjeta «Respaldo de tu cartera»
function RespaldoTarjeta() {
  const r = useResp();
  useNube();
  const eq = useEquipo();
  const { navigate } = useHashRoute();
  const ult = AxResp.ultimo();
  const fallo = AxResp.fallo();
  const nube = AxResp.nube();
  let fila;
  if (eq.ver === 'miembro') fila = { label: 'Respaldo automático', value: 'Lo hace la computadora principal (' + NB_PRINCIPAL + ')', pill: { tone: 'ghost', label: 'principal' }, linkTo: '/ajustes/respaldo' };
  else if (r.estado !== 'encendido') fila = { label: 'Respaldo automático', value: r.estado === 'configurando' ? 'A medias: falta la contraseña' : 'Apagado · hoy, si esta computadora se descompone, la cartera se pierde', pill: { tone: 'warm', label: 'off' }, linkTo: '/ajustes/respaldo' };
  else if (fallo) fila = { label: 'Respaldo automático', value: 'El de anoche no se hizo: ' + (fallo.motivo === 'carpeta' ? 'la carpeta ya no existe' : 'no cabe en tu ' + (nube ? nube.nombre : 'nube')), pill: { tone: 'danger', label: 'falló' }, linkTo: '/ajustes/respaldo' };
  else if (r.comprobar) fila = { label: 'Respaldo automático', value: 'Encendido · comprueba que recuerdas la contraseña', pill: { tone: 'info', label: 'comprobar' }, linkTo: '/ajustes/respaldo' };
  else fila = { label: 'Respaldo automático', value: ult ? 'Último: ' + rpFecha(ult.dia, r.hoy).toLowerCase() + ', ' + ult.hora + (nube ? ' · en tu ' + nube.nombre : '') : 'Encendido · el primero corre esta noche', pill: { tone: 'accent', label: 'on' }, linkTo: '/ajustes/respaldo' };
  const g = { id: 'respaldo', label: 'Respaldo de tu cartera', icon: 'shield', items: [
    fila,
    { label: 'Restaurar', value: 'Desde una copia de tu carpeta, o desde un archivo en otra computadora', linkTo: '/ajustes/respaldo?ir=restaurar' },
    { label: 'Guardar a mano', value: 'Un archivo .piixi con la contraseña que elijas', linkTo: '/ajustes/respaldo?ir=manual' },
  ] };
  return window.SettingsGroupCard ? <SettingsGroupCard group={g} navigate={navigate}/> : null;
}

// Tu cuenta › Dónde viven tus datos: el aviso de respaldo deja de ser «aún no disponible»
function RespaldoEnCuenta() {
  const r = useResp();
  useNube();
  const { navigate } = useHashRoute();
  const ult = AxResp.ultimo();
  const fallo = AxResp.fallo();
  const nube = AxResp.nube();
  const on = r.estado === 'encendido';
  const tono = on && !fallo ? { b: 'var(--ax-accent-line)', bg: 'var(--ax-accent-soft)', c: 'var(--ax-accent)', i: 'check' }
    : fallo ? { b: 'var(--ax-danger)', bg: 'var(--ax-danger-soft)', c: 'var(--ax-danger)', i: 'alert' }
    : { b: 'var(--ax-warm-line)', bg: 'var(--ax-warm-soft)', c: 'var(--ax-warm)', i: 'alert' };
  return (
    <div data-nb="respaldo-cuenta" style={{ marginTop: 12, display: 'flex', alignItems: 'flex-start', gap: 10, border: '1px solid ' + tono.b, background: tono.bg, borderRadius: 6, padding: '12px 14px' }}>
      <AxIcon name={tono.i} size={15} style={{ color: tono.c, flexShrink: 0, marginTop: 1 }}/>
      <div style={{ flex: 1, fontSize: 12, color: 'var(--ax-fg)', lineHeight: 1.6 }}>
        {on && !fallo ? (
          <><span style={{ color: 'var(--ax-fg-strong)' }}>Respaldo automático encendido. </span>{ult ? 'Último: ' + rpFecha(ult.dia, r.hoy).toLowerCase() + ', ' + ult.hora + (nube ? ', en tu ' + nube.nombre + '.' : '') : 'El primero corre esta noche.'} Si esta computadora se descompone, recuperas tu cartera con el archivo y tu contraseña.</>
        ) : fallo ? (
          <><span style={{ color: 'var(--ax-fg-strong)' }}>El respaldo de anoche no se hizo. </span>{fallo.motivo === 'carpeta' ? 'La carpeta ya no existe.' : 'No cupo en tu ' + (nube ? nube.nombre : 'nube') + '.'}</>
        ) : (
          <><span style={{ color: 'var(--ax-fg-strong)' }}>Lo que eso cuesta: </span>hoy no hay respaldo. Si esta computadora se descompone, la cartera se pierde. Respaldar y restaurar es la contraparte honesta de guardar todo aquí.</>
        )}
        <div style={{ marginTop: 8 }}>
          <AxButton variant="secondary" size="sm" icon="cloud" onClick={() => navigate('/ajustes/respaldo')}>{on ? 'Ver el respaldo' : 'Configurar respaldo'}</AxButton>
        </div>
      </div>
    </div>
  );
}

// Equipo › sincronización: la línea de la nube del despacho, arriba
function NbTarjetaEquipo() {
  const n = useNube();
  const eq = useEquipo();
  const { navigate } = useHashRoute();
  const nube = AxNube.info();
  const miembro = eq.ver === 'miembro';
  let texto, pill;
  if (!AxNube.configurada()) {
    texto = miembro ? 'El despacho todavía no tiene nube: si no se alcanzan directo, los cambios esperan.' : 'Sin nube del despacho: si las computadoras no se alcanzan directo, los cambios esperan. Configúrala para que viajen por tu Google Drive, OneDrive o iCloud.';
    pill = <AxPill tone="ghost">Sin nube</AxPill>;
  } else if (n.conexion !== 'ok') {
    texto = nube.nombre + ' no responde en esta computadora. Mientras, sólo directo.'; pill = <AxPill tone="danger">No responde</AxPill>;
  } else if (miembro && n.mPaso !== 'unida') {
    texto = 'El despacho usa el ' + nube.nombre + ' de ' + NB_PRINCIPAL + ', pero esta computadora todavía no entra a la carpeta.'; pill = <AxPill tone="warm">Falta entrar</AxPill>;
  } else {
    const espTxt = (!miembro || n.qEspacio === 'B') && n.espacio !== 'normal' ? ' · ' + (n.espacio === 'lleno' ? 'lleno' : 'casi lleno') : '';
    texto = (miembro ? 'Si no te alcanzas directo con la principal, todo viaja por el ' + nube.nombre + ' del despacho, sellado.' : 'Si no se alcanzan directo, todo viaja por tu ' + nube.nombre + ', sellado. Ningún dato pasa por Piixi.') + espTxt;
    pill = espTxt ? <AxPill tone={n.espacio === 'lleno' ? 'danger' : 'warm'}>{n.espacio === 'lleno' ? 'Lleno' : 'Casi lleno'}</AxPill> : <AxPill tone="accent" dot>{nube.nombre}</AxPill>;
  }
  return (
    <div data-nb="equipo-nube" style={{ background: 'var(--ax-bg-2)', border: '1px solid var(--ax-border)', borderRadius: 10, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
      <span style={{ width: 30, height: 30, borderRadius: 7, background: 'var(--ax-bg-3)', border: '1px solid var(--ax-border-strong)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
        {nube ? <NbNubeIcono id={nube.id} size={17}/> : <AxIcon name="cloud" size={16}/>}
      </span>
      <div style={{ flex: '1 1 320px', minWidth: 0 }}>
        <div style={{ fontSize: 14, color: 'var(--ax-fg-strong)' }}>{miembro ? 'Nube del despacho' : 'Sincronizar por tu nube'}</div>
        <div style={{ fontSize: 12.5, color: 'var(--ax-fg-muted)', marginTop: 2, lineHeight: 1.5 }}>{texto}</div>
        {n.pendientes.length && AxNube.viaDisponible() ? <div style={{ fontSize: 12.5, color: 'var(--ax-info)', marginTop: 2 }}>1 archivo todavía no llega; se reintenta solo.</div> : null}
      </div>
      {pill}
      <AxButton size="sm" variant="secondary" iconRight="chevron-right" onClick={() => navigate('/ajustes/nube')}>{AxNube.configurada() && (!miembro || n.mPaso === 'unida') ? 'Ver' : miembro ? 'Entrar' : 'Configurar'}</AxButton>
    </div>
  );
}

// Equipo › sincronización › miembro: el aviso cuando la red no deja la directa
function NbAvisoMiembro() {
  const n = useNube();
  const { navigate } = useHashRoute();
  const nube = AxNube.info();
  const ir = <div style={{ marginTop: 8 }}><AxButton size="sm" variant="secondary" onClick={() => navigate('/ajustes/nube')}>{AxNube.configurada() ? 'Entrar a la nube del despacho' : 'Ver «Nube del despacho»'}</AxButton></div>;
  if (!AxNube.configurada()) {
    return <EqmAviso tono="warm" icono="reloj">Esta red no permite la conexión directa con la principal, y el despacho todavía no tiene nube. Tus cambios esperan aquí y viajan cuando se alcancen.{ir}</EqmAviso>;
  }
  if (n.conexion !== 'ok') return <EqmAviso tono="bad" icono="alert">Esta red no permite la conexión directa, y {nube.nombre} no responde en esta computadora. Tus cambios esperan aquí.</EqmAviso>;
  if (n.mPaso !== 'unida') return <EqmAviso tono="warm" icono="reloj">Esta red no permite la conexión directa con la principal. Para que tus cambios viajen por el {nube.nombre} del despacho, falta que esta computadora entre a la carpeta.{ir}</EqmAviso>;
  return (
    <EqmAviso tono="info" icono="info">
      Esta red no permite la conexión directa con la principal; sincronizando por {AxNube.nombreVia(true)}…
      {n.pendientes.length ? <div style={{ marginTop: 4 }}>1 archivo todavía no llega ({n.pendientes[0].nombre}); se reintenta solo. No se perdió.</div> : null}
    </EqmAviso>
  );
}

// Barra de arriba: avisos que no pueden esperar a que abras Ajustes
function NbAvisosBarra() {
  const n = useNube();
  const r = useResp();
  const eq = useEquipo();
  const { navigate } = useHashRoute();
  const nube = AxNube.info();
  const miembro = eq.ver === 'miembro';
  const avisos = [];
  if (nube && AxNube.configurada() && n.conexion !== 'ok') avisos.push({ k: 'nube', t: nube.nombre + ' no responde', tono: 'danger', to: '/ajustes/nube' });
  else if (nube && AxNube.configurada() && n.espacio !== 'normal' && (!miembro || n.qEspacio === 'B')) avisos.push({ k: 'espacio', t: (miembro ? 'Nube del despacho ' : nube.nombre + ' ') + (n.espacio === 'lleno' ? 'lleno' : 'casi lleno'), tono: n.espacio === 'lleno' ? 'danger' : 'warm', to: '/ajustes/nube' });
  if (!miembro && r.estado === 'encendido' && AxResp.fallo()) avisos.push({ k: 'respaldo', t: 'El respaldo de anoche falló', tono: 'danger', to: '/ajustes/respaldo' });
  if (!avisos.length) return null;
  return (
    <div style={{ display: 'flex', gap: 6 }}>
      {avisos.map((a) => (
        <button key={a.k} data-nb={'barra-' + a.k} onClick={() => navigate(a.to)} style={{
          appearance: 'none', cursor: 'default', display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 10px', borderRadius: 999,
          fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap',
          color: a.tono === 'danger' ? 'var(--ax-danger)' : 'var(--ax-warm)',
          background: a.tono === 'danger' ? 'var(--ax-danger-soft)' : 'var(--ax-warm-soft)',
          border: '1px solid ' + (a.tono === 'danger' ? 'var(--ax-danger)' : 'var(--ax-warm-line)'),
        }}>
          <AxIcon name="alert" size={12}/>{a.t}
        </button>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// PANEL «SIMULAR» · Nube del agente (#1432 #1439)
// ─────────────────────────────────────────────────────────────
function NbTwkBtn({ onClick, children, fuerte }) {
  return (
    <button type="button" onClick={onClick} style={{
      appearance: 'none', background: fuerte ? 'rgba(0,0,0,0.82)' : 'rgba(255,255,255,0.75)', color: fuerte ? '#fff' : '#29261b',
      border: '1px solid rgba(0,0,0,0.08)', borderRadius: 6, padding: '6px 8px', fontSize: 11, cursor: 'default', textAlign: 'left', lineHeight: 1.35,
    }}>{children}</button>
  );
}
const nbNotaTwk = { fontSize: 10.5, color: 'rgba(41,38,27,0.62)', lineHeight: 1.45 };

function NubeTweaks({ navigate }) {
  const n = useNube();
  const r = useResp();
  const eq = useEquipo();
  const { pushToast } = useAxStore();
  window.__nbToast = pushToast;
  const [copiado, setCopiado] = useState(false);
  const comoPrincipal = () => AxEquipo.setVer('principal');
  const comoMiembro = () => AxEquipo.setVer('miembro');
  const configurada = () => { if (!AxNube.configurada()) AxNube.configurarYa('gdrive'); };
  const limpio = () => { AxNube.limpiar(); AxResp.set({ carpetaExiste: true, comprobar: false }); };
  const sofiaUnida = () => { configurada(); AxNube.marcarCompartida('sofia'); AxNube.unirYa(); };

  const recorridos = [
    { t: '1 · Laura (principal) conecta su Google Drive, crea la carpeta y la comparte', f: () => { limpio(); comoPrincipal(); AxNube.reiniciarPrincipal(); navigate('/ajustes'); } , nota: 'Empieza en Ajustes › Equipo › «Sincronizar por tu nube».' },
    { t: '2 · Sofía (miembro, Windows) entra a la carpeta del despacho', f: () => { limpio(); configurada(); AxNube.marcarCompartida('sofia'); AxNube.reiniciarMiembro(); comoMiembro(); navigate('/equipo-sincronizacion'); }, nota: 'Empieza en Equipo › sincronización, como miembro.' },
    { t: '3 · Sofía elige una carpeta equivocada', f: () => { limpio(); configurada(); AxNube.marcarCompartida('sofia'); AxNube.set({ mPaso: 'carpeta', mCuenta: 'si', mAcceso: true, mRechazo: null }); comoMiembro(); navigate('/ajustes/nube'); } },
    { t: '4 · La red no deja la directa: Sofía sincroniza por la nube de Laura', f: () => { limpio(); sofiaUnida(); comoMiembro(); AxEquipo.setPrincipal('nube'); navigate('/equipo-sincronizacion'); }, nota: 'Mira también el indicador de la barra de arriba.' },
    { t: '5 · Un PDF todavía no llega (reintento, no perdido)', f: () => { limpio(); sofiaUnida(); comoMiembro(); AxEquipo.setPrincipal('nube'); AxNube.simularPendiente(); navigate('/ajustes/nube'); } },
    { t: '6 · El Google Drive de Laura está casi lleno', f: () => { limpio(); configurada(); comoPrincipal(); AxNube.set({ espacio: 'casi' }); navigate('/ajustes/nube'); } },
    { t: '7 · Google Drive se desconectó en la computadora de Laura', f: () => { limpio(); configurada(); comoPrincipal(); AxNube.set({ conexion: 'desconectada' }); navigate('/inicio'); }, nota: 'Aparece arriba en todas las pantallas; el aviso lleva a la nube.' },
    { t: '8 · Laura enciende el respaldo automático', f: () => { limpio(); comoPrincipal(); AxResp.apagar(); navigate('/cuenta'); }, nota: 'Empieza en Tu cuenta › «Dónde viven tus datos» › Configurar respaldo.' },
    { t: '9 · Pasó la noche: último respaldo y restaurar con el llavero', f: () => { limpio(); comoPrincipal(); AxResp.encendidoYa(); AxResp.set({ donde: 'esta' }); navigate('/ajustes/respaldo'); } },
    { t: '10 · El respaldo de anoche falló: la carpeta ya no existe', f: () => { limpio(); comoPrincipal(); AxResp.encendidoYa(); AxResp.set({ carpetaExiste: false }); AxResp.pasaNoche(); navigate('/inicio'); }, nota: 'El aviso sale arriba; Ajustes y Tu cuenta también lo dicen.' },
    { t: '11 · Restaurar en una computadora nueva (sin llavero)', f: () => { limpio(); comoPrincipal(); AxResp.encendidoYa(); AxResp.set({ donde: 'otra' }); navigate('/ajustes/respaldo?ir=restaurar'); } },
  ];

  const preguntas = [
    { texto: 'En un despacho que mezcla Mac y Windows, ¿Piixi deja elegir iCloud?', valor: n.qIcloud,
      a: 'Sí, con un aviso: «ahí iCloud se traba; te sugerimos Google Drive u OneDrive»', b: 'No: iCloud sale apagado si hay alguna computadora Windows en el equipo',
      elegir: (v) => AxNube.set({ qIcloud: v }), donde: 'Sincronizar por tu nube › paso 1',
      ir: () => { comoPrincipal(); AxNube.reiniciarPrincipal(); AxNube.set({ mixto: true, elegida: 'icloud' }); navigate('/ajustes/nube'); } },
    { texto: 'Cuando la directa no se puede, ¿qué dice el indicador de la barra de arriba del miembro?', valor: n.qNombre,
      a: '«Por Google Drive» (el nombre de la nube)', b: '«Por la nube del despacho»',
      elegir: (v) => AxNube.set({ qNombre: v }), donde: 'la barra de arriba, como miembro',
      ir: () => { sofiaUnida(); comoMiembro(); AxEquipo.setPrincipal('nube'); navigate('/inicio'); } },
    { texto: 'Cuando la nube del despacho está casi llena, ¿quién ve el aviso?', valor: n.qEspacio,
      a: 'Sólo la principal (es su nube y la que puede liberar espacio)', b: 'La principal y también los miembros, en la barra de arriba',
      elegir: (v) => AxNube.set({ qEspacio: v }), donde: 'la barra de arriba y Nube del despacho, como miembro',
      ir: () => { sofiaUnida(); AxNube.set({ espacio: 'casi' }); comoMiembro(); navigate('/ajustes/nube'); } },
    { texto: 'Al restaurar en ESTA misma computadora, ¿hay que escribir la contraseña del respaldo?', valor: r.qRestaurar,
      a: 'No: Piixi usa la del llavero; sólo confirmas con la contraseña de la computadora o Touch ID', b: 'Sí, siempre se escribe (aunque esté en el llavero)',
      elegir: (v) => AxResp.set({ qRestaurar: v }), donde: 'Respaldo de tu cartera › Restaurar',
      ir: () => { comoPrincipal(); if (AxResp.get().estado !== 'encendido') AxResp.encendidoYa(); AxResp.set({ donde: 'esta' }); navigate('/ajustes/respaldo?ir=restaurar'); } },
    { texto: 'Como la contraseña vive en el llavero, el agente puede olvidarla. ¿Piixi se la vuelve a pedir para comprobar que la recuerda?', valor: r.qRecordar,
      a: 'Sí: cada 30 días la pide una vez (y si la olvidó, la cambia ahí mismo)', b: 'No: sólo se escribe al encender el respaldo',
      elegir: (v) => AxResp.set({ qRecordar: v, comprobar: v === 'A' ? AxResp.get().comprobar : false }), donde: 'Respaldo de tu cartera, tras «pasan 30 días»',
      ir: () => { comoPrincipal(); if (AxResp.get().estado !== 'encendido') AxResp.encendidoYa(); AxResp.pasan30(); navigate('/ajustes/respaldo'); } },
  ];
  const resumen = 'Nube del agente (#1432 #1439):\n' + preguntas.map((q, i) => (i + 1) + '. ' + q.texto + '\n   → ' + q.valor + ': ' + (q.valor === 'B' ? q.b : q.a)).join('\n');
  const copiar = () => { if (window.pregCopiar) pregCopiar(resumen); setCopiado(true); setTimeout(() => setCopiado(false), 4000); };

  return (
    <>
      <TweakSection label="Nube del agente (#1432 #1439) · para validar"/>
      <div style={{ fontSize: 11, color: '#444', lineHeight: 1.45 }}>
        Dos pantallas nuevas: «Sincronizar por tu nube» (Ajustes › Equipo) y «Respaldo de tu cartera» (Ajustes, y desde Tu cuenta). Una nube por despacho, la de la principal; la contraseña del respaldo se guarda en el llavero. Elige una situación y navégala a clics.
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 6 }}>
        {recorridos.map((x, i) => (
          <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
            <NbTwkBtn onClick={x.f}>→ {x.t}</NbTwkBtn>
            {x.nota ? <div style={{ ...nbNotaTwk, paddingLeft: 8 }}>{x.nota}</div> : null}
          </div>
        ))}
      </div>

      <TweakSection label="Preguntas para ti · nube del agente"/>
      <div style={nbNotaTwk}>Cinco dudas abiertas. Toca A o B y la app cambia en ese momento; «Llévame ahí» te deja donde se siente la diferencia.</div>
      <div>
        {preguntas.map((q, i) => (
          <div key={i} data-nb-pregunta={i + 1} style={{ display: 'flex', flexDirection: 'column', gap: 5, padding: '9px 0', borderTop: '1px solid rgba(0,0,0,.08)' }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: '#111', lineHeight: 1.35 }}>{i + 1}. {q.texto}</div>
            <PregBoton letra="A" activo={q.valor === 'A'} onClick={() => q.elegir('A')}><i style={{ fontStyle: 'normal', fontWeight: 600, color: '#0a7a46' }}>Propuesta · </i>{q.a}</PregBoton>
            <PregBoton letra="B" activo={q.valor === 'B'} onClick={() => q.elegir('B')}>{q.b}</PregBoton>
            <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', fontSize: 10.5, color: '#555' }}>
              <span>Se ve en: {q.donde}</span>
              <button type="button" onClick={q.ir} style={{ flex: '0 0 auto', border: 0, background: 'transparent', color: '#0a7a46', fontWeight: 600, cursor: 'pointer', font: 'inherit', fontSize: 10.5, padding: 0 }}>Llévame ahí →</button>
            </div>
          </div>
        ))}
      </div>
      <button type="button" onClick={copiar} style={{ marginTop: 4, width: '100%', padding: '8px 10px', borderRadius: 7, border: '1px solid #0a7a46', background: copiado ? '#0a7a46' : 'rgba(255,255,255,.8)', color: copiado ? '#fff' : '#0a7a46', font: 'inherit', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
        {copiado ? 'Copiado: pégalo en la conversación' : 'Copiar mis respuestas'}
      </button>

      <TweakSection label="Nube del agente · controles finos"/>
      <TweakRadio label="Ver como" value={eq.ver} options={[{ value: 'principal', label: 'Principal' }, { value: 'miembro', label: 'Miembro' }]} onChange={(v) => AxEquipo.setVer(v)}/>
      <TweakRadio label="Nube del despacho" value={AxNube.configurada() ? n.nube : 'ninguna'}
        options={[{ value: 'ninguna', label: 'Sin configurar' }, { value: 'gdrive', label: 'Google Drive' }, { value: 'onedrive', label: 'OneDrive' }, { value: 'icloud', label: 'iCloud' }]}
        onChange={(v) => { if (v === 'ninguna') AxNube.reiniciarPrincipal(); else AxNube.configurarYa(v); }}/>
      <TweakRadio label="Computadora de la principal" value={n.soP} options={[{ value: 'mac', label: 'Mac' }, { value: 'win', label: 'Windows' }]} onChange={(v) => AxNube.set({ soP: v })}/>
      <TweakRadio label="Equipo" value={n.mixto ? 'mixto' : 'mac'} options={[{ value: 'mac', label: 'Sólo Mac' }, { value: 'mixto', label: 'Mac y Windows' }]} onChange={(v) => AxNube.set({ mixto: v === 'mixto' })}/>
      <TweakRadio label="Piixi encuentra la app" value={n.detecta ? 'si' : 'no'} options={[{ value: 'si', label: 'Sí' }, { value: 'no', label: 'No: diálogo' }]} onChange={(v) => AxNube.set({ detecta: v === 'si' })}/>
      <TweakRadio label="macOS pide permiso" value={n.permisoMac ? 'si' : 'no'} options={[{ value: 'si', label: 'Sí' }, { value: 'no', label: 'No' }]} onChange={(v) => AxNube.set({ permisoMac: v === 'si', permiso: null })}/>
      <TweakRadio label="Espacio de la nube" value={n.espacio} options={[{ value: 'normal', label: 'Normal' }, { value: 'casi', label: 'Casi lleno' }, { value: 'lleno', label: 'Lleno' }]} onChange={(v) => AxNube.set({ espacio: v })}/>
      <TweakRadio label="App de la nube" value={n.conexion} options={[{ value: 'ok', label: 'Conectada' }, { value: 'desconectada', label: 'No responde' }]} onChange={(v) => AxNube.set({ conexion: v })}/>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        <NbTwkBtn onClick={() => AxNube.simularPendiente()}>→ Un archivo todavía no llega</NbTwkBtn>
        <NbTwkBtn onClick={() => AxNube.llego()}>→ Tu nube ya lo bajó (llega y se aplica)</NbTwkBtn>
        <NbTwkBtn onClick={() => { AxNube.marcarCompartida('sofia'); AxNube.unirYa(); }}>→ Sofía ya entró a la carpeta</NbTwkBtn>
        <NbTwkBtn onClick={() => AxNube.reiniciarMiembro()}>→ Sofía vuelve a empezar</NbTwkBtn>
      </div>
      <TweakRadio label="Respaldo automático" value={r.estado === 'encendido' ? 'on' : 'off'} options={[{ value: 'off', label: 'Apagado' }, { value: 'on', label: 'Encendido' }]}
        onChange={(v) => { if (v === 'on') AxResp.encendidoYa(); else AxResp.apagar(); }}/>
      <TweakRadio label="Carpeta del respaldo" value={r.carpetaExiste ? 'existe' : 'no'} options={[{ value: 'existe', label: 'Existe' }, { value: 'no', label: 'Ya no existe' }]} onChange={(v) => AxResp.set({ carpetaExiste: v === 'existe' })}/>
      <TweakRadio label="Nube que Piixi encuentra (respaldo)" value={r.detectada}
        options={[{ value: 'gdrive', label: 'Google Drive' }, { value: 'onedrive', label: 'OneDrive' }, { value: 'icloud', label: 'iCloud' }, { value: 'ninguna', label: 'Ninguna' }]}
        onChange={(v) => AxResp.set({ detectada: v })}/>
      <TweakRadio label="Restaurar en" value={r.donde} options={[{ value: 'esta', label: 'Esta compu' }, { value: 'otra', label: 'Otra compu' }]} onChange={(v) => AxResp.set({ donde: v })}/>
      <TweakRadio label="La contraseña tecleada" value={r.abre ? 'abre' : 'no'} options={[{ value: 'abre', label: 'Abre' }, { value: 'no', label: 'No abre' }]} onChange={(v) => AxResp.set({ abre: v === 'abre' })}/>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        <NbTwkBtn fuerte onClick={() => AxResp.pasaNoche()}>→ Pasa la noche (corre el respaldo)</NbTwkBtn>
        <NbTwkBtn onClick={() => { AxResp.pasan30(); if (AxResp.get().qRecordar === 'B') pushToast({ kind: 'info', message: 'Con la respuesta B no pasa nada: Piixi no vuelve a pedir la contraseña.' }); }}>→ Pasan 30 días</NbTwkBtn>
      </div>
    </>
  );
}

Object.assign(window, {
  AxNube, useNube, AxResp, useResp, NB_NUBES,
  NubeScreen, RespaldoScreen, NubeFilaAjustes, RespaldoTarjeta, RespaldoEnCuenta,
  NbTarjetaEquipo, NbAvisoMiembro, NbAvisosBarra, NubeTweaks,
});
