/* Mi.Asistente — Mini-app de Telegram · prototipo Pieza 3a · 04-sep-2026
   Valores de Back End/public/css/tokens.css. Nada inventado.                */
const T = {
  g950:'#141210', g900:'#1C1917', g800:'#292522', g700:'#3E3A37',
  v600:'#6C63FF', v700:'#574BFF', v800:'#4A43CC',
  green:'#1DB954', amber:'#E8B84C', coral:'#E86A5F',
  c100:'#F5F4F2', c300:'#D8D5D1', c500:'#A09E9C',
  vSoft:'rgba(108,99,255,.12)', vBorder:'rgba(108,99,255,.30)',
  gSoft:'rgba(29,185,84,.12)', gBorder:'rgba(29,185,84,.30)',
  aSoft:'rgba(232,184,76,.12)', aBorder:'rgba(232,184,76,.30)',
  coSoft:'rgba(232,106,95,.12)', coBorder:'rgba(232,106,95,.30)',
  display:"'Bebas Neue',Impact,sans-serif",
  sans:"'DM Sans','Helvetica Neue',Arial,sans-serif",
  mono:"'JetBrains Mono','Courier New',monospace",
};
const money = n => '$\u2009' + n.toLocaleString('es-AR');

const BIT0 = {
  '1042':[{ h:'08:14', t:'Paula preguntó por WhatsApp', d:'“¿Pintás living y comedor?”', por:'cli', tono:'alta' },
          { h:'08:19', t:'Mia cotizó y agendó para hoy 14:30', por:'mia', tono:'mia' },
          { h:'08:41', t:'Paula confirmó y pagó la seña', d:'50% · $24.250', por:'cli', tono:'ok' }],
  '1041':[{ h:'28 AGO', t:'Carlos pidió presupuesto', por:'cli', tono:'alta' },
          { h:'28 AGO', t:'Presupuesto elaborado', d:'6 ítems · $312.000', por:'vos', tono:'vos' },
          { h:'28 AGO', t:'Mia se lo mandó por WhatsApp', por:'mia', tono:'mia' },
          { h:'31 AGO', t:'Mia le insistió una vez', d:'Sin respuesta', por:'mia', tono:'cambio' }],
  '1040':[{ h:'07:52', t:'Marina escribió por primera vez', por:'cli', tono:'alta' },
          { h:'07:58', t:'Mia agendó para hoy 16:00', por:'mia', tono:'mia' },
          { h:'08:03', t:'Marina confirmó', por:'cli', tono:'ok' }],
};
const JOBS = [
  { id:'1042', cli:'Paula Giménez', svc:'Living y comedor · 2 manos', hora:'14:30', dia:'HOY', monto:48500, tsm:'CONFIRMADO', paso:1, hist:['mia','mia'], extra:'Seña 50% cobrada', tel:'+54 9 341 402 118', donde:'Corrientes 1450, 3° A' },
  { id:'1041', cli:'Carlos Ferreyra', svc:'Casa completa · interior', hora:'—', dia:'PRESUPUESTO', monto:312000, tsm:'CREADO', paso:0, hist:['mia'], enviado:true, emitido:'28 AGO', extra:'Enviado hace 6 días', tel:'+54 9 341 233 460', donde:'Mendoza 2340, 4° B',
    items:[
      { t:'mat',  d:'Látex interior 20 L',           c:4, p:42000 },
      { t:'mat',  d:'Fijador al agua 10 L',          c:2, p:21500 },
      { t:'mat',  d:'Enduido plástico 25 kg',        c:2, p:12900 },
      { t:'mat',  d:'Rodillos, pinceles y cinta',    c:1, p:19200 },
      { t:'obra', d:'Preparación · lijado y enduido', c:1, p:38000 },
      { t:'obra', d:'Aplicación · 2 manos',          c:1, p:18000 },
    ] },
  { id:'1040', cli:'Marina Ruiz', svc:'Frente de placard', hora:'16:00', dia:'HOY', monto:22000, tsm:'CONFIRMADO', paso:1, hist:['mia','mia'], extra:'Primera vez', tel:'+54 9 341 588 907', donde:'Ov. Lagos 2233' },
  { id:'1039', cli:'Diego Salas', svc:'Frente exterior · revoque y pintura', hora:'09:00', dia:'MAÑANA', monto:38000, tsm:'CREADO', paso:0, hist:['mia'], extra:'Esperando respuesta', tel:'+54 9 341 671 025', donde:'San Luis 871, PB' },
  { id:'1038', cli:'Paula Giménez', svc:'Mano extra en el pasillo', hora:'11:00', dia:'SAB', monto:18000, tsm:'CONFIRMADO', paso:1, hist:['mia','mia'], extra:'Reservó el sábado', tel:'+54 9 341 402 118', donde:'Corrientes 1450, 3° A' },
];
/* Quién mueve cada estado. El esquema real ya lo guarda: tickets.historial_estados
   es [{estado, timestamp, por}] — y la lista TSM incluye CONFIRMADO_IMPLICITO,
   COMPLETADO_PRESUMIDO y REQUIERE_VALIDACION, o sea que la distinción "Mia lo dio
   por hecho / falta que el dueño valide" YA existe en la base. Lo que faltaba era
   que la interfaz la mostrara, para que el tipo sepa qué toca y qué no.       */
/* Los pasos dependen del FLUJO, y "Presupuestado" no era el primero: el
   trabajo arranca cuando se agenda el turno. El presupuesto se elabora
   después —en la primera visita, o durante ella— y sólo en los rubros que
   presupuestan. Antes eran cuatro pasos fijos que le mentían a la mitad de
   los rubros y ponían el presupuesto antes de conocer el trabajo.        */
/* Cada paso declara TRES cosas: quién lo marca, qué texto lleva y —si es de
   los que hacés— a dónde te manda. Un paso que dice "lo armás vos" y no te
   deja armarlo es un cartel, no un control.
   Los rótulos salen del patrón: en un taller no empieza un trabajo, entra un
   artículo, y no se realiza, se entrega.                                  */
const PASO_DEF = {
  AGENDADO:     { t:'Turno agendado',         por:'mia', x:'Mia lo agenda cuando el cliente pide fecha' },
  PRESUPUESTADO:{ t:'Elaborar el presupuesto', th:'Presupuesto elaborado', por:'vos', x:'Cargá los ítems y los materiales', ir:'items' },
  ENVIADO:      { t:'Presupuesto enviado',    por:'mia', x:'Se tilda cuando Mia se lo manda', auto:true },
  APROBADO:     { t:'Presupuesto confirmado', por:'mia', x:'Se tilda cuando el cliente lo acepta', auto:true },
  COBRADO:      { t:'Trabajo cobrado',        por:'vos', x:'Lo marcás vos: es plata, no se presume' },
};
/* Los patrones que no son "ir y hacer" le dicen distinto a lo mismo. */
/* El medio de la escalera sale de la CADENA del TSM del patrón: se camina
   desde CREADO tomando la transición que avanza. Antes había un único slot
   EN_CURSO para todos, así que un taller no tenía "En reparación" —los días
   que el artículo está en el banco— y un pedido no tenía "En preparación".
   Los rótulos son los del propio TSM_ACCIONES.                          */
function cadenaTSM(patron) {
  const tabla = TSM_ACCIONES[patron] || TSM_ACCIONES.C;
  const salto = ['CANCELADO','AUSENTE','DEMORADO'];
  const out = []; let est = 'CREADO', guarda = 0;
  while (tabla[est] && guarda++ < 8) {
    const sig = (tabla[est] || []).find(([e]) => !salto.includes(e));
    if (!sig) break;
    out.push({ k:sig[0], t:sig[1] });
    est = sig[0];
  }
  return out;
}
/* Quién marca cada estado del TSM: Mia sabe lo que pasa por el chat. */
const POR_TSM = { CONFIRMADO:'mia', RECIBIDO:'vos', EN_REPARACION:'vos', LISTO_PARA_RETIRAR:'vos',
  EN_CAMINO:'vos', EN_PREPARACION:'vos', EN_CURSO:'vos', FINALIZADO:'vos', ENTREGADO:'vos', RETIRADO:'vos' };
const AYUDA_TSM = { CONFIRMADO:'Mia lo marca cuando el cliente dice que sí',
  FINALIZADO:'Lo marcás vos al terminar', ENTREGADO:'Lo marcás vos al entregarlo', RETIRADO:'Lo marcás vos cuando lo pasa a buscar',
  RECIBIDO:'Lo marcás vos cuando el cliente te lo deja',
  EN_REPARACION:'Lo marcás vos cuando lo ponés en el banco',
  LISTO_PARA_RETIRAR:'Lo marcás vos cuando terminaste',
  EN_CAMINO:'Lo marcás vos al salir — Mia le avisa que vas',
  EN_PREPARACION:'Lo marcás vos cuando arrancás a prepararlo',
  EN_CURSO:'Lo marcás vos al arrancar' };
/* El rótulo es el NOMBRE DEL ESTADO (EST_LBL), no el texto del botón que lleva
   a él: TSM_ACCIONES dice "Confirmar" porque es una acción, y la escalera
   tiene que decir "Confirmado" porque es un hito. */
const NOM_TSM = { CONFIRMADO:'Turno confirmado' };
/* Dos confirmaciones distintas caían en el mismo peldaño: la del EMPRENDEDOR
   —acepta el turno y Mia le avisa al cliente— y la del CLIENTE, que acepta un
   horario que le propusieron. Normalmente confirma el emprendedor; después de
   una reagenda la pelota es del cliente. El peldaño es el mismo, pero dice de
   quién es el próximo movimiento. */
const confirmDe = j => j && j.esperaCli
  ? { por:'mia', auto:true, x:'Le propusimos el horario nuevo · esperando que responda' }
  : { por:'vos', x:'Lo confirmás vos y Mia le avisa al cliente' };
/* Vocabulario por rubro para TODA la escalera, no sólo el estado final: un
   taller no agenda un turno —el cliente deja un artículo— y una pastelera no
   confirma un turno, confirma un pedido. Se perdió al reescribir pasoDe sobre
   cadenaTSM y volvió a decir "Turno" en los 7 patrones. */
const NOM_FIN = {
  A:{ FINALIZADO:'Trabajo realizado' },
  C:{ FINALIZADO:'Trabajo realizado' },
  B:{ FINALIZADO:'Turno terminado' },
  F:{ AGENDADO:'Consulta agendada', CONFIRMADO:'Consulta confirmada', FINALIZADO:'Consulta terminada' },
  /* El taller no tiene peldaño de confirmación: la cadena va CREADO → RECIBIDO,
     el cliente confirma dejando el artículo. */
  G:{ AGENDADO:'Entrada agendada', RECIBIDO:'Artículo recibido',
      EN_REPARACION:'En reparación', LISTO_PARA_RETIRAR:'Artículo listo para retirar',
      RETIRADO:'Retirado por el cliente', COBRADO:'Reparación cobrada' },
  D:{ AGENDADO:'Pedido agendado', CONFIRMADO:'Pedido confirmado', EN_PREPARACION:'Pedido en preparación',
      ENTREGADO:'Pedido entregado', COBRADO:'Pedido cobrado' },
  E:{ AGENDADO:'Evento agendado', CONFIRMADO:'Evento confirmado', FINALIZADO:'Evento finalizado', COBRADO:'Evento cobrado' },
};
/* Cómo se llama arrancar depende de si vas vos o viene el cliente. */
const NOM_COMENZADO = {
  A:['Llegué y arranqué','Lo marcás vos al llegar — Mia no está ahí'],
  C:['Llegué y arranqué','Lo marcás vos al llegar — Mia no está ahí'],
  E:['Arrancó el evento','Lo marcás vos al empezar'],
  B:['Turno comenzado','Lo marcás vos cuando lo hacés pasar'],
  F:['Consulta comenzada','Lo marcás vos cuando lo hacés pasar'],
};
const PASO_LLEGUE = { t:'Comenzado', por:'vos', x:'Lo marcás vos al arrancar' };
const pasoDe = (k, patron, job) => {
  const voz = (NOM_FIN[patron] || {})[k];
  /* El rubro manda sobre el rótulo; confirmDe sólo decide de quién es el turno. */
  if (k === 'CONFIRMADO') return { t:voz || 'Turno confirmado', ...confirmDe(job) };
  if (voz && PASO_DEF[k]) return { ...PASO_DEF[k], t:voz };
  if (k === LLEGUE) { const n = NOM_COMENZADO[patron];
    return n ? { t:n[0], por:'vos', x:n[1] } : PASO_LLEGUE; }
  if (PASO_DEF[k]) return PASO_DEF[k];
  const n = (NOM_FIN[patron] || {})[k] || NOM_TSM[k] || EST_LBL[k] || k;
  return { t:n, por:POR_TSM[k] || 'vos', x:AYUDA_TSM[k] || 'Lo marcás vos' };
};
/* Los tres atajos —arrancar, terminar, cobrar— se resuelven por CANDIDATOS,
   no por una clave fija: la escalera cambia de claves según el patrón y cada
   vez que eso pasó, los botones desaparecieron en silencio. Una sola función
   para las dos superficies. El rótulo sale de pasoDe, así un taller lee
   "Artículo recibido" y no vocabulario de pintor. */
const CAND = { arranca:['COMENZADO','EN_CURSO','EN_REPARACION','EN_PREPARACION'],
  termina:['FINALIZADO','ENTREGADO','RETIRADO'], cobra:['COBRADO'] };
function atajos(cfg) {
  const ps = pasosDe(cfg), p = (cfg && cfg.patron) || 'C';
  return ['arranca','termina','cobra'].map(g => {
    const k = CAND[g].find(x => ps.indexOf(x) >= 0);
    return k ? { k, n:ps.indexOf(k), t:pasoDe(k, p).t } : null;
  }).filter(Boolean);
}

/* El TSM no tiene el momento en que el trabajo ARRANCA: en domicilio salta de
   "en camino" a "finalizado", y en el local de "confirmado" a "terminado". En
   un trabajo de dos horas eso son dos horas sin estado, justo cuando el
   cliente pregunta "¿ya está?". COMENZADO es un paso interno —no le manda
   nada a nadie— que llena ese hueco de los dos lados. Los patrones que ya
   tienen un estado de curso propio (taller, pedidos) no lo necesitan.    */
const LLEGUE = 'COMENZADO';
const EN_CURSO_TSM = ['EN_CURSO','EN_REPARACION','EN_PREPARACION'];
function pasosDe(cfg) {
  const p = (cfg && cfg.patron) || 'C';
  const cad = cadenaTSM(p);
  const medio = cad.slice(0, -1).map(x => x.k);
  const fin = cad.length ? cad[cad.length - 1].k : 'FINALIZADO';
  if (!medio.some(k => EN_CURSO_TSM.includes(k))) {
    const iCam = medio.indexOf('EN_CAMINO');
    medio.splice(iCam >= 0 ? iCam + 1 : medio.length, 0, LLEGUE);
  }
  const pres = cfg && cfg.presupuesto && cfg.presupuesto.on ? ['PRESUPUESTADO','ENVIADO','APROBADO'] : [];
  /* Dónde entra el presupuesto depende del oficio, no es siempre al final.
     Un taller diagnostica al recibir, cotiza y RECIÉN AHÍ repara; una
     pastelera cotiza antes de ponerse a preparar. Clavarlo antes del estado
     final hacía que el artículo estuviera listo para retirar sin haberse
     cotizado, y que el reparto saliera sin aprobación. */
  const ANCLA = { G:'RECIBIDO', D:'CONFIRMADO', E:'CONFIRMADO', B:'CONFIRMADO', F:'CONFIRMADO' };
  const todos = ['AGENDADO', ...medio];
  const i = pres.length ? todos.indexOf(ANCLA[p]) : -1;
  if (i >= 0) todos.splice(i + 1, 0, ...pres);
  return [...todos, ...(i >= 0 ? [] : pres), fin, 'COBRADO'];
}/* Configuración = las 6 cards del onboarding real de panel.html, en el mismo
   orden que el tour. Los valores se DERIVAN del estado (ver pieza3-docs.jsx),
   así que guardar en una pantalla cambia lo que dice esta lista.            */
/* Las 7 cards del onboarding, que ahora se hace COMPLETO acá — cero pasos por
   conversación de WhatsApp (decisión 04-sep). El rubro es la primera porque de
   ahí sale el preset de todas las demás.                                     */
const CFG = [
  { k:'Tu rubro' }, { k:'Servicios y precios' }, { k:'Horarios y días' },
  { k:'Feriados y vacaciones' }, { k:'Capacidad y metas' }, { k:'Sobreturnos' },
  { k:'Visitas a domicilio' }, { k:'Cobros y señas' }, { k:'Avisos al cliente' },
  { k:'Recibos y presupuestos' },
];
/* Historial: los trabajos viejos que ya no están en la operación del día pero
   siguen siendo del cliente. Sin esto la ficha miente por omisión — dice
   "14 visitas" y muestra una. */
const HIST = {
  'Paula Giménez':[
    { id:'0994', svc:'Cocina y lavadero', cuando:'12 AGO', monto:64000, tsm:'FINALIZADO', pres:true },
    { id:'0961', svc:'Dormitorio principal', cuando:'24 JUL', monto:52000, tsm:'FINALIZADO', pres:true },
    { id:'0930', svc:'Retoque de humedad', cuando:'01 JUL', monto:18000, tsm:'FINALIZADO', pres:false },
    { id:'0902', svc:'Frente exterior', cuando:'14 JUN', monto:96000, tsm:'CANCELADO', pres:true },
  ],
  'Carlos Ferreyra':[
    { id:'0988', svc:'Baño y pasillo', cuando:'29 AGO', monto:74000, tsm:'FINALIZADO', pres:true },
    { id:'0954', svc:'Presupuesto galpón', cuando:'18 JUL', monto:410000, tsm:'CANCELADO', pres:true },
  ],
  'Diego Salas':[
    { id:'0975', svc:'Medianera', cuando:'08 AGO', monto:88000, tsm:'FINALIZADO', pres:true },
    { id:'0940', svc:'Portón y rejas', cuando:'02 JUL', monto:47000, tsm:'FINALIZADO', pres:false },
  ],
};
const histDe = n => HIST[n] || [];

const CLIENTES_INIT = [
  { n:'Paula Giménez',  tel:'+54 9 341 402 118', dir:'Corrientes 1450, 3° A · Centro',  notas:'Prefiere turnos de tarde. Hay que cubrir el piso de madera.', v:14, ult:'HOY 14:30',   total:612000, tag:'Frecuente' },
  { n:'Marina Ruiz',    tel:'+54 9 341 588 907', dir:'Ov. Lagos 2233 · Abasto',          notas:'Llegó por recomendación de Paula. Departamento sin ascensor.', v:0,  ult:'HOY 16:00',   total:0,      tag:'Nuevo' },
  { n:'Carlos Ferreyra',tel:'+54 9 341 233 460', dir:'Mendoza 2340, 4° B · Centro',      notas:'Trabaja hasta las 18. Portero eléctrico 4B.', v:3,  ult:'29 AGO',      total:186000, tag:null },
  { n:'Diego Salas',    tel:'+54 9 341 671 025', dir:'San Luis 871, PB · Norte',         notas:'Casa con frente a la calle. Hay que andamio.', v:7,  ult:'MAÑANA 9:00', total:274000, tag:'Frecuente' },
];
/* Historial + lo que está vivo en `jobs`. Nada de contadores escritos a mano. */
const clienteResumen = (cli, jobs) => {
  const suyos = jobs.filter(j => j.cli === cli.n);
  const cobrados = suyos.filter(j => esCobrado(j.tsm));
  const hist = histDe(cli.n), histOk = hist.filter(h => esCobrado(h.tsm));
  const prox = suyos.find(j => !esCierre(j.tsm) && (j.dia === 'HOY' || j.dia === 'MAÑANA'));
  return {
    suyos, hist,
    visitas: histOk.length + cobrados.length,
    facturado: histOk.reduce((s,h) => s + h.monto, 0) + cobrados.reduce((s,j) => s + j.monto, 0),
    proxima: prox ? `${prox.dia} · ${prox.hora}` : '—',
  };
};

/* ── átomos ───────────────────────────────────────────────── */
const Lbl = ({ children, c = T.c500, mb = 0 }) => (
  <div style={{ fontFamily:T.mono, fontSize:9.5, letterSpacing:'.13em', textTransform:'uppercase', color:c, marginBottom:mb }}>{children}</div>
);
/* Un solo estado: tsm. El campo `est` era un segundo estado en paralelo y
   cualquier estado nuevo se colapsaba sobre uno viejo al derivarlo — así
   RELEVADO se veía como "A confirmar". El badge lee el TSM y punto. */
const Badge = ({ tsm, dot }) => { const t = EST_TONO[tsm];
  const e = { c:t==='coral'?T.coral:t==='amber'?T.amber:t==='green'?T.green:T.c500,
              bg:t==='coral'?T.coSoft:t==='amber'?T.aSoft:t==='green'?T.gSoft:T.g800,
              bd:t==='coral'?T.coBorder:t==='amber'?T.aBorder:t==='green'?T.gBorder:T.g700,
              t:EST_LBL[tsm] || tsm };
  return <span style={{ display:'inline-flex', alignItems:'center', gap:5, fontFamily:T.mono, fontSize:9, letterSpacing:'.09em', textTransform:'uppercase', color:e.c, background:e.bg, border:`1px solid ${e.bd}`, borderRadius:4, padding:'4px 7px', whiteSpace:'nowrap' }}>
    {dot && <i style={{ width:5, height:5, borderRadius:9, background:e.c, display:'block' }} />}{e.t}</span>;
};
const Chip = ({ children, on, onClick }) => (
  <button onClick={onClick} style={{ minWidth:0, minHeight:44, display:'flex', alignItems:'center', justifyContent:'center', gap:5, fontFamily:T.sans, fontSize:12.5, fontWeight:600, color:on ? T.c100:T.c500, background:on?T.vSoft:'transparent', border:`1px solid ${on?T.vBorder:T.g700}`, borderRadius:999, padding:'0 10px', cursor:'pointer', whiteSpace:'nowrap' }}>{children}</button>
);
const Card = ({ children, pad = 16, gap = 12, onClick, style, ...r }) => (
  <div onClick={onClick} {...r} style={{ background:T.g900, border:`1px solid ${T.g800}`, borderRadius:18, padding:pad, display:'flex', flexDirection:'column', gap, ...style }}>{children}</div>
);
const Row = ({ children, onClick }) => (
  <div onClick={onClick} role={onClick?'button':undefined} style={{ minHeight:64, display:'flex', alignItems:'center', gap:12, padding:'12px 14px', background:T.g900, border:`1px solid ${T.g800}`, borderRadius:14, cursor:onClick?'pointer':'default' }}>{children}</div>
);

/* ── chrome de Telegram ───────────────────────────────────── */
/* El chrome de Telegram NO es el mismo en las dos plataformas. iOS: botón
   "Close" azul a la izquierda, título centrado y un solo ⋯ a la derecha.
   Android: ícono a la izquierda, título alineado, ⋮ y ✕. Verificado contra
   una captura real de iPhone (03-sep-2026).                                */
function TgChrome({ back, title, sub, nota, ios }) {
  if (ios) return (
    <div style={{ flex:'none', background:T.g950, borderBottom:`1px solid ${T.g800}`, height:52, display:'flex', alignItems:'center', gap:10, padding:'0 12px' }}>
      {back
        ? <button onClick={back} aria-label="Atrás" style={{ flex:'none', display:'flex', alignItems:'center', gap:2, background:'transparent', border:'none', cursor:'pointer', padding:'6px 4px', color:'#3E8BFF', fontFamily:T.sans, fontSize:15.5 }}>
            <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#3E8BFF" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
          </button>
        : <button onClick={nota} style={{ flex:'none', background:'transparent', border:'none', cursor:'pointer', padding:'6px 4px', color:'#3E8BFF', fontFamily:T.sans, fontSize:15.5, fontWeight:400 }}>Close</button>}
      <div style={{ flex:1, minWidth:0, textAlign:'center' }}>
        <div style={{ fontFamily:T.sans, fontSize:15.5, fontWeight:600, color:T.c100, letterSpacing:'-.2px', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{title}</div>
        <div style={{ fontFamily:T.sans, fontSize:11.5, color:T.c500, marginTop:0 }}>{sub}</div>
      </div>
      <button onClick={nota} aria-label="Más" style={{ width:29, height:29, flex:'none', borderRadius:99, display:'grid', placeItems:'center', background:'transparent', border:'1.5px solid #3E8BFF', color:'#3E8BFF', fontSize:13, lineHeight:1, cursor:'pointer', padding:0 }}>⋯</button>
    </div>
  );
  return (
    <div style={{ flex:'none', background:T.g950, borderBottom:`1px solid ${T.g800}`, height:52, display:'flex', alignItems:'center', gap:12, padding:'0 14px' }}>
      {back
        ? <button onClick={back} aria-label="Atrás" style={{ width:32, height:32, flex:'none', display:'grid', placeItems:'center', background:'transparent', border:'none', cursor:'pointer', padding:0 }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={T.c100} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
          </button>
        : <svg width="26" height="26" viewBox="0 0 400 400" style={{ flex:'none' }}><circle cx="200" cy="200" r="200" fill={T.v600}/><circle cx="200" cy="200" r="179" fill="none" stroke="#fff" strokeOpacity=".10" strokeWidth="42"/><rect x="118" y="138" width="164" height="100" rx="26" fill={T.c100}/><path d="M152 238 L122 284 L200 238 Z" fill={T.c100}/><circle cx="160" cy="188" r="11" fill={T.v600}/><circle cx="200" cy="188" r="11" fill={T.v600}/><circle cx="240" cy="188" r="11" fill={T.v600}/><circle cx="306" cy="110" r="40" fill={T.c100}/><circle cx="306" cy="110" r="29" fill={T.green}/><circle cx="306" cy="110" r="10.5" fill={T.c100}/></svg>}
      <div style={{ minWidth:0, flex:1 }}>
        <div style={{ fontFamily:T.sans, fontSize:15, fontWeight:600, color:T.c100, letterSpacing:'-.2px', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{title}</div>
        <div style={{ fontFamily:T.mono, fontSize:9.5, color:T.c500, letterSpacing:'.06em', marginTop:1 }}>{sub}</div>
      </div>
      <div style={{ display:'flex', gap:4, flex:'none', color:T.c500 }}>
        <button onClick={nota} aria-label="Menú de Telegram" style={{ width:30, height:30, display:'grid', placeItems:'center', fontSize:17, lineHeight:1, background:'transparent', border:'none', color:'inherit', cursor:'pointer', padding:0 }}>⋮</button>
        <button onClick={nota} aria-label="Cerrar" style={{ width:30, height:30, display:'grid', placeItems:'center', fontSize:15, lineHeight:1, background:'transparent', border:'none', color:'inherit', cursor:'pointer', padding:0 }}>✕</button>
      </div>
    </div>
  );
}

/* MainButton nativo: 50 px, flush abajo, sobre el teclado, uno por pantalla */
/* MainButton nativo: 50 px, flush abajo, sobre el teclado, uno por pantalla.
   En iOS suma el safe-area del home indicator — sin eso el botón cae abajo de
   la barrita y se toca mal. Es el hallazgo 2 de la sección iOS.            */
/* Entra deslizándose desde el borde derecho y se va sola. Ancho casi completo
   y anclada abajo, arriba del MainButton: en la mano, el pulgar tapa el centro
   de la pantalla y una pastilla chica arriba no se ve.                    */
function Toast({ k, msg, tono, ios }) {
  /* Fondo sólido del color de lo que pasó: verde confirma y es el que más
     aparece, coral avisa, violeta informa. Sin ícono — la frase alcanza. */
  const bg = tono === 'ok' ? T.green : tono === 'mal' ? T.coral : T.v600;
  const fg = tono === 'ok' ? '#08240F' : tono === 'mal' ? '#3B120D' : '#FFFFFF';
  return (
    <div style={{ position:'absolute', left:12, right:12, bottom:ios ? 94 : 82, zIndex:29, pointerEvents:'none',
      display:'flex', flexDirection:'column', gap:3, minHeight:54, padding:'13px 16px',
      background:bg, borderRadius:12, boxShadow:'0 12px 36px rgba(0,0,0,.5)',
      animation:'tgslide 2.6s cubic-bezier(.2,.9,.3,1) forwards' }}>
      <span style={{ fontFamily:T.sans, fontSize:14.5, fontWeight:700, color:fg, lineHeight:1.35, letterSpacing:'-.1px' }}>{msg}</span>
      <span style={{ fontFamily:T.mono, fontSize:8.5, letterSpacing:'.11em', textTransform:'uppercase', color:fg, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{k}</span>
    </div>
  );
}

function MainButton({ label, onClick, tone = 'primary', busy, ios, lift, cerrar }) {
  const bg = tone === 'danger' ? T.coral : tone === 'off' ? T.g800 : T.v600;
  const fg = tone === 'off' ? T.c500 : '#fff';
  return (
    <div style={{ flex:'none', position:lift?'relative':'static', zIndex:lift?25:'auto', padding: ios ? '8px 8px 22px' : '8px 8px 10px', background:T.g950, borderTop:`1px solid ${T.g800}`, display:'flex', flexDirection:'column', gap:7 }}>
      {/* Salir de una pantalla no puede depender de una flecha de 20 px arriba
          a la izquierda: con el teléfono en una mano no se llega. El cierre va
          al alcance del pulgar, arriba de la acción principal, en TODAS. */}
      {cerrar && <button onClick={cerrar} style={{ width:'100%', minHeight:42, border:`1px solid ${T.g700}`, borderRadius:10, background:'transparent', color:T.c300, fontFamily:T.sans, fontSize:14, fontWeight:600, cursor:'pointer' }}>Cerrar ventana</button>}
      <button onClick={onClick} disabled={busy} style={{ width:'100%', height:50, border:'none', borderRadius:10, background:busy?T.v800:bg, color:fg, fontFamily:T.sans, fontSize:16, fontWeight:600, cursor:busy?'default':'pointer', display:'flex', alignItems:'center', justifyContent:'center', gap:9, boxShadow:tone==='off'?'none':'0 2px 10px rgba(0,0,0,.35)', transition:'background 200ms ease' }}>
        {busy && <i style={{ width:15, height:15, border:'2px solid rgba(255,255,255,.35)', borderTopColor:'#fff', borderRadius:9, display:'block', animation:'tgspin .7s linear infinite' }} />}
        {label}
      </button>
    </div>
  );
}

/* Tab bar · 4 destinos, 56 px de alto */
/* Iconos: los mismos de panel.html (.nav-item svg, líneas 1434-1458).
   No se dibuja un segundo set — la mini-app habla el mismo idioma que el panel. */
/* Mismos iconos que public/panel.html (nav, líneas 1433-1462) y que el panel
   web de esta pieza. Guardan ELEMENTOS, no strings `d`: el calendario de
   Agenda necesita un <rect>, y redibujarlo como path daba una caja 22% más
   angosta que la del panel — dos glifos distintos para el mismo destino. */
const ICONS = {
  hoy:      [['path',{ d:'M3 11l9-8 9 8v9a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z' }]],
  trabajos: [['rect',{ x:3, y:5, width:18, height:16, rx:2 }], ['path',{ d:'M3 10h18M8 3v4M16 3v4' }]],
  clientes: [['circle',{ cx:9, cy:8, r:3.5 }], ['path',{ d:'M3 21c0-3 3-5 6-5s6 2 6 5' }], ['circle',{ cx:17, cy:9, r:2.5 }], ['path',{ d:'M14 17c1-2 3-3 5-3' }]],
  negocio:  [['path',{ d:'M4 20V10M10 20V4M16 20v-7M22 20H2' }]],
};
const TabIcon = ({ id, on }) => (
  <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke={on?T.v600:T.c500} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
    {(ICONS[id] || []).map(([tag, at], k) => React.createElement(tag, { key:k, ...at }))}
  </svg>
);
const TABS = [
  { id:'hoy', t:'Hoy' }, { id:'trabajos', t:'Agenda' },
  { id:'clientes', t:'Clientes' }, { id:'negocio', t:'Negocio' },
];
function TabBar({ tab, go }) {
  return (
    <div style={{ flex:'none', display:'grid', gridTemplateColumns:'repeat(4,1fr)', background:T.g950, borderTop:`1px solid ${T.g800}` }}>
      {TABS.map(x => { const on = x.id === tab;
        return <button key={x.id} onClick={() => go(x.id)} style={{ height:56, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:4, background:'transparent', border:'none', borderTop:`2px solid ${on?T.v600:'transparent'}`, cursor:'pointer', padding:0 }}>
          <TabIcon id={x.id} on={on} />
          <span style={{ fontFamily:T.sans, fontSize:10.5, fontWeight:on?700:500, color:on?T.c100:T.c500 }}>{x.t}</span>
        </button>;
      })}
    </div>
  );
}

/* ── pantallas ────────────────────────────────────────────── */
/* Cada alerta está atada a un trabajo real. Si el trabajo se cerró o el dueño
   la marcó vista, la alerta desaparece y el contador baja con ella: no hay un
   número escrito a mano ni una copia congelada del trabajo.                  */
/* Una sola resolución de alertas para las tres superficies: el rótulo y el
   texto salen del trabajo vivo. El panel tenía su propia lista literal, así
   que prometía "Mandar" un presupuesto enviado hace seis días. */
function alertasDe(jobs, visto = []) {
  return ALERTAS.map(a => { const t = jobs.find(x => x.id === a.job);
    return { ...a, trabajo:t,
      a:typeof a.a === 'function' ? (t ? a.a(t) : '') : a.a,
      tx:typeof a.tx === 'function' ? (t ? a.tx(t) : null) : a.tx }; })
    .filter(a => a.trabajo && !esCierre(a.trabajo.tsm) && !visto.includes(a.id));
}

const TONE = { coral:{ c:T.coral, bg:T.coSoft, bd:T.coBorder }, amber:{ c:T.amber, bg:T.aSoft, bd:T.aBorder }, green:{ c:T.green, bg:T.gSoft, bd:T.gBorder } };
const ALERTAS = [
  { id:'a1', job:'1042', ic:'↺', tone:'coral', a:'Pasarlo', acc:'pasar',
    tx:<>Paula pidió pasar el turno de las <b style={{color:T.c100}}>14:30 al viernes</b>.</> },
  /* El rótulo y el texto salen del ESTADO del presupuesto: uno sin enviar se
     manda, uno enviado hace 6 días se insiste. Tenerlo escrito a mano hacía
     que la alerta prometiera "Mandar" y el documento ofreciera "Insistirle". */
  { id:'a2', job:'1041', ic:'!', tone:'amber', acc:'enviar',
    a:j => j.enviado ? 'Insistir' : 'Mandar',
    tx:j => j.enviado
      ? <>El presupuesto de Carlos <b style={{color:T.c100}}>vence hoy</b> y sigue sin respuesta.</>
      : <>El presupuesto de Carlos está <b style={{color:T.c100}}>sin enviar</b>.</> },
  { id:'a3', job:'1040', ic:'✓', tone:'green', a:'Listo', acc:'visto',
    tx:<>Marina confirmó y pagó la seña.</> },
];

/* Los tres números de esta pantalla salen del MISMO array. Si dicen "hoy",
   cuentan hoy: nada de sumar el trabajo de mañana en el total del día.     */
function Hoy({ open, jobs, cfg, visto, onAlerta, onPaso, verMate }) {
  const [armadoA, pedirA] = useArmar();
  const hoy = jobs.filter(j => j.dia === 'HOY' && !esCierre(j.tsm));
  const aCobrar = hoy.filter(j => j.tsm !== 'RELEVADO').reduce((s,j) => s + j.monto, 0);
  const listos = hoy.filter(j => j.tsm === 'CONFIRMADO').length;
  const enCurso = hoy[0] || null;
  const abiertos = jobs.filter(j => !esCierre(j.tsm));
  const pendiente = abiertos.reduce((s,j) => s + j.monto, 0);
  const alertas = ALERTAS.map(a => { const t = jobs.find(x => x.id === a.job);
      return { ...a, trabajo:t, a:typeof a.a === 'function' ? (t ? a.a(t) : '') : a.a, tx:typeof a.tx === 'function' ? (t ? a.tx(t) : null) : a.tx }; })
    .filter(a => a.trabajo && a.trabajo.est !== 'cerrado' && !visto.includes(a.id));
  const saludo = !hoy.length
    ? <>Hoy no te queda nada agendado. Te avisé por los presupuestos abiertos.</>
    : hoy.length === 1
      ? <>Te queda <b>un trabajo</b> hoy{listos ? ', y ya está para cerrar' : ''}.</>
      : listos === hoy.length
        ? <>Tenés <b>{hoy.length} trabajos</b> hoy y los {hoy.length === 2 ? 'dos' : hoy.length} están para cerrar.</>
        : <>Tenés <b>{hoy.length} trabajos</b> hoy{listos ? <> y {listos === 1 ? 'uno ya está' : listos + ' ya están'} para cerrar</> : ''}.</>;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {/* La card de Mate abre su hilo: no sólo saluda, es el canal por donde
          avisa cambios, cancelaciones, cierre de jornada y los reportes. */}
      <button onClick={verMate} style={{ background:T.g900, border:`1px solid ${T.g800}`, borderRadius:'18px 18px 18px 6px', padding:'14px 16px', cursor:'pointer', textAlign:'left', width:'100%' }}>
        <Lbl mb={6}>MATE · 8:40</Lbl>
        <div style={{ fontFamily:T.sans, fontSize:15, lineHeight:1.5, color:T.c100 }}>Buen día. {saludo}</div>
        <div style={{ fontFamily:T.mono, fontSize:9, letterSpacing:'.1em', color:T.c300, marginTop:9 }}>VER TODO LO QUE ME AVISÓ →</div>
      </button>

      {/* Vacío ≠ cero: sin trabajos hoy no van dos ceros gigantes, va una card
          que mide lo único que queda para hacer. */}
      {hoy.length ? (
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
          <Card pad={14} gap={2}>
            <Lbl>A COBRAR HOY</Lbl>
            <div style={{ fontFamily:T.mono, fontSize:19, fontWeight:500, color:T.c100, letterSpacing:'-.4px', marginTop:4 }}>{money(aCobrar)}</div>
          </Card>
          <Card pad={14} gap={2}>
            <Lbl>TRABAJOS HOY</Lbl>
            <div style={{ fontFamily:T.display, fontSize:30, lineHeight:1.16, color:T.c100, marginTop:2 }}>{hoy.length}</div>
          </Card>
        </div>
      ) : (
        <Card pad={14} gap={0}>
          <Lbl>HOY</Lbl>
          <div style={{ fontFamily:T.sans, fontSize:15, color:T.c300, marginTop:6 }}>Sin trabajos agendados.</div>
          {abiertos.length > 0 && <div style={{ display:'flex', alignItems:'baseline', gap:10, marginTop:12, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
            <div style={{ fontFamily:T.sans, fontSize:13.5, color:T.c500, flex:1 }}>{abiertos.length === 1 ? 'Queda 1 presupuesto' : 'Quedan ' + abiertos.length + ' presupuestos'} esperando respuesta</div>
            <div style={{ fontFamily:T.mono, fontSize:15, fontWeight:500, color:T.c100, letterSpacing:'-.3px' }}>{money(pendiente)}</div>
          </div>}
        </Card>
      )}

      {enCurso && <Card pad={0} gap={0} style={{ overflow:'hidden', cursor:'pointer' }} onClick={() => open(enCurso)} role="button">
        <div style={{ display:'flex', alignItems:'center', gap:10, padding:'13px 16px', borderBottom:`1px solid ${T.g800}` }}>
          <i style={{ width:9, height:9, borderRadius:9, background:T.green, boxShadow:`0 0 0 4px ${T.gSoft}`, display:'block', animation:'tgpulse 2s ease-in-out infinite' }} />
          <span style={{ fontFamily:T.mono, fontSize:9.5, letterSpacing:'.13em', color:T.green }}>AHORA</span>
          <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:10, color:T.c500 }}>{enCurso.hora}</span>
        </div>
        <div style={{ padding:'14px 16px', display:'flex', flexDirection:'column', gap:12 }}>
          <div>
            <div style={{ fontFamily:T.sans, fontSize:17, fontWeight:700, color:T.c100, letterSpacing:'-.3px' }}>{enCurso.svc}</div>
            <div style={{ fontFamily:T.sans, fontSize:13, color:T.c500, marginTop:2 }}>{enCurso.cli}</div>
          </div>
          {/* Los chips se resuelven por CLAVE, no por índice. Estaban en 1/2/3,
              que era correcto con la escalera vieja de cuatro pasos; con la de
              ocho, "Cobrado" caía en "Presupuesto elaborado" y se pintaba verde
              como si hubieras cobrado — con el total del día sin moverse y sin
              recibo emitido. Y "Llegó" apuntaba a "Turno confirmado".        */}
          <div style={{ display:'flex', gap:6 }}>
            {atajos(cfg).map(({ k, n, t }) => { const hecho = enCurso.paso >= n, activo = enCurso.paso + 1 === n;
              const habla = saltoHabla(pasosDe(cfg), enCurso.paso, n), arm = armadoA === k;
              return <button key={k} onClick={e => { e.stopPropagation(); habla && !hecho ? pedirA(k, () => onPaso(enCurso.id, n)) : onPaso(enCurso.id, n); }} style={{ flex:1, minWidth:0, minHeight:44, fontFamily:T.sans, fontSize:11.5, fontWeight:600, padding:'8px 6px', borderRadius:999, cursor:'pointer', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', color:arm?T.amber:hecho?T.green:activo?T.c100:T.c500, background:arm?T.aSoft:hecho?T.gSoft:activo?T.vSoft:'transparent', border:`1px solid ${arm?T.aBorder:hecho?T.gBorder:activo?T.vBorder:T.g700}` }}>{arm ? '¿Seguro?' : t}</button>;
            })}
          </div>
        </div>
      </Card>}

      <Recorrido jobs={jobs} cfg={cfg} open={open} />

      {alertas.length > 0 && <div>
        <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:10 }}>
          <div style={{ fontFamily:T.display, fontSize:19, lineHeight:1.16, letterSpacing:'.4px', color:T.c100 }}>ATENCIÓN REQUERIDA</div>
          <span style={{ fontFamily:T.mono, fontSize:10, color:T.c500 }}>{alertas.length}</span>
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
          {alertas.map(a => { const t = TONE[a.tone];
            return <div key={a.id} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 14px', minHeight:64, background:T.g900, border:`1px solid ${t.bd}`, borderRadius:14 }}>
              <span style={{ width:30, height:30, flex:'none', borderRadius:8, display:'grid', placeItems:'center', background:t.bg, color:t.c, fontSize:15 }}>{a.ic}</span>
              <span style={{ flex:1, fontFamily:T.sans, fontSize:13.5, lineHeight:1.45, color:T.c300 }}>{a.tx}</span>
              <button onClick={() => onAlerta(a)} style={{ flex:'none', minHeight:44, fontFamily:T.sans, fontSize:12.5, fontWeight:600, color:T.c100, background:'transparent', border:`1px solid ${T.g700}`, borderRadius:8, padding:'0 13px', cursor:'pointer' }}>{a.a}</button>
            </div>;
          })}
        </div>
      </div>}
    </div>
  );
}

const FILTROS = [
  { id:'Todos',       t:'Todos',       test:() => true },
  { id:'Confirmados', t:'Confirmados', test:j => j.tsm === 'CONFIRMADO' },
  { id:'Esperando',   t:'Esperando',   test:j => j.tsm === 'CREADO' || j.tsm === 'RELEVADO' },
];
/* Tres filtros, no cuatro: cuatro sumaban 433 px en una ventana de 412 y el
   último quedaba tajeado — el defecto que esta misma pieza denuncia. El conteo
   informa más que un cuarto chip, y la diferencia entre esperar una confirmación
   y no tener respuesta sigue leyéndose en el badge de cada card.                */
function Trabajos({ open, jobs, filtro, setFiltro }) {
  const f = FILTROS.find(x => x.id === filtro) || FILTROS[0];
  const vis = jobs.filter(f.test);
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <div style={{ display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:7 }}>
        {FILTROS.map(x => <Chip key={x.id} on={filtro===x.id} onClick={() => setFiltro(x.id)}>{x.t} <b style={{ fontWeight:400, opacity:.65 }}>{jobs.filter(x.test).length}</b></Chip>)}
      </div>
      <div style={{ display:'flex', flexDirection:'column', gap:9 }}>
        {vis.map(j => (
          <div key={j.id} onClick={() => open(j)} style={{ background:T.g900, border:`1px solid ${T.g800}`, borderRadius:14, padding:14, display:'flex', flexDirection:'column', gap:11, cursor:'pointer' }}>
            <div style={{ display:'flex', alignItems:'flex-start', gap:12 }}>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontFamily:T.sans, fontSize:15.5, fontWeight:700, color:T.c100, letterSpacing:'-.2px' }}>{j.svc}</div>
                <div style={{ fontFamily:T.sans, fontSize:12.5, color:T.c500, marginTop:2 }}>{j.cli}</div>
              </div>
              <div style={{ fontFamily:T.mono, fontSize:14, color:T.c100, letterSpacing:'-.2px', whiteSpace:'nowrap' }}>{money(j.monto)}</div>
            </div>
            <div style={{ display:'flex', alignItems:'center', gap:8, paddingTop:11, borderTop:`1px solid ${T.g800}` }}>
              <span style={{ fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500, marginRight:'auto' }}>{j.dia}{j.hora !== '—' && ' · ' + j.hora}</span>
              <Badge tsm={j.tsm} dot={j.tsm==='CONFIRMADO'} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function Detalle({ job, cfg, onPaso, onDoc, onNotas, onAcc }) {
  /* Los peldaños que disparan un WhatsApp piden confirmación: un toque
     accidental le avisa al cliente algo que no pasó, y el mensaje no vuelve. */
  const [armado, pedir] = useArmar();
  const pasos = pasosDe(cfg), ult = pasos.length - 1;
  /* Un trabajo FINALIZADO no está cerrado: falta la plata. Tomarlo como cerrado
     bloqueaba la escalera justo antes de cobrar. */
  const done = job.tsm === 'CANCELADO' || !!(job.hist || [])[ult];
  /* Cerrado no es lo mismo que cobrado: un CANCELADO también cierra el ticket
     pero no generó plata ni recibo. */
  const pago = esCobrado(job.tsm);
  const canc = job.tsm === 'CANCELADO';
  const paso = pago ? ult : job.paso;
  const esPres = job.dia === 'PRESUPUESTO';
  const nn = (job.notas || []).length;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <Card>
        <div style={{ display:'flex', alignItems:'flex-start', gap:12 }}>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontFamily:T.sans, fontSize:20, fontWeight:700, color:T.c100, letterSpacing:'-.5px' }}>{job.svc}</div>
            <div style={{ fontFamily:T.sans, fontSize:13.5, color:T.c500, marginTop:3 }}>{job.cli} · {job.tel}</div>
          </div>
          <Badge tsm={job.tsm} dot={job.tsm==='CONFIRMADO'} />
        </div>
        <div style={{ display:'flex', alignItems:'baseline', gap:10, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
          <div><Lbl mb={4}>{pago ? 'COBRADO' : canc ? 'NO SE COBRÓ' : 'A COBRAR'}</Lbl><div style={{ fontFamily:T.mono, fontSize:22, fontWeight:500, color:pago?T.green:canc?T.c500:T.c100, letterSpacing:'-.5px', textDecoration:canc?'line-through':'none' }}>{money(job.monto)}</div></div>
          <div style={{ marginLeft:'auto', textAlign:'right' }}><Lbl mb={4}>CUÁNDO</Lbl><div style={{ fontFamily:T.mono, fontSize:12.5, color:T.c300 }}>{job.dia}{job.hora !== '—' && ' · ' + job.hora}</div></div>
        </div>
      </Card>

      {(pago || esPres) && <button onClick={() => onDoc(pago ? 'recibo' : 'presupuesto')} style={{ display:'flex', alignItems:'center', gap:13, minHeight:68, padding:'0 16px', background:T.g900, border:`1px solid ${done?T.gBorder:T.vBorder}`, borderRadius:18, cursor:'pointer', textAlign:'left', width:'100%' }}>
        <span style={{ width:30, height:37, flex:'none', borderRadius:4, border:`1px solid ${pago?T.gBorder:T.vBorder}`, background:pago?T.gSoft:T.vSoft, display:'grid', placeItems:'center', fontFamily:T.mono, fontSize:8, color:pago?T.green:T.v600 }}>{pago?'REC':'PRE'}</span>
        <span style={{ flex:1, minWidth:0 }}>
          <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100 }}>{pago ? `Recibo N° 00${job.id}` : `Presupuesto N° ${job.id}`}</span>
          <span style={{ display:'block', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500, marginTop:3 }}>{pago ? 'EMITIDO Y ENVIADO' : job.enviado ? 'ENVIADO · SIN RESPUESTA' : 'SIN ENVIAR'}</span>
        </span>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="2.2"><path d="M9 6l6 6-6 6"/></svg>
      </button>}

      <button onClick={onNotas} style={{ display:'flex', alignItems:'center', gap:12, minHeight:60, padding:'0 16px', background:T.g900, border:`1px solid ${T.g800}`, borderRadius:18, cursor:'pointer', textAlign:'left', width:'100%' }}>
        <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={nn?T.v600:T.c500} strokeWidth="1.8" style={{ flex:'none' }}><path d="M5 4h11l4 4v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M8 12h8M8 16h5"/></svg>
        <span style={{ flex:1, minWidth:0 }}>
          <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100 }}>Anotaciones y fotos</span>
          <span style={{ display:'block', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:nn ? T.c100 : T.c500, marginTop:3 }}>{nn ? `${nn} ${nn === 1 ? 'ANOTADA' : 'ANOTADAS'}` : 'NADA ANOTADO'}</span>
        </span>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="2.2"><path d="M9 6l6 6-6 6"/></svg>
      </button>

      <Bitacora job={job} />

      <Card>
        <div style={{ display:'flex', alignItems:'baseline', gap:10 }}>
          <Lbl>ESTADO DEL TRABAJO</Lbl>
          <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', color:T.c500 }}>QUIÉN LO MARCÓ</span>
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap:2 }}>
          {/* `ok` sale de hist y de nada más: el atajo "si cobró, todo hecho"
              volvía a pintar los pasos de presupuesto que nunca ocurrieron. */}
          {pasos.map((k,i) => { const d = pasoDe(k, cfg.patron, job), ev = evidencia(job, k);
            const por = (job.hist||[])[i] || (ev ? d.por : null), ok = !!por;
            const tocable = !done && i === paso + 1 && !d.auto;
            const habla = saltoHabla(pasos, job.paso, i);
            const go = () => { if (done) return;
              if (d.ir === 'items') { onDoc('items'); return; }
              if (d.auto) return;
              onPaso(job.id, i); };
            /* Retroceder no manda nada, así que no pide confirmación. */
            const volver = ok && !done && i === job.paso && !MSG_WA[k];
            const accion = () => (habla && !ok) ? pedir(k, go) : go();
            const arm = armado === k;
            /* Un paso que hay que HACER no es un check: mientras está pendiente
               va como botón, y recién cuando se hizo pasa a ser un hito ✓. */
            if (d.ir && !ok) return (
              <button key={k} onClick={accion} disabled={done} style={{ display:'flex', alignItems:'center', gap:10, minHeight:46, width:'100%', margin:'4px 0 6px', padding:'0 13px', background:T.vSoft, border:`1px solid ${T.vBorder}`, borderRadius:11, cursor:done?'default':'pointer', textAlign:'left' }}>
                <span style={{ width:20, height:20, flex:'none', borderRadius:6, display:'grid', placeItems:'center', background:T.v600, color:'#fff', fontSize:13, lineHeight:1 }}>+</span>
                <span style={{ flex:1, minWidth:0 }}>
                  <span style={{ display:'block', fontFamily:T.sans, fontSize:13.5, fontWeight:700, color:T.c100 }}>{d.t}</span>
                  <span style={{ display:'block', fontFamily:T.sans, fontSize:11.5, color:T.c500, marginTop:1 }}>{d.x}</span>
                </span>
                <span style={{ flex:'none', fontFamily:T.mono, fontSize:9, letterSpacing:'.1em', color:T.c300 }}>ABRIR →</span>
              </button>
            );
            return <button key={k} onClick={accion} disabled={done || d.auto} style={{ display:'flex', alignItems:'center', gap:12, minHeight:48, background:'transparent', border:'none', padding:0, textAlign:'left', width:'100%', cursor:done?'default':'pointer' }}>
              <span style={{ width:22, height:22, flex:'none', borderRadius:9, display:'grid', placeItems:'center', background:ok?T.gSoft:'transparent', border:`1px solid ${ok?T.gBorder:T.g700}`, color:ok?T.green:T.g700, fontSize:11 }}>{ok?'✓':''}</span>
              <span style={{ flex:1, minWidth:0 }}>
                <span style={{ display:'block', fontFamily:T.sans, fontSize:14, fontWeight:ok?600:400, color:ok?T.c100:T.c500 }}>{ok && d.th ? d.th : d.t}</span>
                {!ok && <span style={{ display:'block', fontFamily:T.sans, fontSize:11.5, lineHeight:1.4, color:T.c500, marginTop:2 }}>{d.x}</span>}
              </span>
              {por
                ? <span style={{ flex:'none', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', padding:'3px 7px', borderRadius:4, color:por==='mia'?T.c100:T.c300, background:por==='mia'?T.vSoft:T.g800, border:`1px solid ${por==='mia'?T.vBorder:T.g700}` }}>{por==='mia'?'MIA':'VOS'}</span>
                : d.auto ? <span style={{ flex:'none', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', color:T.c500 }}>AUTOMÁTICO</span>
                : (tocable || d.ir) && <span style={{ flex:'none', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', color:arm?T.amber:T.c300 }}>{arm ? '¿SEGURO?' : d.ir ? 'ABRIR →' : 'TOCAR'}</span>}
              {volver && <span style={{ flex:'none', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', color:T.c500 }}>DESHACER</span>}
            </button>;
          })}
        </div>
        <div style={{ paddingTop:11, borderTop:`1px solid ${T.g800}`, fontFamily:T.sans, fontSize:12, lineHeight:1.5, color:T.c500 }}>Los pasos de Mia se tildan solos: agendar, confirmar, mandar el presupuesto y avisar que lo aceptaron. <b style={{ color:T.c300 }}>Los tuyos se tocan</b>, y el del presupuesto te lleva directo a armarlo.</div>
      </Card>

      <Card>
        <Lbl>{job.extra.toUpperCase()}</Lbl>
        <div style={{ fontFamily:T.sans, fontSize:13.5, lineHeight:1.5, color:T.c300 }}>
          {canc ? <>Le avisé que se canceló. Si quiere retomarlo, te lo vuelvo a pasar.</>
                : pago ? <>Le mandé el recibo por WhatsApp y quedó registrado en el cierre del mes.</>
                : job.tsm === 'CREADO' && esPres ? <>Se lo mandé hace 6 días y no contestó. Puedo mandarle un recordatorio ahora.</>
                : <>Ya le confirmé el turno y la seña. Cuando lo cierres le mando el recibo sola.</>}
        </div>
      </Card>
    </div>
  );
}

function Clientes({ open, q, setQ, clientes, nuevo, jobs }) {
  const vis = clientes.filter(c => c.n.toLowerCase().includes(q.toLowerCase()));
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <div style={{ position:'relative' }}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="2" style={{ position:'absolute', left:13, top:15 }}><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
        <input value={q} onChange={e => setQ(e.target.value)} placeholder="Buscar cliente" style={{ width:'100%', height:46, boxSizing:'border-box', padding:'0 14px 0 36px', background:T.g900, border:`1px solid ${T.g700}`, borderRadius:12, color:T.c100, fontFamily:T.sans, fontSize:14.5, outline:'none' }} />
      </div>
      <div style={{ display:'flex', flexDirection:'column', gap:9 }}>
        {vis.map(c => { const r = clienteResumen(c, jobs);
          return <Row key={c.n} onClick={() => open(c)}>
            <span style={{ width:38, height:38, flex:'none', borderRadius:99, background:T.vSoft, border:`1px solid ${T.vBorder}`, display:'grid', placeItems:'center', fontFamily:T.sans, fontSize:13.5, fontWeight:700, color:T.v600 }}>{c.n.split(' ').map(w=>w[0]).join('')}</span>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ display:'flex', alignItems:'center', gap:7 }}>
                <span style={{ fontFamily:T.sans, fontSize:15, fontWeight:600, color:T.c100, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.n}</span>
                {c.tag && <span style={{ fontFamily:T.mono, fontSize:8.5, letterSpacing:'.09em', textTransform:'uppercase', color:T.c500, background:T.g800, border:`1px solid ${T.g700}`, borderRadius:4, padding:'3px 6px', flex:'none' }}>{c.tag}</span>}
              </div>
              <div style={{ fontFamily:T.mono, fontSize:10, color:T.c500, marginTop:3 }}>{r.visitas} {r.visitas === 1 ? 'visita' : 'visitas'}{r.proxima !== '—' ? ' · ' + r.proxima : ''}</div>
            </div>
            <span style={{ fontFamily:T.mono, fontSize:12.5, color:T.c300, flex:'none' }}>{money(r.facturado)}</span>
          </Row>;
        })}
        {!vis.length && <div style={{ padding:'34px 16px', textAlign:'center', display:'flex', flexDirection:'column', alignItems:'center', gap:12 }}>
          <div style={{ fontFamily:T.sans, fontSize:14, color:T.c500, lineHeight:1.5 }}>No encontré a nadie con ese nombre.</div>
          <button onClick={() => nuevo(q)} style={{ minHeight:44, fontFamily:T.sans, fontSize:13.5, fontWeight:600, color:T.v600, background:T.vSoft, border:`1px solid ${T.vBorder}`, borderRadius:10, padding:'0 18px', cursor:'pointer' }}>Cargar {q.trim() ? '“' + q.trim() + '”' : 'un cliente'}</button>
          <button onClick={() => setQ('')} style={{ minHeight:44, fontFamily:T.sans, fontSize:13, fontWeight:600, color:T.c500, background:'transparent', border:'none', cursor:'pointer', padding:'0 10px' }}>Ver todos los clientes</button>
        </div>}
      </div>
    </div>
  );
}

function ClienteDet({ cli, jobs, abrir, onMapa, verHist }) {
  const r = clienteResumen(cli, jobs);
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <Card>
        <div style={{ display:'flex', alignItems:'center', gap:13 }}>
          <span style={{ width:50, height:50, flex:'none', borderRadius:99, background:T.vSoft, border:`1px solid ${T.vBorder}`, display:'grid', placeItems:'center', fontFamily:T.sans, fontSize:17, fontWeight:700, color:T.v600 }}>{cli.n.split(' ').map(w=>w[0]).join('')}</span>
          <div style={{ minWidth:0 }}>
            <div style={{ fontFamily:T.sans, fontSize:19, fontWeight:700, color:T.c100, letterSpacing:'-.4px' }}>{cli.n}</div>
            <div style={{ fontFamily:T.mono, fontSize:11, color:T.c500, marginTop:3 }}>{cli.tel || 'sin teléfono'}</div>
          </div>
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap:10, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
          <div style={{ display:'flex', alignItems:'flex-start', gap:10 }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="1.8" style={{ flex:'none', marginTop:1 }}><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="2.6"/></svg>
            <span style={{ flex:1, minWidth:0, fontFamily:T.sans, fontSize:13.5, lineHeight:1.45, color:cli.dir?T.c300:T.c500 }}>{cli.dir || 'Sin dirección cargada'}</span>
            {cli.dir && <button onClick={() => onMapa(cli)} style={{ flex:'none', minHeight:36, fontFamily:T.sans, fontSize:12, fontWeight:600, color:T.v600, background:T.vSoft, border:`1px solid ${T.vBorder}`, borderRadius:8, padding:'0 11px', cursor:'pointer' }}>Mapa</button>}
          </div>
          {cli.notas && <div style={{ display:'flex', alignItems:'flex-start', gap:10 }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="1.8" style={{ flex:'none', marginTop:1 }}><path d="M5 4h11l4 4v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M8 12h8M8 16h5"/></svg>
            <span style={{ flex:1, minWidth:0, fontFamily:T.sans, fontSize:13.5, lineHeight:1.45, color:T.c300 }}>{cli.notas}</span>
          </div>}
        </div>
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:10, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
          {[['VISITAS', r.visitas], ['FACTURADO', money(r.facturado)], ['PRÓXIMA', r.proxima]].map(([k,v]) => (
            <div key={k}><Lbl mb={5}>{k}</Lbl><div style={{ fontFamily:T.mono, fontSize:12.5, color:T.c100, letterSpacing:'-.2px' }}>{v}</div></div>
          ))}
        </div>
      </Card>
      <Card>
        <Lbl>SUS TRABAJOS</Lbl>
        {r.suyos.length ? (
          <div style={{ display:'flex', flexDirection:'column', gap:0 }}>
            {r.suyos.map((j,i) => (
              <button key={j.id} onClick={() => abrir(j)} style={{ display:'flex', alignItems:'center', gap:10, minHeight:52, borderTop:i?`1px solid ${T.g800}`:'none', background:'transparent', border:'none', padding:0, cursor:'pointer', textAlign:'left', width:'100%' }}>
                <span style={{ fontFamily:T.sans, fontSize:14, color:T.c100, flex:1, minWidth:0, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{j.svc}</span>
                <Badge tsm={j.tsm} />
                <span style={{ fontFamily:T.mono, fontSize:12.5, color:T.c300, minWidth:62, textAlign:'right', flex:'none' }}>{money(j.monto)}</span>
              </button>
            ))}
          </div>
        ) : (
          <div style={{ fontFamily:T.sans, fontSize:13.5, lineHeight:1.5, color:T.c500 }}>Ahora mismo no tiene ningún trabajo abierto.</div>
        )}
      </Card>
      {r.hist.length > 0 && (
        <button onClick={verHist} style={{ display:'flex', alignItems:'center', gap:12, minHeight:60, padding:'0 16px', background:T.g900, border:`1px solid ${T.g800}`, borderRadius:18, cursor:'pointer', textAlign:'left', width:'100%' }}>
          <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="1.8" style={{ flex:'none' }}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
          <span style={{ flex:1, minWidth:0 }}>
            <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100 }}>Todo el historial</span>
            <span style={{ display:'block', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500, marginTop:3 }}>{r.hist.length} TRABAJOS ANTERIORES · DESDE JUN</span>
          </span>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="2.2"><path d="M9 6l6 6-6 6"/></svg>
        </button>
      )}
    </div>
  );
}

/* Historial completo del cliente. Cada trabajo lista SUS documentos —el
   presupuesto y el recibo—, no el último: el presupuesto de hace tres meses
   es justo lo que se busca cuando el cliente vuelve a llamar. */
function Historial({ cli, hist, jobs, abrirDoc }) {
  const vivos = jobs.filter(j => j.cli === cli.n);
  const fila = (id, svc, cuando, monto, tsm, pres, real) => {
    const canc = tsm === 'CANCELADO';
    /* El recibo existe reci\u00e9n cuando el trabajo se cobr\u00f3. Ofrecerlo antes es
       prometer un documento que no se emiti\u00f3. */
    const hayRec = esCobrado(tsm);
    return (
      <Card key={id} pad={0} gap={0} style={{ overflow:'hidden' }}>
        <div style={{ display:'flex', alignItems:'flex-start', gap:11, padding:'13px 14px' }}>
          <span style={{ flex:1, minWidth:0 }}>
            <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:canc?T.c500:T.c100, textDecoration:canc?'line-through':'none' }}>{svc}</span>
            <span style={{ display:'block', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.08em', color:T.c500, marginTop:3 }}>#{id} · {cuando} · {EST_LBL[tsm] || tsm}</span>
          </span>
          <span style={{ fontFamily:T.mono, fontSize:13, color:canc?T.c500:T.c300, flex:'none' }}>{money(monto)}</span>
        </div>
        {(pres || hayRec) ? (
          <div style={{ display:'flex', gap:1, borderTop:`1px solid ${T.g800}` }}>
            {pres && <button onClick={() => abrirDoc(id, 'presupuesto', real)} style={{ flex:1, minHeight:44, background:'transparent', border:'none', borderRight:hayRec?`1px solid ${T.g800}`:'none', color:T.v600, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>Presupuesto</button>}
            {hayRec && <button onClick={() => abrirDoc(id, 'recibo', real)} style={{ flex:1, minHeight:44, background:'transparent', border:'none', color:T.green, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>Recibo</button>}
          </div>
        ) : (
          <div style={{ borderTop:`1px solid ${T.g800}`, minHeight:40, display:'grid', placeItems:'center', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500 }}>{canc ? 'SIN DOCUMENTOS' : 'TODAVÍA SIN DOCUMENTOS'}</div>
        )}
      </Card>
    );
  };
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:11 }}>
      {vivos.length > 0 && <>
        <Lbl>ABIERTOS AHORA</Lbl>
        {vivos.map(j => fila(j.id, j.svc, j.dia === 'PRESUPUESTO' ? 'sin fecha' : `${j.dia} · ${j.hora}`, j.monto, j.tsm, j.dia === 'PRESUPUESTO' || esCierre(j.tsm), true))}
      </>}
      <div style={{ paddingTop:vivos.length?6:0 }}><Lbl>ANTERIORES</Lbl></div>
      {hist.map(h => fila(h.id, h.svc, h.cuando, h.monto, h.tsm, h.pres, false))}
      <div style={{ fontFamily:T.sans, fontSize:12.5, lineHeight:1.55, color:T.c500, padding:'4px 4px 0' }}>Cada trabajo guarda sus dos documentos. El presupuesto viejo es lo que buscás cuando el cliente vuelve a llamar por lo mismo.</div>
    </div>
  );
}

function Negocio({ abrirCfg, destacado, reVerGuia, resumen, jobs, verDocs, cards, conPresupuesto, cfg, pend }) {
  const nDocs = { pre:jobs.filter(j => !conHora(j) && !esCierre(j.tsm)).length, rec:jobs.filter(j => esCobrado(j.tsm)).length, can:jobs.filter(j => j.tsm === 'CANCELADO').length };
  /* Los cuatro números salen de `jobs` con los MISMOS predicados que usa el
     panel web. Eran literales de antes del refactor del TSM y la pantalla se
     contradecía sola: decía $412.500 cobrado con cero recibos emitidos. */
  const cobrado = jobs.filter(j => esCobrado(j.tsm)).reduce((s,j) => s + j.monto, 0);
  const aCobrar = jobs.filter(j => !esCierre(j.tsm) && conHora(j) && j.tsm !== 'RELEVADO').reduce((s,j) => s + j.monto, 0);
  const enPres  = jobs.filter(j => !esCierre(j.tsm) && !conHora(j)).reduce((s,j) => s + j.monto, 0);
  const perdido = jobs.filter(j => j.tsm === 'CANCELADO').reduce((s,j) => s + j.monto, 0);
  const bars = [['Cobrado', cobrado, T.green], ['A cobrar', aCobrar, T.v600], ['En presupuestos', enPres, T.amber], ['Perdido', perdido, T.coral]].filter(b => b[1] > 0);
  const max = Math.max(cobrado, aCobrar, enPres, perdido, 1);
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <Card>
        <Lbl>SALUD DEL NEGOCIO · SEPTIEMBRE</Lbl>
        <div style={{ fontFamily:T.mono, fontSize:26, fontWeight:500, color:T.c100, letterSpacing:'-.8px' }}>{money(cobrado)}</div>
        <div style={{ fontFamily:T.sans, fontSize:12.5, color:T.c500, marginTop:-6 }}>{cobrado ? 'cobrado en lo que va del mes' : 'cobrado todavía este mes'}</div>
        <div style={{ display:'flex', flexDirection:'column', gap:11, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
          {bars.map(([k,v,c]) => (
            <div key={k} style={{ display:'grid', gridTemplateColumns:'74px 1fr 74px', alignItems:'center', gap:10 }}>
              <span style={{ fontFamily:T.sans, fontSize:12.5, color:T.c300 }}>{k}</span>
              <span style={{ height:7, background:T.g800, borderRadius:9, overflow:'hidden' }}><i style={{ display:'block', height:'100%', width:`${v/max*100}%`, background:c, borderRadius:9 }} /></span>
              <span style={{ fontFamily:T.mono, fontSize:11, color:T.c100, textAlign:'right' }}>{money(v)}</span>
            </div>
          ))}
        </div>
      </Card>
      <Card>
        <div style={{ display:'flex', alignItems:'center', gap:10 }}>
          <Lbl>RESEÑAS</Lbl>
          <span style={{ marginLeft:'auto', fontFamily:T.display, fontSize:24, lineHeight:1.16, color:T.c100 }}>4,8</span>
          <span style={{ color:T.v600, fontSize:13, letterSpacing:1 }}>★★★★★</span>
        </div>
        <div style={{ borderTop:`1px solid ${T.g800}`, paddingTop:12, fontFamily:T.sans, fontSize:13.5, lineHeight:1.5, color:T.c300, fontStyle:'italic' }}>“Puntual y prolija. Volvería sin dudar.”</div>
        <div style={{ fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500 }}>PAULA G. · HACE 2 DÍAS</div>
      </Card>
      <button onClick={verDocs} style={{ display:'flex', alignItems:'center', gap:12, minHeight:62, padding:'0 16px', background:T.g900, border:`1px solid ${T.g800}`, borderRadius:18, cursor:'pointer', textAlign:'left', width:'100%' }}>
        <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="1.8" style={{ flex:'none' }}><path d="M5 4h11l4 4v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M15 4v5h5"/></svg>
        <span style={{ flex:1, minWidth:0 }}>
          <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100 }}>Documentos</span>
          <span style={{ display:'block', fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.c500, marginTop:3 }}>{nDocs.pre} PRESUP · {nDocs.rec} RECIBOS · {nDocs.can} CANCELADOS</span>
        </span>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={T.c500} strokeWidth="2.2"><path d="M9 6l6 6-6 6"/></svg>
      </button>
      <Card pad={0} gap={0} style={{ overflow:'hidden' }}>
        {cards.map((c,i) => { const on = destacado === c.k, raiz = c.k === 'Tu rubro', preset = esPreset(cfg, c.k), sinAplicar = pend.includes(c.k);
          /* raíz en violeta-600 daba 4.05:1 a 9px; el punto de color sigue en violeta
             y el texto sube a cloud-300. */
          const col = sinAplicar ? T.amber : raiz ? T.v600 : preset ? T.amber : T.green;
          const colTxt = raiz ? T.c300 : col;
          return <button key={c.k} onClick={() => abrirCfg(c)} style={{ display:'flex', alignItems:'center', gap:10, minHeight:60, width:'100%', padding:'10px 14px', background:on?T.vSoft:'transparent', border:'none', borderTop:i?`1px solid ${T.g800}`:'none', boxShadow:on?`inset 0 0 0 1px ${T.vBorder}`:'none', cursor:'pointer', textAlign:'left' }}>
            <span style={{ width:6, height:6, flex:'none', borderRadius:9, background:col }} />
            <span style={{ flex:1, minWidth:0 }}>
              <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, color:T.c100, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.k}</span>
              <span style={{ display:'block', fontFamily:T.mono, fontSize:9, letterSpacing:'.09em', color:colTxt, marginTop:2, whiteSpace:'nowrap' }}>{sinAplicar ? 'SIN APLICAR EN MIA' : raiz ? 'DE ACÁ SALE TODO' : preset ? 'VIENE DE TU RUBRO' : 'LO AJUSTASTE VOS'}</span>
            </span>
            <span style={{ fontFamily:T.mono, fontSize:10.5, color:on ? T.c100:T.c500, flex:'0 1 auto', minWidth:0, maxWidth:'50%', textAlign:'right', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{resumen[c.k]}</span>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={on?T.v600:T.c500} strokeWidth="2.2" style={{ flex:'none' }}><path d="M9 6l6 6-6 6"/></svg>
          </button>;
        })}
      </Card>
      <button onClick={reVerGuia} style={{ minHeight:48, fontFamily:T.sans, fontSize:13.5, fontWeight:600, color:T.c500, background:'transparent', border:`1px solid ${T.g800}`, borderRadius:12, cursor:'pointer' }}>Volver a ver la guía de configuración</button>
    </div>
  );
}

/* ── hoja de confirmación ─────────────────────────────────── */
/* Cargar un trabajo. El MainButton queda arriba del teclado — que es medio
   punto del hallazgo 06: con viewportStableHeight el campo nunca se tapa.   */
function NuevoCli({ form, setForm }) {
  const campo = (k, label, ph, mono) => (
    <div style={{ display:'flex', flexDirection:'column', gap:7 }}>
      <Lbl>{label}</Lbl>
      <input value={form[k]} onChange={e => setForm({ ...form, [k]:e.target.value })} placeholder={ph} inputMode={mono?'tel':undefined}
        style={{ width:'100%', height:48, boxSizing:'border-box', padding:'0 14px', background:T.g950, border:`1px solid ${form[k]?T.vBorder:T.g700}`, borderRadius:12, color:T.c100, fontFamily:mono?T.mono:T.sans, fontSize:15, outline:'none' }} />
    </div>
  );
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <Card gap={16}>
        {campo('n', 'NOMBRE', 'Nombre y apellido')}
        {campo('tel', 'TELÉFONO', '+54 9 341 ···', true)}
        {campo('dir', 'DIRECCIÓN', 'Calle, altura y barrio')}
        {campo('notas', 'NOTAS', 'Lo que conviene recordar')}
      </Card>
      <div style={{ fontFamily:T.sans, fontSize:12.5, lineHeight:1.55, color:T.c500, padding:'0 4px' }}>Con el teléfono Mia lo reconoce cuando escribe y ya sabe qué le hiciste antes. <b style={{ color:T.c300 }}>En producción esto es un toque:</b> <code style={{ fontFamily:T.mono, fontSize:11 }}>requestContact()</code> trae el número del propio Telegram.</div>
    </div>
  );
}

function Nuevo({ form, setForm, conPresupuesto = true }) {
  const campo = (k, label, ph, mono) => (
    <div style={{ display:'flex', flexDirection:'column', gap:7 }}>
      <Lbl>{label}</Lbl>
      <input value={form[k]} onChange={e => setForm({ ...form, [k]:e.target.value })} placeholder={ph} inputMode={mono?'numeric':undefined}
        style={{ width:'100%', height:48, boxSizing:'border-box', padding:'0 14px', background:T.g950, border:`1px solid ${form[k]?T.vBorder:T.g700}`, borderRadius:12, color:T.c100, fontFamily:mono?T.mono:T.sans, fontSize:15, outline:'none' }} />
    </div>
  );
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {conPresupuesto && <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:7 }}>
        {['Confirmado','Presupuesto'].map(t => <Chip key={t} on={form.tipo===t} onClick={() => setForm({ ...form, tipo:t })}>{t}</Chip>)}
      </div>}
      <Card gap={16}>
        {campo('cli', 'CLIENTE', 'Nombre y apellido')}
        {campo('svc', 'TRABAJO', 'Qué le vas a hacer')}
        {campo('monto', 'CUÁNTO', '0', true)}
      </Card>
      <div style={{ fontFamily:T.sans, fontSize:12.5, lineHeight:1.55, color:T.c500, padding:'0 4px' }}>
        {form.tipo === 'Presupuesto'
          ? <>Armo el presupuesto y te lo muestro antes de mandarlo. Vos le das enviar y se lo paso por WhatsApp.</>
          : <>Con esto Mia ya puede confirmarle el turno. El resto lo completa ella hablando con el cliente.</>}
      </div>
    </div>
  );
}

/* Hoja de confirmación de los cambios de configuración. Aplicar no es
   inmediato — Mia tiene que recargar su configuración y la propagación a
   WhatsApp depende de Meta — así que se avisa antes, no después.          */
function SheetCfg({ pend, resumen, onClose, onConfirm, busy }) {
  return (
    <div onClick={() => !busy && onClose()} style={{ position:'absolute', inset:0, background:'rgba(8,7,6,.62)', display:'flex', alignItems:'flex-end', zIndex:26 }}>
      <div onClick={e => e.stopPropagation()} style={{ width:'100%', background:T.g900, borderTop:`1px solid ${T.g700}`, borderRadius:'20px 20px 0 0', padding:'8px 16px 16px', display:'flex', flexDirection:'column', gap:13 }}>
        <div style={{ width:34, height:4, borderRadius:9, background:T.g700, alignSelf:'center' }} />
        <div>
          <div style={{ fontFamily:T.sans, fontSize:18, fontWeight:700, color:T.c100, letterSpacing:'-.3px' }}>Aplicar {pend.length} {pend.length === 1 ? 'cambio' : 'cambios'} en Mia</div>
          <div style={{ fontFamily:T.sans, fontSize:13, lineHeight:1.5, color:T.c500, marginTop:5 }}>Hasta que no los apliques, Mia sigue atendiendo con la configuración anterior.</div>
        </div>
        <Card pad={0} gap={0} style={{ overflow:'hidden' }}>
          {pend.map((k,i) => (
            <div key={k} style={{ display:'flex', alignItems:'center', gap:10, minHeight:46, padding:'0 14px', borderTop:i?`1px solid ${T.g800}`:'none' }}>
              <span style={{ flex:1, minWidth:0, fontFamily:T.sans, fontSize:13.5, color:T.c100, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{k}</span>
              <span style={{ fontFamily:T.mono, fontSize:10, color:T.c300, flex:'none' }}>{resumen[k]}</span>
            </div>
          ))}
        </Card>
        <div style={{ display:'flex', gap:10, alignItems:'flex-start', padding:'11px 13px', background:T.aSoft, border:`1px solid ${T.aBorder}`, borderRadius:12 }}>
          <span style={{ fontFamily:T.mono, fontSize:12, color:T.amber, flex:'none', lineHeight:1.3 }}>!</span>
          <span style={{ flex:1, fontFamily:T.sans, fontSize:12.5, lineHeight:1.5, color:T.c300 }}>Tarda hasta un par de minutos en tomar efecto, y las charlas que Mia ya tenga abiertas terminan con la configuración vieja. <b style={{ color:T.c100 }}>Te aviso por acá cuando esté listo.</b></span>
        </div>
        <div style={{ display:'flex', gap:9 }}>
          <button onClick={onClose} disabled={busy} style={{ flex:'none', minHeight:50, padding:'0 18px', borderRadius:12, background:'transparent', border:`1px solid ${T.g700}`, color:T.c300, fontFamily:T.sans, fontSize:15, fontWeight:600, cursor:busy?'default':'pointer' }}>Después</button>
          <button onClick={onConfirm} disabled={busy} style={{ flex:1, minHeight:50, borderRadius:12, background:busy?T.v800:T.v600, border:'none', color:'#fff', fontFamily:T.sans, fontSize:15.5, fontWeight:700, cursor:busy?'default':'pointer', display:'flex', alignItems:'center', justifyContent:'center', gap:9 }}>
            {busy && <span style={{ width:15, height:15, border:'2px solid rgba(255,255,255,.35)', borderTopColor:'#fff', borderRadius:99, animation:'tgspin .7s linear infinite', display:'block' }} />}
            {busy ? 'Avisándole a Mia…' : 'Aplicar ahora'}
          </button>
        </div>
      </div>
    </div>
  );
}

function Sheet({ job, onClose, onConfirm, busy }) {
  return (
    <div onClick={onClose} style={{ position:'absolute', inset:0, background:'rgba(8,7,6,.62)', display:'flex', alignItems:'flex-end', zIndex:20 }}>
      <div onClick={e => e.stopPropagation()} style={{ width:'100%', background:T.g900, borderTop:`1px solid ${T.g700}`, borderRadius:'20px 20px 0 0', padding:'18px 16px 16px', display:'flex', flexDirection:'column', gap:14, animation:'tgin 220ms ease-out' }}>
        <i style={{ width:38, height:4, borderRadius:9, background:T.g700, alignSelf:'center' }} />
        <div>
          <div style={{ fontFamily:T.display, fontSize:23, lineHeight:1.16, letterSpacing:'.4px', color:T.c100 }}>CERRAR EL TRABAJO</div>
          <div style={{ fontFamily:T.sans, fontSize:13.5, lineHeight:1.5, color:T.c500, marginTop:5 }}>Marco <b style={{ color:T.c100 }}>{job.svc}</b> como cobrado y le mando el recibo a {job.cli.split(' ')[0]} por WhatsApp.</div>
        </div>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'13px 14px', background:T.g950, border:`1px solid ${T.g800}`, borderRadius:12 }}>
          <Lbl>TOTAL</Lbl>
          <span style={{ fontFamily:T.mono, fontSize:19, fontWeight:500, color:T.c100, letterSpacing:'-.4px' }}>{money(job.monto)}</span>
        </div>
        <button onClick={onConfirm} disabled={busy} style={{ width:'100%', height:50, border:'none', borderRadius:10, background:busy?T.v800:T.v600, color:'#fff', fontFamily:T.sans, fontSize:16, fontWeight:600, cursor:busy?'default':'pointer', display:'flex', alignItems:'center', justifyContent:'center', gap:9 }}>
          {busy && <i style={{ width:15, height:15, border:'2px solid rgba(255,255,255,.35)', borderTopColor:'#fff', borderRadius:9, display:'block', animation:'tgspin .7s linear infinite' }} />}
          {busy ? 'Cobrando…' : `Confirmar ${money(job.monto)}`}
        </button>
      </div>
    </div>
  );
}

/* ── app ──────────────────────────────────────────────────── */
/* Set de datos alternativo: peluquería (patrón B). El cliente va al local,
   así que no hay dirección, no hay "en camino" y no hay presupuesto — se
   cobra el precio de lista. Sirve para ver que la app cambia de forma con el
   rubro y no sólo de rótulos. */
const CFG_LOCAL = { ...CFG_INIT, rubro:'Peluquería', patron:'B',
  presupuesto:{ on:false, items:false },
  domicilio:{ on:false, pct:0, zonas:[] },
  servicios:[{ n:'Corte y brushing', p:18000, m:45 }, { n:'Color raíz', p:32000, m:90 }, { n:'Alisado', p:54000, m:150 }] };
const JOBS_LOCAL = [
  { id:'2041', cli:'Paula Giménez', svc:'Corte y brushing', hora:'14:30', dia:'HOY', monto:18000, tsm:'CONFIRMADO', paso:1, hist:['mia','mia'], extra:'Vuelve cada 6 semanas', tel:'+54 9 341 402 118', donde:'En el local' },
  { id:'2040', cli:'Marina Ruiz', svc:'Color raíz', hora:'16:00', dia:'HOY', monto:32000, tsm:'CONFIRMADO', paso:1, hist:['mia','mia'], extra:'Primera vez', tel:'+54 9 341 588 907', donde:'En el local' },
  { id:'2039', cli:'Diego Salas', svc:'Corte y brushing', hora:'09:30', dia:'MAÑANA', monto:18000, tsm:'CREADO', paso:0, hist:['mia'], extra:'Esperando respuesta', tel:'+54 9 341 671 025', donde:'En el local' },
];

function MiniApp({ onb = false, ios = false, espera = false, local = false }) {
  const [tab, setTab] = React.useState(onb ? 'negocio' : 'hoy');
  const [esp, setEsp] = React.useState(espera);
  const [coach, setCoach] = React.useState(onb ? 0 : null);
  const [stack, setStack] = React.useState([]);
  const [jobs, setJobs] = React.useState((local ? JOBS_LOCAL : JOBS).map(j => ({ ...j, bit:BIT0[j.id] || [{ h:'08:00', t:'Trabajo creado', por:j.hist && j.hist[0] === 'mia' ? 'mia' : 'vos', tono:'alta' }] })));
  const [filtro, setFiltro] = React.useState('Todos');
  const [q, setQ] = React.useState('');
  const [sheet, setSheet] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [haptic, setHaptic] = React.useState(null);
  const [visto, setVisto] = React.useState([]);
  const [form, setForm] = React.useState({ cli:'', svc:'', monto:'', tipo:'Confirmado', dia:'HOY' });
  const [cfg, setCfg] = React.useState(local ? CFG_LOCAL : CFG_INIT);
  const [clientes, setClientes] = React.useState(CLIENTES_INIT);
  const [formCli, setFormCli] = React.useState({ n:'', tel:'', dir:'', notas:'' });
  const [seg, setSeg] = React.useState('Turnos');
  /* Guardar en una card deja el cambio pendiente; recién "Aplicar" lo manda a
     Mia. Son dos pasos distintos a propósito: el emprendedor puede tocar cinco
     cosas y aplicarlas juntas, en vez de reiniciar al bot cinco veces.     */
  const [pend, setPend] = React.useState([]);
  const [cfgSheet, setCfgSheet] = React.useState(false);
  /* El editor trabaja sobre un BORRADOR. Antes escribía directo en la config
     viva: si salías sin guardar el cambio quedaba aplicado igual, pero fuera
     de la lista de pendientes — así que la pantalla mostraba el valor nuevo
     y Mia seguía con el viejo, sin que el botón de aplicar se prendiera. */
  const [draft, setDraft] = React.useState(null);
  const [accSheet, setAccSheet] = React.useState(null);
  const [matePaso, setMatePaso] = React.useState(0);
  const [nuevoTurno, setNuevoTurno] = React.useState({ dia:'MAÑANA', hora:'15:00', motivo:'relevado' });
  const [revisita, setRevisita] = React.useState(false);
  const [docRango, setDocRango] = React.useState('Mes');
  const [docOff, setDocOff] = React.useState(0);
  const [docFiltro, setDocFiltro] = React.useState('Todos');
  const [rango, setRango] = React.useState('Hoy');
  const [agOff, setAgOff] = React.useState(0);
  const resumen = cfgResumen(cfg);
  const cards = cfgCards();
  const conPres = hayPresupuesto(cfg);
  /* UNA sola fuente: si el flujo no presupuesta, esos trabajos no existen para
     ninguna pantalla. Pasar un booleano a cada vista hacía que cada pantalla
     nueva fuera una fuga nueva — Hoy y el detalle se habían quedado afuera.
     `jobs` completo queda solo para resolver la pila por id.                */
  const visibles = conPres ? jobs : jobs.filter(j => j.dia !== 'PRESUPUESTO');
  /* El tour también sigue al flujo: si una card no aplica, su paso no existe. */
  const pasos = PASOS_ONB.filter(p => cards.some(c => c.k === p.k));
  const scroller = React.useRef(null);

  /* El aviso lleva TONO: verde confirma, coral avisa que algo falló o se
     canceló, violeta es info. Antes todo era la misma pastilla gris de 9 px,
     así que cobrar un trabajo y equivocarse de campo se veían igual.      */
  /* El aviso sale SOLO cuando pasó algo que el tipo tiene que saber: se cobró,
     se canceló, se reagendó, se mandó un presupuesto, se aplicó configuración.
     Navegar, tocar una pestaña o abrir una ficha dispara el haptic —que es una
     API real— pero no pinta nada: un cartel en cada click deja de leerse. */
  const buzz = (k, msg) => {
    if (!msg) return;
    const tono = /success/.test(k) ? 'ok' : /error|warning/.test(k) ? 'mal' : 'info';
    setHaptic({ k, msg, tono, id:Date.now() });
    window.clearTimeout(buzz._t);
    buzz._t = window.setTimeout(() => setHaptic(null), 2600);
  };
  const top = stack[stack.length - 1] || null;
  const push = s => { setStack(v => [...v, s]); buzz('impactOccurred · light'); if (scroller.current) scroller.current.scrollTop = 0; };
  const pop = () => { setDraft(null); setStack(v => v.slice(0, -1)); buzz('impactOccurred · light'); };
  const goTab = t => { setStack([]); setTab(t); buzz('selectionChanged'); if (scroller.current) scroller.current.scrollTop = 0; };

  /* Cada paso se rellena con SU dueño; sólo el que tocaste queda a tu nombre.
     Antes estampaba el mismo `por` en todos los huecos, así que saltar del
     paso 1 al 7 ponía VOS sobre "Presupuesto enviado" y "confirmado", que son
     de Mia — y contradecía el párrafo que está justo debajo. */
  /* `hist` es la verdad de qué pasó: un hueco significa "no se hizo". Saltar a
     "Terminé" no puede dar por hecho que hubo presupuesto elaborado, enviado y
     aprobado — antes los estampaba, y dos de ellos a nombre de Mia, sobre un
     trabajo sin ítems y con enviado en false. Se rellenan sólo con evidencia. */
  /* Toda transición pasa por acá: mueve el estado Y deja el evento. Antes el
     estado se pisaba y la reagenda no dejaba rastro. */
  const bitar = (j, ev) => anotar(j, ev);

  const marcar = (j, n, por) => { const p = pasosDe(cfg), h = [...(j.hist || [])];
    for (let i = 0; i <= n; i++) {
      if (h[i]) continue;
      if (i === n) { h[i] = por; continue; }
      /* Los pasos de presupuesto sólo se rellenan con evidencia; el resto sí. */
      const esPres = ['PRESUPUESTADO','ENVIADO','APROBADO'].includes(p[i]);
      if (!esPres || evidencia(j, p[i])) h[i] = (PASO_DEF[p[i]] || {}).por || 'vos';
    }
    return h.slice(0, n + 1); };

  const cerrar = () => { setBusy(true); window.setTimeout(() => {
    const ult = pasosDe(cfg).length - 1;
    setJobs(v => v.map(j => j.id === sheet.id ? bitar({ ...j, tsm:'FINALIZADO', paso:ult, hist:marcar(j, ult, 'vos'), extra:'Cobrado' }, { t:`Cobrado ${money(j.monto)}`, d:'Mia le mandó el recibo', por:'vos', tono:'ok', tipo:'cobro' }) : j));
    setBusy(false); setSheet(null);
    buzz('notificationOccurred · success', `Cobrado ${money(sheet.monto)} · recibo enviado`);
  }, 1150); };

  /* Avanzar el trabajo. Llegar a "Cobrado" abre la confirmación de cobro.
     Si el paso ES un estado del TSM, mueve el TSM: tenerlos separados hacía
     que tildar "En camino" en la escalera dejara el ticket en CONFIRMADO, y
     entonces el menú seguía ofreciendo las acciones de antes —sin "Me
     demoré"— aunque la pantalla dijera que ya ibas en camino. */
  const onPaso = (id, n) => {
    const j = jobs.find(x => x.id === id);
    if (!j || esCierre(j.tsm)) return;
    const k = pasosDe(cfg)[n], d = pasoDe(k, cfg.patron, j);
    if (d.auto) return;
    /* Retroceder: sólo pasos internos y sólo el último alcanzado, para que
       deshacer no borre media escalera de un toque. */
    if (j.paso === n && !EST_LBL[k]) {
      setJobs(v => v.map(x => x.id === id ? bitar({ ...x, paso:n - 1, hist:(x.hist||[]).slice(0, n) }, { t:'Deshecho: ' + d.t, por:'vos', tono:'vos' }) : x));
      buzz('impactOccurred · light');
      return;
    }
    if (n >= pasosDe(cfg).length - 1) { setSheet(j); buzz('impactOccurred · medium'); return; }
    if (EST_LBL[k] && k !== 'AGENDADO') { accionDe(j, k); return; }
    setJobs(v => v.map(x => x.id === id ? bitar({ ...x, paso:n, hist:marcar(x, n, 'vos') }, { t:d.t, por:'vos', tono:'vos' }) : x));
    buzz('selectionChanged');
  };

  const guardar = () => {
    const dePrecio = precioDe(cfg, form.svc);
    const monto = dePrecio != null ? dePrecio : (parseInt(String(form.monto).replace(/\D/g, ''), 10) || 0);
    const id = String(1043 + jobs.length - JOBS.length);
    const pres = form.tipo === 'Presupuesto';
    const nom = form.cli.trim();
    /* Si el cliente no estaba en la agenda, Mate lo da de alta con lo que
       juntó — igual que Mia cuando entra alguien nuevo por WhatsApp. */
    if (nom && !clientes.some(c => c.n === nom)) {
      /* Sin teléfono real la ficha dice "sin teléfono"; poner el placeholder
         como dato era mostrar un número falso. */
      setClientes(v => [{ n:nom, tel:form.tel || '', dir:form.donde || '', notas:'', v:0, ult:'—', total:0, tag:'Nuevo' }, ...v]);
    }
    setJobs(v => [{ bit:[{ h:hora(), t:pres ? 'Presupuesto creado con Mate' : 'Trabajo creado con Mate', d:`${form.svc.trim()} · ${money(monto)}`, por:'vos', tono:'alta' }], id, cli:nom, svc:form.svc.trim(), hora:pres?'—':(form.hora || '—'), dia:pres?'PRESUPUESTO':(form.dia || 'HOY'), monto,
      tsm:'CREADO', paso:0, enviado:false, hist:['vos'], emitido:'3 SEP',
      extra:pres?'Sin enviar':'Mia le confirma', tel:form.tel || '', donde:form.donde || 'En tu local' }, ...v]);
    setForm({ cli:'', svc:'', monto:'', tipo:'Confirmado', dia:'HOY' });
    setMatePaso(0);
    setStack(pres ? [{ type:'doc', id, tipo:'presupuesto' }] : v => v.slice(0, -1));
    if (pres) setTab('trabajos');
    buzz('notificationOccurred · success', pres ? 'Presupuesto armado, falta enviarlo' : `Turno cargado · Mia le confirma a ${nom.split(' ')[0]}`);
  };

  /* Una acción del TSM mueve el estado y dispara el WhatsApp al cliente. */
  /* Relevamiento: la visita terminó pero el trabajo no. Cierra el evento y
     abre el presupuesto — el estado que al TSM le falta. */
  const relevar = tipo => {
    const j = accSheet;
    /* El ticket NO se cierra: la visita terminó, el trabajo no. Queda en
       RELEVADO, fuera de la lista de cobrados y fuera de CIERRE. */
    setJobs(v => v.map(x => x.id === j.id ? { ...x, tsm:'RELEVADO', paso:3, hist:marcar(x, 3, 'vos'), extra:'Relevado · falta presupuestar' } : x));
    setAccSheet(null);
    const f = { cli:j.cli, svc:j.svc, monto:'', tipo:'Presupuesto', dia:'HOY', tel:j.tel, donde:j.donde };
    setForm(f);
    setMatePaso(primerPaso(pregsDe(cfg, 'Presupuesto', f), f));
    setRevisita(tipo === 'A_PRESUPUESTAR');
    setStack([{ type:'nuevo' }]);
    buzz('notificationOccurred · success', 'Visita cerrada · ahora el presupuesto');
  };

  const accionDe = (j, est) => {
    const mal = est === 'CANCELADO' || est === 'AUSENTE';
    /* Si el estado vive en la escalera, se tilda también ahí: mover el TSM y
       dejar el check sin marcar era la otra mitad del mismo choque. */
    /* El peldaño es el del estado, y nada más. Usar esCobrado acá promovía
       FINALIZADO al último peldaño —COBRADO— y daba por cobrada plata que
       nadie cobró: la escalera separa "trabajo hecho" de "plata adentro". */
    const p = pasosDe(cfg), n = p.indexOf(est);
    const limpiaEspera = est === 'CONFIRMADO' ? { esperaCli:false } : null;
    setJobs(v => v.map(x => x.id === j.id ? bitar({ ...x, tsm:est,
      paso:n >= 0 ? Math.max(x.paso, n) : x.paso,
      hist:n >= 0 ? marcar(x, n, 'vos') : x.hist,
      ...(limpiaEspera || {}), extra:EST_LBL[est] }, { t:EST_LBL[est], d:`Mia le avisó a ${j.cli.split(' ')[0]}`, por:'vos', tono:mal ? 'mal' : est === 'DEMORADO' ? 'cambio' : 'vos', tipo:est === 'DEMORADO' ? 'demora' : est === 'CANCELADO' ? 'cancela' : null }) : x));
    setAccSheet(null);
    buzz(mal ? 'notificationOccurred · warning' : 'notificationOccurred · success', `${EST_LBL[est]} · Mia le avisó a ${j.cli.split(' ')[0]}`);
  };
  const accion = est => accionDe(accSheet, est);

  const guardarCli = () => {
    setClientes(v => [{ n:formCli.n.trim(), tel:formCli.tel.trim(), dir:formCli.dir.trim(), notas:formCli.notas.trim(), v:0, ult:'—', total:0, tag:'Nuevo' }, ...v]);
    setFormCli({ n:'', tel:'', dir:'', notas:'' });
    setQ('');
    setStack(v => v.slice(0, -1));
    buzz('notificationOccurred · success', `${formCli.n.trim()} quedó en tu agenda`);
  };

  const enviar = id => {
    /* Enviar tilda el paso de Mia: es de los que pasan, no de los que tocás. */
    setJobs(v => v.map(x => { if (x.id !== id) return x;
      const p = pasosDe(cfg), n = p.indexOf('ENVIADO'), h = [...(x.hist || [])];
      if (n >= 0) { if (!h[n-1]) h[n-1] = 'vos'; h[n] = 'mia'; }
      return bitar({ ...x, enviado:true, extra:'Enviado hoy', hist:h, paso:Math.max(x.paso, n) }, { t:'Presupuesto enviado', d:`Mia se lo mandó a ${x.cli.split(' ')[0]}`, por:'mia', tono:'mia' }); }));
    buzz('notificationOccurred · success', 'Presupuesto enviado por WhatsApp');
  };

  /* chrome + MainButton por pantalla. La pila guarda IDENTIFICADORES, no copias
     del trabajo: el detalle siempre lee el estado vivo. */
  const nuevoBtn = { label:'Cargar un trabajo', on:() => { setForm({ cli:'', svc:'', monto:'', tipo:'Confirmado', dia:'HOY' }); setMatePaso(0); push({ type:'nuevo' }); } };
  let title = 'Mi.Asistente', sub = 'jueves 3 de septiembre', body, main = null;
  if (!top) {
    if (tab === 'hoy')      { body = <Hoy open={j => push({ type:'job', id:j.id })} jobs={visibles} cfg={cfg} visto={visto} onPaso={onPaso} verMate={() => push({ type:'mate' })} onAlerta={a => {
      if (a.acc === 'visto') { setVisto(v => [...v, a.id]); buzz('impactOccurred · light'); }
      else if (a.acc === 'pasar') { setNuevoTurno({ dia:'MAÑANA', hora:'15:00', motivo:'cliente' }); push({ type:'pasar', id:a.job }); buzz('impactOccurred · medium'); }
      else if (a.acc === 'enviar') { push({ type:'doc', id:a.job, tipo:'presupuesto' }); buzz('impactOccurred · medium'); }
    }} />; main = nuevoBtn; }
    if (tab === 'trabajos') {
      const turnos = visibles.filter(j => conHora(j) && !esCierre(j.tsm));
      const pres = visibles.filter(j => !conHora(j) && !esCierre(j.tsm));
      title = 'Agenda';
      const segAct = conPres ? seg : 'Turnos';
      sub = segAct === 'Presupuestos' ? `${pres.length} esperando respuesta` : `${turnos.length} turnos por delante`;
      body = <Agenda jobs={visibles} cfg={cfg} open={j => push({ type:'job', id:j.id })} conPresupuesto={conPres} seg={segAct} setSeg={s => { setSeg(s); buzz('selectionChanged'); }} rango={rango} setRango={r => { setRango(r); buzz('selectionChanged'); }} off={agOff} setOff={setAgOff} />;
      main = segAct === 'Presupuestos'
        ? { label:'Armar un presupuesto', on:() => { setForm({ cli:'', svc:'', monto:'', tipo:'Presupuesto', dia:'HOY' }); setMatePaso(0); push({ type:'nuevo' }); } }
        : nuevoBtn;
    }
    if (tab === 'clientes') { title = 'Clientes'; sub = `${clientes.length} en la agenda`; body = <Clientes open={c => push({ type:'cli', n:c.n })} q={q} setQ={setQ} clientes={clientes} jobs={visibles} nuevo={n => { setFormCli({ n:n.trim(), tel:'', dir:'', notas:'' }); push({ type:'nuevoCli' }); }} />; main = { label:'Cargar un cliente', on:() => { setFormCli({ n:'', tel:'', dir:'', notas:'' }); push({ type:'nuevoCli' }); } }; }
    if (tab === 'negocio')  { title = 'Negocio'; sub = pend.length ? `${pend.length} ${pend.length === 1 ? 'cambio sin aplicar' : 'cambios sin aplicar'}` : 'septiembre 2026'; body = <Negocio abrirCfg={c => push({ type:'cfg', k:c.k })} destacado={coach !== null ? (pasos[coach]||{}).k : null} cfg={cfg} cards={cards} pend={pend} conPresupuesto={conPres} resumen={resumen} jobs={visibles} verDocs={() => push({ type:'docs' })} reVerGuia={() => { setCoach(0); buzz('impactOccurred · light'); }} />;
      main = pend.length
        ? { label:`Aplicar ${pend.length} ${pend.length === 1 ? 'cambio' : 'cambios'} en Mia`, on:() => { setCfgSheet(true); buzz('impactOccurred · medium'); } }
        : { label:'Mia está al día con tu configuración', on:() => buzz('sin cambios pendientes'), tone:'off' };
    }
  } else if (top.type === 'job') {
    const job = jobs.find(j => j.id === top.id);
    const ultP = pasosDe(cfg).length - 1;
    const cobrado = !!(job.hist || [])[ultP];
    const hecho = esCierre(job.tsm) && job.tsm !== 'CANCELADO';
    const done = job.tsm === 'CANCELADO' || cobrado;
    const pres = job.dia === 'PRESUPUESTO';
    title = job.svc; sub = `#${job.id} · ${job.cli}`;
    body = <Detalle job={job} cfg={cfg} onPaso={onPaso} onDoc={t => push(t === 'items' ? { type:'items', id:job.id } : { type:'doc', id:job.id, tipo:t })} onNotas={() => push({ type:'notas', id:job.id })} onAcc={() => setAccSheet(job)} />;
    /* La acción principal sale del estado, no de una cadena de ifs inventada:
       un presupuesto sin enviar se manda, uno enviado se recuerda, y un turno
       vivo abre la hoja con las acciones REALES de su patrón. */
    main = hecho && !cobrado
      ? { label:'Cobrar ' + money(job.monto), on:() => { setSheet(job); buzz('impactOccurred · medium'); } }
      : done
      ? (job.tsm === 'CANCELADO'
          /* Un trabajo cancelado no se vuelve a cotizar: se vuelve a agendar.
             El presupuesto, si lo había, sigue siendo válido. */
          ? { label:'Volver a agendar con ' + job.cli.split(' ')[0], on:() => { const f = { cli:job.cli, svc:job.svc, monto:String(job.monto || ''), tipo:'Confirmado', dia:'HOY', tel:job.tel, donde:job.donde }; setForm(f); setMatePaso(primerPaso(pregsDe(cfg, f.tipo, f), f)); push({ type:'nuevo' }); } }
          : { label:'Ver el recibo', on:() => push({ type:'doc', id:job.id, tipo:'recibo' }) })
      : pres
        ? (job.enviado
            ? { label:'Insistirle a ' + job.cli.split(' ')[0], on:() => buzz('notificationOccurred · success') }
            : { label:'Mandar el presupuesto', on:() => enviar(job.id) })
        /* El botón dice lo que hace: cada opción de la hoja manda un WhatsApp.
           "¿Qué pasó con este trabajo?" sonaba a encuesta. */
        : { label:`Avisarle algo a ${job.cli.split(' ')[0]}`, on:() => { setAccSheet(job); buzz('impactOccurred · medium'); } };
  } else if (top.type === 'cli') {
    const cli = clientes.find(c => c.n === top.n);
    const r = clienteResumen(cli, visibles);
    title = cli.n; sub = r.visitas ? `${r.visitas} ${r.visitas === 1 ? 'visita' : 'visitas'}` : 'sin visitas todavía';
    body = <ClienteDet cli={cli} jobs={visibles} abrir={j => push({ type:'job', id:j.id })} verHist={() => push({ type:'hist', n:cli.n })} onMapa={() => buzz('openLink')} />;
    main = { label:'Cargarle un trabajo', on:() => { const f = { cli:cli.n, svc:'', monto:'', dia:'HOY', tipo:'Confirmado', tel:cli.tel, donde:cli.dir }; setForm(f); setMatePaso(primerPaso(pregsDe(cfg, f.tipo, f), f)); push({ type:'nuevo' }); } };
  } else if (top.type === 'hist') {
    const cli = clientes.find(c => c.n === top.n);
    const r = clienteResumen(cli, visibles);
    title = 'Historial'; sub = `${cli.n} · ${money(r.facturado)} en total`;
    body = <Historial cli={cli} hist={r.hist} jobs={visibles} abrirDoc={(id, tipo, real) => real ? push({ type:'doc', id, tipo }) : buzz('abre el ' + tipo + ' #' + id)} />;
    main = { label:`Cargarle un trabajo`, on:() => { const f = { cli:cli.n, svc:'', monto:'', dia:'HOY', tipo:'Confirmado', tel:cli.tel, donde:cli.dir }; setForm(f); setMatePaso(primerPaso(pregsDe(cfg, f.tipo, f), f)); push({ type:'nuevo' }); } };
  } else if (top.type === 'cfg') {
    const d = draft || cfg;
    const sinGuardar = draft && JSON.stringify(draft) !== JSON.stringify(cfg);
    title = top.k; sub = sinGuardar ? 'sin guardar' : cfgResumen(d)[top.k];
    body = <CfgEditor k={top.k} cfg={d} set={fn => setDraft(x => fn(x || cfg))} />;
    main = { label:sinGuardar ? 'Guardar' : 'Listo', tone:sinGuardar ? 'primary' : 'off', cierra:true, on:() => {
      if (sinGuardar) { setCfg({ ...draft, tocado:draft.tocado.includes(top.k) ? draft.tocado : [...draft.tocado, top.k] });
        setPend(v => v.includes(top.k) ? v : [...v, top.k]); buzz('notificationOccurred · success', 'Guardado · falta aplicarlo en Mia'); }
      setDraft(null); pop();
    } };
  } else if (top.type === 'doc') {
    const job = jobs.find(j => j.id === top.id);
    const recibo = top.tipo === 'recibo';
    title = recibo ? 'Recibo' : 'Presupuesto';
    sub = `#${job.id} · ${job.cli}`;
    body = <DocView job={job} tipo={top.tipo} cfg={cfg} editar={cfg.presupuesto.items ? () => push({ type:'items', id:job.id }) : null} />;
    main = recibo
      ? { label:'Mandárselo otra vez', on:() => buzz('notificationOccurred · success', 'Recibo reenviado') }
      : job.enviado
        ? { label:'Insistirle a ' + job.cli.split(' ')[0], on:() => buzz('notificationOccurred · success', `Mia le escribió a ${job.cli.split(' ')[0]}`) }
        : { label:'Mandar el presupuesto', on:() => enviar(job.id) };
  } else if (top.type === 'notas') {
    const job = jobs.find(j => j.id === top.id);
    const n = (job.notas || []).length;
    title = 'Anotaciones'; sub = `#${job.id} · ${n ? n + (n === 1 ? ' nota' : ' notas') : 'sin notas'}`;
    body = <Notas job={job} onAdd={t => setJobs(v => v.map(x => x.id === job.id ? { ...x, notas:[...(x.notas||[]), { t, cuando:'HOY · AHORA' }] } : x))}
      onFoto={() => { setJobs(v => v.map(x => x.id === job.id ? { ...x, notas:[...(x.notas||[]), { t:'Foto del lugar', foto:true, tono:(x.notas||[]).length % 3, cuando:'HOY · AHORA' }] } : x)); buzz('notificationOccurred · success', 'Foto guardada en el trabajo'); }} />;
    main = { label:'Listo', cierra:true, on:() => { pop(); buzz('notificationOccurred · success'); } };
  } else if (top.type === 'pasar') {
    const job = jobs.find(j => j.id === top.id);
    title = `Reagendar ${(VOZ[cfg.patron] || VOZ.C).ev}`; sub = `#${job.id} · ${job.cli}`;
    body = <Pasar job={job} sel={nuevoTurno} setSel={setNuevoTurno} patron={cfg.patron} />;
    const motR = MOTIVOS.find(x => x.k === nuevoTurno.motivo) || MOTIVOS[0];
    const cuandoR = `${nuevoTurno.dia === 'SAB' ? 'el sábado' : nuevoTurno.dia === 'HOY' ? 'hoy' : 'mañana'} ${nuevoTurno.hora}`;
    main = { label:motR.mantiene ? `Vuelvo ${cuandoR}` : `Propónele ${cuandoR}`, on:() => {
      /* La escalera vuelve a "sin confirmar" porque el turno nuevo hay que
         confirmarlo de nuevo — pero la bitácora conserva que ya había estado
         confirmado y en camino. Ese ida y vuelta es el dato que antes se
         perdía y que el reporte necesita. */
      const mot = MOTIVOS.find(x => x.k === nuevoTurno.motivo) || MOTIVOS[0];
      setJobs(v => v.map(x => x.id === job.id ? bitar({ ...x, dia:nuevoTurno.dia, hora:nuevoTurno.hora, tsm:'CREADO', ...trasReagenda(x, pasosDe(cfg), mot.k), extra:mot.mantiene ? mot.t : 'Reprogramado · sin confirmar' },
        { t:'Reagendado', d:`${mot.t} · de ${x.dia} ${x.hora} a ${nuevoTurno.dia === 'SAB' ? 'sábado' : nuevoTurno.dia === 'HOY' ? 'hoy' : 'mañana'} ${nuevoTurno.hora}`, por:'vos', tono:'cambio', tipo:'reagenda' }) : x));
      setVisto(v => v.includes('a1') ? v : [...v, 'a1']);
      pop(); buzz('notificationOccurred · success', `Mia le propone ${nuevoTurno.hora} a ${job.cli.split(' ')[0]}`);
    } };
  } else if (top.type === 'mate') {
    title = 'Mate'; sub = 'lo que te avisa por Telegram';
    body = <MateHilo jobs={jobs} cfg={cfg} />;
    main = { label:'Listo', cierra:true, on:() => pop() };
  } else if (top.type === 'docs') {
    title = 'Documentos'; sub = 'presupuestos y recibos';
    body = <Docs jobs={jobs} rango={docRango} setRango={r => { setDocRango(r); buzz('selectionChanged'); }} off={docOff} setOff={setDocOff} filtro={docFiltro} setFiltro={f => { setDocFiltro(f); buzz('selectionChanged'); }} abrir={(j,t) => push({ type:'doc', id:j.id, tipo:t })} />;
    main = conPres
      ? { label:'Armar un presupuesto', on:() => { setForm({ cli:'', svc:'', monto:'', tipo:'Presupuesto', dia:'HOY' }); setMatePaso(0); push({ type:'nuevo' }); } }
      : null;
  } else if (top.type === 'items') {
    const job = jobs.find(j => j.id === top.id);
    const its = job.items || [];
    title = 'Ítems del presupuesto'; sub = `${its.length} · ${money(totalItems(its))}`;
    body = <ItemsEditor items={its} set={n => setJobs(v => v.map(x => x.id === job.id ? { ...x, items:n, monto:totalItems(n) } : x))}
      ticket={job.ticket} setTicket={t => setJobs(v => v.map(x => x.id === job.id ? { ...x, ticket:t } : x))} />;
    main = { label:'Listo', cierra:true, on:() => { pop(); buzz('notificationOccurred · success'); } };
  } else if (top.type === 'nuevoCli') {
    const listo = formCli.n.trim() && formCli.tel.trim();
    title = 'Nuevo cliente'; sub = formCli.n.trim() || 'sin nombre todavía';
    body = <NuevoCli form={formCli} setForm={setFormCli} />;
    main = { label:listo ? 'Guardar el cliente' : 'Completá los dos campos', on:() => listo ? guardarCli() : buzz('notificationOccurred · error', 'Faltan el nombre y el teléfono'), tone:listo ? 'primary' : 'off' };
  } else {
    const pregs = pregsDe(cfg, form.tipo, form);
    const fin = matePaso >= pregs.length;
    title = form.tipo === 'Presupuesto' ? 'Nuevo presupuesto' : 'Nuevo trabajo';
    sub = fin ? 'listo para guardar' : `con Mate · ${matePaso + 1} de ${pregs.length}`;
    body = <MateCarga cfg={cfg} form={form} setForm={setForm} paso={matePaso} clientes={clientes} responder={(k,v,p,c) => {
      /* Al elegir un cliente de la agenda se heredan su teléfono y su
         dirección, así que el paso siguiente se RECALCULA sobre el formulario
         nuevo: avanzar de a uno volvía a preguntar "¿dónde es?" a un cliente
         que ya tenía dirección cargada. */
      const f2 = { ...form, [k]:v, ...(p ? { monto:String(p) } : {}), ...(c ? { tel:c.tel, donde:c.dir || form.donde } : {}) };
      setForm(f2);
      setMatePaso(primerPaso(pregsDe(cfg, f2.tipo, f2), f2));
      buzz('selectionChanged');
    }} />;
    main = fin
      ? { label:form.tipo === 'Presupuesto' ? 'Armar el presupuesto' : 'Guardar el trabajo', on:guardar }
      : { label:'Contestá lo que te pregunta Mate', on:() => buzz('notificationOccurred · error', 'Contestale a Mate para seguir'), tone:'off' };
  }

  if (esp && !top) {
    title = 'Mi.Asistente'; sub = 'configuración terminada';
    body = <Espera cfg={cfg} onVer={() => { setEsp(false); setTab('hoy'); buzz('impactOccurred · light'); }} />;
    main = { label:'Ver el panel mientras espero', on:() => { setEsp(false); setTab('hoy'); buzz('impactOccurred · light'); } };
  }

  /* Durante el onboarding el MainButton ES el "Siguiente" del tour: una sola
     acción principal por pantalla, también mientras se enseña. */
  if (coach !== null) {
    const ultimo = coach >= pasos.length - 1;
    main = { label: ultimo ? 'Listo ✓' : 'Siguiente →', on:() => {
      if (ultimo) { setCoach(null); buzz('notificationOccurred · success'); }
      else { setCoach(coach + 1); buzz('impactOccurred · light'); }
    } };
  }

  /* IOSDevice dibuja su status bar en position:absolute (no ocupa lugar en el
     flujo) porque espera que la reserven el title/IOSNavBar, que no usamos.
     AndroidDevice en cambio la pone en flujo. Reservamos los 62 px acá. */
  return (
    <div style={{ height:'100%', paddingTop: ios ? 62 : 0, display:'flex', flexDirection:'column', background:T.g950, position:'relative', overflow:'hidden' }}>
      <TgChrome ios={ios} back={top ? pop : null} title={title} sub={sub} nota={() => buzz('chrome de Telegram')} />
      {!esp && espera && !top && <BannerEspera onVer={() => { setEsp(true); buzz('impactOccurred · light'); }} />}
      <div ref={scroller} style={{ flex:1, overflowY:'auto', padding:'16px 16px 22px', scrollbarWidth:'none' }}>{body}</div>
      {main && <MainButton label={main.label} onClick={main.on} tone={main.tone} busy={busy} ios={ios} lift={coach !== null} cerrar={top && !main.cierra ? pop : null} />}
      {!top && !esp && coach === null && <TabBar tab={tab} go={goTab} />}
      {sheet && <Sheet job={sheet} onClose={() => !busy && setSheet(null)} onConfirm={cerrar} busy={busy} />}
      {cfgSheet && <SheetCfg pend={pend} resumen={resumen} busy={busy} onClose={() => setCfgSheet(false)} onConfirm={() => { setBusy(true); window.setTimeout(() => { setBusy(false); setCfgSheet(false); setPend([]); buzz('notificationOccurred · success', 'Mia ya tiene la configuración nueva'); }, 1250); }} />}
      {accSheet && <SheetAcc job={accSheet} patron={cfg.patron} onClose={() => setAccSheet(null)} onAccion={accion} onRelevar={relevar} onPasar={() => { const j = accSheet; setAccSheet(null); setNuevoTurno({ dia:j.dia === 'HOY' ? 'MAÑANA' : 'SAB', hora:'15:00' }); push({ type:'pasar', id:j.id }); }} />}
      {coach !== null && <Coach pasos={pasos} i={Math.min(coach, pasos.length - 1)} total={pasos.length} inset={ios ? 81 : 69} onSkip={() => { setCoach(null); buzz('impactOccurred · light'); }} onBack={() => { setCoach(coach - 1); buzz('impactOccurred · light'); }} />}
      {haptic && <Toast key={haptic.id} {...haptic} ios={ios} />}
    </div>
  );
}
Object.assign(window, { MiniApp });
