/* Panel web navegable · Pieza 3a
   Antes era una maqueta de UNA sección para mostrar el tour. Ahora las siete
   vistas del panel real son navegables y comen de los MISMOS datos que la
   mini-app: mismo JOBS, mismo CFG_INIT, mismas funciones derivadas. Si algo
   cambia en el modelo, cambia en las tres superficies a la vez.

   Lo que cambia en escritorio no es el contenido, es la densidad: hay lugar
   para ver la semana entera y la tabla de clientes con sus columnas, que en
   390 px había que partir en pantallas.
   Los cuatro colores del tour NO son de tokens.css — se marcan a propósito. */

const TOUR_OFF = { bg:'#1A1714', ink:'#F5F3EF', mut:'#C9C4BC', accent:'#8B84FF' };

const NAV_WEB = [
  { id:'inicio', t:'Inicio' }, { id:'agenda', t:'Agenda' }, { id:'tickets', t:'Documentos' },
  { id:'clientes', t:'Clientes' }, { id:'reportes', t:'Reportes' },
  { id:'resenas', t:'Reseñas' }, { id:'configuracion', t:'Configuración' },
];
/* Iconos levantados verbatim de public/panel.html (nav, líneas 1433-1462).
   Dos necesitan hijos que no son <path> — el rect de la agenda y los círculos
   de clientes y del engranaje— así que el mapa guarda elementos, no un solo
   string de path. Redibujarlos "parecido" es exactamente lo que hace que el
   panel real no se parezca a la maqueta.                                   */
const NAV_D = {
  inicio:        [['path',{ d:'M3 11l9-8 9 8v9a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z' }]],
  agenda:        [['rect',{ x:3, y:5, width:18, height:16, rx:2 }], ['path',{ d:'M3 10h18M8 3v4M16 3v4' }]],
  tickets:       [['path',{ d:'M3 8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3a2 2 0 0 0 0-4z' }], ['path',{ d:'M10 6v12' }]],
  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' }]],
  reportes:      [['path',{ d:'M4 20V10M10 20V4M16 20v-7M22 20H2' }]],
  resenas:       [['path',{ d:'M12 3l2.6 5.4 5.9.8-4.3 4.1 1.1 5.8L12 16.5 6.7 19.1l1.1-5.8L3.5 9.2l5.9-.8L12 3z' }]],
  configuracion: [['circle',{ cx:12, cy:12, r:3 }], ['path',{ d:'M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z' }]],
};
const NavIcon = ({ id, color }) => (
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" style={{ flex:'none' }}>
    {(NAV_D[id] || []).map(([tag, at], i) => React.createElement(tag, { key:i, ...at }))}
  </svg>
);

/* Vocabulario levantado de panel.html, no inventado:
     .card       graphite-900 · borde graphite-700 · radio 20 · padding 28 28 26
                 sombra 0 6px 20px rgba(0,0,0,.22)
     .card-title sans 700 17px con la flecha ↳ en violeta, margen inferior 24
     .grid-primary 320px + 1fr, gap 18 · .grid-secondary 1fr 1fr
   El panel viejo de esta pieza usaba radio 14 y padding 16: se veía como otra
   app. Estos son los números de producción.                                */
const W = {
  card:{ background:T.g900, border:`1px solid ${T.g700}`, borderRadius:20, padding:'26px 26px 24px', boxShadow:'0 6px 20px rgba(0,0,0,.22)', display:'flex', flexDirection:'column', minWidth:0 },
  lbl:{ fontFamily:T.mono, fontSize:9, letterSpacing:'.13em', textTransform:'uppercase', color:T.c500 },
  h:{ fontFamily:T.display, fontSize:22, lineHeight:1.16, letterSpacing:'.4px', color:T.c100, fontWeight:400 },
  td:{ fontFamily:T.sans, fontSize:13, color:T.c300, padding:'12px 16px', borderTop:`1px solid ${T.g800}` },
  th:{ fontFamily:T.mono, fontSize:8.5, letterSpacing:'.13em', textTransform:'uppercase', color:T.c500, padding:'0 16px 10px', textAlign:'left', fontWeight:400 },
  g1:{ display:'grid', gridTemplateColumns:'340px minmax(0,1fr)', gap:18, alignItems:'start' },
  g2:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:18, alignItems:'start' },
};
/* Grillas y piezas de panel.html, leídas —no estimadas—:
     .clientes-grid  repeat(3,1fr) gap 14 · .client-card radio 14 pad 18 20
                     con hover: borde + translateY(-2px) + sombra
     .rep-kpis       repeat(4,1fr) gap 14 · .rep-row 1.4fr 1fr gap 14
     .rep-card       radio 18 pad 24 26 · .rep-layer-label mono 10 tracking .12em
     .res-summary    flex gap 28 radio 18 pad 22 28 · .res-avg .n mono 36
                     .res-stat con borde izquierdo y padding 22
     .res-grid       repeat(2,1fr) gap 14 · .res-card radio 14 pad 20 22
     .cfg-grid       1fr 1fr gap 14 · .toolbar flex gap 10 · .pill 7 14 radio 999
   Casi todas usan gap 14, no 18: sólo Inicio va a 18.                    */
const G = {
  clientes:{ display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:14, alignItems:'start' },
  repKpis:{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:12 },
  repRow:{ display:'grid', gridTemplateColumns:'1.4fr 1fr', gap:14, alignItems:'stretch' },
  repCard:{ background:T.g900, border:`1px solid ${T.g700}`, borderRadius:18, padding:'24px 26px', display:'flex', flexDirection:'column', minWidth:0 },
  resGrid:{ display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:14 },
  resCard:{ background:T.g900, border:`1px solid ${T.g700}`, borderRadius:14, padding:'20px 22px', display:'flex', flexDirection:'column', gap:8 },
  cliCard:{ background:T.g900, border:`1px solid ${T.g700}`, borderRadius:14, padding:'18px 20px', display:'flex', flexDirection:'column', gap:14, cursor:'pointer', textAlign:'left', transition:'border-color .15s ease, transform .15s ease, box-shadow .15s ease' },
  cfg:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14, alignContent:'start' },
  toolbar:{ display:'flex', alignItems:'center', gap:10, flexShrink:0 },
};
/* .bars-row verbatim de panel.html:910-939 — grilla 180px 1fr 50px, gap 14,
   pista de 10 px en grafito-700 y relleno con degradé #6C63FF → #4036B5.
   Estaba redibujado a mano con barras de 8 px y color plano.             */
const BarRow = ({ label, v, max, col, primera }) => (
  <div style={{ display:'grid', gridTemplateColumns:'180px 1fr 50px', alignItems:'center', gap:14, padding:'8px 0', borderTop:primera?'none':`1px solid ${T.g700}` }}>
    <span style={{ fontFamily:T.sans, fontSize:13, color:T.c100, minWidth:0, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{label}</span>
    <span style={{ height:10, background:T.g700, borderRadius:5, overflow:'hidden', display:'block' }}>
      <i style={{ display:'block', height:'100%', width:`${Math.max(v ? 3 : 0, v / max * 100)}%`, background:col || 'linear-gradient(90deg,#6C63FF 0%,#4036B5 100%)', borderRadius:5 }} />
    </span>
    <span style={{ fontFamily:T.mono, fontSize:12, color:T.c300, textAlign:'right' }}>{money(v)}</span>
  </div>
);

const Capa = ({ children }) => <div style={{ fontFamily:T.mono, fontSize:10, letterSpacing:'.12em', textTransform:'uppercase', color:T.c500, marginTop:4 }}>{children}</div>;
/* El Pill activo tenía el rótulo en violeta-600 sobre violeta-soft: 3.86:1 a
   12px. El color de estado vive en el borde y el fondo; el texto en cloud. */
const Pill = ({ on, children, click }) => (
  <button onClick={click} style={{ display:'inline-flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:999, fontFamily:T.sans, fontWeight:on?600:500, fontSize:12,
    color:on?T.c100:T.c300, background:on?T.vSoft:'transparent', border:`1px solid ${on?T.vBorder:T.g700}`, cursor:'pointer' }}>{children}</button>
);

/* El título con ↳ es la firma del panel: aparece en las 12 cards de producción
   y es lo que hace que se lea como una sola pieza y no como widgets sueltos. */
const CardT = ({ children, extra }) => (
  <h2 style={{ display:'flex', alignItems:'center', gap:8, margin:'0 0 20px', fontFamily:T.sans, fontWeight:700, fontSize:17, color:T.c100, letterSpacing:'-.2px' }}>
    <span style={{ color:T.v600, fontWeight:700 }}>↳</span>{children}
    {extra && <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:11, letterSpacing:'.04em', color:T.c500, fontWeight:400 }}>{extra}</span>}
  </h2>
);
const WBadge = ({ tsm }) => { const t = EST_TONO[tsm];
  const c = t === 'coral' ? T.coral : t === 'amber' ? T.amber : t === 'green' ? T.green : T.c500;
  const bg = t === 'coral' ? T.coSoft : t === 'amber' ? T.aSoft : t === 'green' ? T.gSoft : T.g800;
  const bd = t === 'coral' ? T.coBorder : t === 'amber' ? T.aBorder : t === 'green' ? T.gBorder : T.g700;
  return <span style={{ display:'inline-flex', fontFamily:T.mono, fontSize:8.5, letterSpacing:'.1em', textTransform:'uppercase', color:c, background:bg, border:`1px solid ${bd}`, borderRadius:4, padding:'3px 7px', whiteSpace:'nowrap' }}>{EST_LBL[tsm] || tsm}</span>;
};
/* .kpi verbatim de panel.html:661-680 — radio 14, padding 18 20, valor mono
   500 a 28px con line-height 1 y tracking -0.4, label sans 12 cloud-300 a 8px.
   NO lleva barra de acento: el color vive en el número (.kpi.green/.violet/
   .coral .v). Antes esta pieza inventaba una barra de 2px que el panel no
   tiene, y el resto de las cards son radio 20 — el KPI es la excepción.   */
const KPI = ({ k, v, col }) => (
  <div style={{ background:T.g900, border:`1px solid ${T.g700}`, borderRadius:14, padding:'18px 20px' }}>
    <div style={{ fontFamily:T.mono, fontWeight:500, fontSize:28, color:col || T.c100, lineHeight:1, letterSpacing:'-.4px' }}>{v}</div>
    <div style={{ fontFamily:T.sans, fontSize:12, color:T.c300, marginTop:8 }}>{k}</div>
  </div>
);

/* .thead / .trow verbatim de panel.html:759-785 — no es un <table>, es una
   grilla de filas con columnas declaradas. Header mono 10px con tracking .14em
   y padding 16 22; fila sans 13 con padding 14 22 y borde abajo. El id va en
   violeta mono 500, el nombre en cloud-100 600, la hora en mono 11 cloud-500. */
const TRow = ({ cols, children, head, on }) => (
  <div onClick={on} style={{ cursor:on?'pointer':'default', display:'grid', gridTemplateColumns:cols, columnGap:14, alignItems:head?'baseline':'center',
    padding:head ? '16px 22px' : '14px 22px', borderBottom:`1px solid ${T.g700}`,
    fontFamily:head ? T.mono : T.sans, fontSize:head ? 10 : 13,
    letterSpacing:head ? '.14em' : 0, textTransform:head ? 'uppercase' : 'none', color:head ? T.c500 : T.c300 }}>{children}</div>
);

/* ── vistas ───────────────────────────────────────────────── */
function WInicio({ jobs, cfg, abrir, visto, onAlerta, rs }) {
  const abiertos = jobs.filter(j => !esCierre(j.tsm));
  const hoy = jobs.filter(j => j.dia === 'HOY' && conHora(j));
  /* A cobrar = trabajo aceptado y con fecha. Un presupuesto que el cliente no
     contestó NO es plata por cobrar — mezclarlos infla el número y hace que
     esta vista se contradiga con Reportes. */
  /* Esta pantalla está scopeada a HOY —lo dice el subtítulo y la card de al
     lado— así que el KPI también. Sumar toda la semana acá daba $126.500
     contra los $70.500 que muestra el teléfono para el mismo día. */
  const pend = hoy.filter(j => !esCierre(j.tsm) && j.tsm !== 'RELEVADO').reduce((s,j) => s + j.monto, 0);
  const pendSem = abiertos.filter(j => conHora(j) && j.tsm !== 'RELEVADO').reduce((s,j) => s + j.monto, 0);
  const enPie = abiertos.filter(j => !conHora(j)).reduce((s,j) => s + j.monto, 0);
  const cobrado = jobs.filter(j => esCobrado(j.tsm)).reduce((s,j) => s + j.monto, 0);
  /* Mismo resolver que la mini-app: rótulo y texto del trabajo vivo. */
  const alertas = alertasDe(jobs, visto);
  const TONO_A = { coral:[T.coral,T.coSoft,T.coBorder], amber:[T.amber,T.aSoft,T.aBorder], green:[T.green,T.gSoft,T.gBorder] };
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:18 }}>
      <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:18 }}>
        <KPI k={hoy.length ? 'trabajos hoy · próximo ' + hoy[0].hora : 'trabajos hoy'} v={hoy.length} />
        <KPI k={`a cobrar hoy · ${money(pendSem)} en la semana`} v={money(pend)} col={T.v600} />
        <KPI k="en presupuestos · esperando" v={money(enPie)} col={T.amber} />
        <KPI k="cobrado este mes" v={money(cobrado)} col={T.green} />
      </div>

      <div style={W.card}>
        <CardT extra={`${alertas.length} ${alertas.length === 1 ? 'alerta' : 'alertas'} del asistente`}>Atención requerida</CardT>
        <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
          {alertas.map(x => { const [c,bg,bd] = TONO_A[x.tone] || TONO_A.amber;
            return (
              <div key={x.id} style={{ display:'flex', alignItems:'center', gap:14, padding:'12px 14px', borderRadius:12, border:`1px solid ${bd}`, background:T.g950 }}>
                <span style={{ width:30, height:30, flex:'none', borderRadius:8, display:'grid', placeItems:'center', background:bg, color:c, fontSize:15 }}>{x.ic}</span>
                <span style={{ flex:1, fontFamily:T.sans, fontSize:13.5, lineHeight:1.45, color:T.c300 }}>{x.tx}</span>
                {/* Los botones resuelven, igual que en el teléfono: antes eran
                    decoración y no hacían absolutamente nada. */}
                <button onClick={() => onAlerta(x)} style={{ flex:'none', fontFamily:T.sans, fontWeight:600, fontSize:12.5, border:`1px solid ${T.g700}`, background:'transparent', color:T.c100, padding:'8px 15px', borderRadius:8, cursor:'pointer' }}>{x.a}</button>
              </div>
            );
          })}
        </div>
      </div>

      <div style={W.g1}>
        <div style={W.card}>
          <CardT>Hoy</CardT>
          <div style={{ display:'flex', flexDirection:'column', gap:26, flex:1, justifyContent:'center' }}>
            {[['Trabajos', hoy.length, T.c100], ['A cobrar', money(pend), T.v600], ['Confirmados', hoy.filter(j => j.tsm === 'CONFIRMADO').length, T.green]].map(([k,v,c]) => (
              <div key={k} style={{ display:'flex', flexDirection:'column', gap:4 }}>
                <div style={{ fontFamily:T.mono, fontSize:30, fontWeight:500, color:c, letterSpacing:'-1px', lineHeight:1 }}>{v}</div>
                <div style={W.lbl}>{k}</div>
              </div>
            ))}
          </div>
          <div style={{ marginTop:22, paddingTop:16, borderTop:`1px solid ${T.g800}`, fontFamily:T.sans, fontSize:12.5, lineHeight:1.5, color:T.c500 }}>Mia está respondiendo. Última consulta hace 8 minutos.</div>
        </div>

        <div style={W.card}>
          <CardT extra={hoy.length === 1 ? '1 parada' : `${hoy.length} paradas`}>Agenda de hoy</CardT>
          {hoy.length ? (
            <div style={{ display:'flex', flexDirection:'column' }}>
              {hoy.map((j,i) => (
                <button key={j.id} onClick={() => abrir(j.id)} style={{ display:'flex', alignItems:'center', gap:16, padding:'14px 0', borderTop:i?`1px solid ${T.g800}`:'none', background:'transparent', border:'none', cursor:'pointer', textAlign:'left', width:'100%' }}>
                  <span style={{ fontFamily:T.mono, fontSize:14, color:T.c100, width:50, flex:'none' }}>{j.hora}</span>
                  <span style={{ width:3, alignSelf:'stretch', flex:'none', borderRadius:9, background:j.tsm === 'CONFIRMADO' ? T.green : T.amber, display:'block' }} />
                  <span style={{ flex:1, minWidth:0 }}>
                    <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100 }}>{j.svc}</span>
                    <span style={{ display:'block', fontFamily:T.sans, fontSize:12.5, color:T.c500, marginTop:3 }}>{j.cli} · {j.donde}</span>
                  </span>
                  <WBadge tsm={j.tsm} />
                  <span style={{ fontFamily:T.mono, fontSize:14, color:T.c100, flex:'none' }}>{money(j.monto)}</span>
                </button>
              ))}
            </div>
          ) : <div style={{ fontFamily:T.sans, fontSize:13.5, color:T.c500 }}>Sin turnos hoy.</div>}
        </div>
      </div>

      <div style={W.g2}>
        <div style={W.card}>
          <CardT>Presupuestos abiertos</CardT>
          {jobs.filter(j => !conHora(j) && !esCierre(j.tsm)).map((j,i) => (
            <button key={j.id} onClick={() => abrir(j.id)} style={{ display:'flex', alignItems:'center', gap:12, padding:'13px 0', borderTop:i?`1px solid ${T.g800}`:'none', background:'transparent', border:'none', cursor:'pointer', textAlign:'left', width:'100%' }}>
              <span style={{ flex:1, minWidth:0 }}>
                <span style={{ display:'block', fontFamily:T.sans, fontSize:14, fontWeight:600, color:T.c100, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{j.svc}</span>
                <span style={{ display:'block', fontFamily:T.mono, fontSize:10, letterSpacing:'.07em', color:T.c500, marginTop:3 }}>{j.cli} · {j.enviado ? j.extra.toUpperCase() : 'SIN ENVIAR'}</span>
              </span>
              <span style={{ fontFamily:T.mono, fontSize:13.5, color:T.c100, flex:'none' }}>{money(j.monto)}</span>
            </button>
          ))}
        </div>
        <div style={W.card}>
          <CardT extra={`${resStats(rs).prom} ★`}>Reseñas nuevas</CardT>
          {rs.slice(0, 2).map(({ n, d, t }, i) => (
            <div key={i} style={{ padding:'13px 0', borderTop:i?`1px solid ${T.g800}`:'none' }}>
              <div style={{ display:'flex', alignItems:'baseline', gap:9 }}>
                <span style={{ fontFamily:T.sans, fontSize:13.5, fontWeight:600, color:T.c100 }}>{n}</span>
                <span style={{ color:T.v600, fontSize:10.5, letterSpacing:1 }}>★★★★★</span>
                <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:9, letterSpacing:'.09em', color:T.c500 }}>{d}</span>
              </div>
              <div style={{ fontFamily:T.sans, fontSize:13, lineHeight:1.5, color:T.c300, fontStyle:'italic', marginTop:5 }}>“{t}”</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* .live-job verbatim de panel.html:1282-1288 — el elemento más distintivo del
   panel y el que faltaba entero acá. Punto verde de 10px con halo de 4px en
   green-soft, latiendo (ljpulse 2s), título sans 600 15px y subtítulo mono 11
   cloud-500. Es lo único de la pantalla que se mueve: marca el que está en
   curso ahora mismo.                                                       */
function LiveJob({ job, abrir, onPaso, onTsm, cfg, onDemora, onReag }) {
  const [armado, pedir] = useArmar();
  if (!job) return null;
  /* Los pasos rápidos salen de TSM_ACCIONES[patrón][estado], igual que el
     menú del cajón: estaban escritos a mano con las transiciones del patrón C,
     así que una peluquería veía "En camino" —que su máquina no permite— y un
     taller no veía "En reparación". Se toman las transiciones que avanzan
     (cancelar no es un atajo) más el paso interno de la escalera.
     Los que le hablan al cliente van marcados: la línea de abajo promete que
     viajan al bot, y el interno no lo hace.                              */
  const pasos = pasosDe(cfg);
  /* El paso interno de arranque se busca por la clave que la escalera use
     —COMENZADO en los flujos que no tienen estado de curso, EN_CURSO /
     EN_REPARACION / EN_PREPARACION en los que sí—, y su rótulo sale de
     pasoDe: buscarlo por un nombre fijo lo hacía desaparecer. */
  const at = atajos(cfg).find(x => CAND.arranca.includes(x.k));
  const kCurso = at && at.k, iCurso = at ? at.n : -1;
  const rapidos = [
    ...(iCurso >= 0 && !esCierre(job.tsm) ? [{ l:at.t, paso:iCurso, on:job.paso >= iCurso, wa:saltoHabla(pasosDe(cfg), job.paso, iCurso) }] : []),
    ...accionesDe(cfg.patron, job.tsm)
      .map(([e,l]) => ({ l, tsm:e, on:job.tsm === e, wa:true, mal:e === 'CANCELADO' || e === 'AUSENTE' })),
    ...(!esCierre(job.tsm) && job.hora && job.hora !== '—' ? [{ l:'Reagendar', reag:true, wa:true }] : []),
  ];
  return (
    <div onClick={() => abrir(job.id)} style={{ ...W.card, marginBottom:18, gap:14, padding:'22px 26px', cursor:'pointer' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:16, flexWrap:'wrap' }}>
        <div style={{ display:'flex', alignItems:'center', gap:12, minWidth:0 }}>
          <i style={{ width:10, height:10, flex:'none', borderRadius:'50%', background:T.green, boxShadow:`0 0 0 4px ${T.gSoft}`, display:'block', animation:'ljpulse 2s ease-in-out infinite' }} />
          <div style={{ minWidth:0 }}>
            <div style={{ fontFamily:T.sans, fontWeight:600, fontSize:15, color:T.c100 }}>{job.svc}</div>
            <div style={{ fontFamily:T.mono, fontSize:11, color:T.c500, marginTop:3 }}>{job.cli.toUpperCase()} · {job.hora} · {(job.donde || '').toUpperCase()}</div>
          </div>
        </div>
        {/* .live-steps: avanzar sin abrir el trabajo. */}
        <div style={{ display:'flex', alignItems:'center', gap:8 }}>
          {rapidos.map(r => (
            <button key={r.l} title={r.wa ? 'Le avisa al cliente · pide confirmación' : 'Sólo para vos · no le llega al cliente'}
              onClick={e => { e.stopPropagation();
                const go = () => r.reag ? onReag(job) : r.tsm ? onTsm(job, r.tsm) : onPaso(job.id, r.paso);
                /* Reagendar abre su propia pantalla con motivo: ahí ya hay
                   confirmación, no hace falta armar dos veces. */
                r.wa && !r.reag ? pedir(r.l, go) : go(); }}
              style={{ display:'inline-flex', alignItems:'center', gap:6, minHeight:32, padding:'0 12px', borderRadius:8,
                background:armado===r.l?T.aSoft:r.on?T.gSoft:r.mal?T.coSoft:'transparent',
                border:armado===r.l ? `1px solid ${T.aBorder}` : r.wa ? `1px solid ${r.on?T.gBorder:r.mal?T.coBorder:T.g700}` : `1px dashed ${r.on?T.gBorder:T.g700}`,
                color:armado===r.l?T.amber:r.on?T.green:r.mal?T.coral:T.c300, fontFamily:T.sans, fontSize:12, fontWeight:600, cursor:'pointer' }}>
              {r.wa && <i style={{ width:4, height:4, borderRadius:9, background:armado===r.l?T.amber:r.on?T.green:r.mal?T.coral:T.v600, display:'block' }} />}{armado===r.l ? '¿Seguro? · tocá otra vez' : r.l}
            </button>
          ))}
          <span style={{ fontFamily:T.mono, fontSize:15, color:T.c100, marginLeft:6 }}>{money(job.monto)}</span>
        </div>
      </div>
      {/* .live-actions: el aviso de que esto viaja al bot, más las dos acciones
          del código — "Avisar demora" y "Marcar terminado". */}
      <div style={{ display:'flex', alignItems:'center', gap:12, paddingTop:13, borderTop:`1px solid ${T.g800}`, flexWrap:'wrap' }}>
        <span style={{ display:'flex', alignItems:'center', gap:8, minWidth:0 }}>
          <i style={{ width:6, height:6, flex:'none', borderRadius:9, background:T.green, display:'block' }} />
          <span style={{ fontFamily:T.sans, fontSize:12, color:T.c500 }}>Los pasos con punto se sincronizan con el bot de WhatsApp; los punteados son sólo para vos</span>
        </span>
        <span style={{ marginLeft:'auto', display:'flex', alignItems:'center', gap:9 }}>
          <WBadge tsm={job.tsm} />
          <button onClick={e => { e.stopPropagation(); onDemora(job); }} style={{ minHeight:34, padding:'0 14px', borderRadius:9, background:'transparent', border:`1px solid ${T.g700}`, color:T.c100, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>Avisar demora</button>
          <button onClick={e => { e.stopPropagation(); onTsm(job, 'FINALIZADO'); }} style={{ minHeight:34, padding:'0 14px', borderRadius:9, background:T.v600, border:'none', color:'#fff', fontFamily:T.sans, fontSize:12.5, fontWeight:700, cursor:'pointer' }}>Marcar terminado →</button>
        </span>
      </div>
    </div>
  );
}

function WAgenda({ jobs, cfg, abrir, onPaso, onTsm, onDemora, onReag, rango, off, p }) {
  const live = jobs.find(j => j.dia === 'HOY' && conHora(j) && !esCierre(j.tsm));
  const clave = ['L','M','X','J','V','S','D'];
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {off === 0 && <LiveJob job={live} abrir={abrir} onPaso={onPaso} onTsm={onTsm} cfg={cfg} onDemora={onDemora} onReag={onReag} />}
      <Capa>Estructura del rubro · {rango === 'Semana' ? 'cinco columnas' : 'calendario'}</Capa>
      {rango === 'Semana' ? (
        /* .week-grid: cinco columnas. El sábado y el domingo no son columnas
           propias en el código — la semana laboral es de cinco. */
        <div style={DAY.grid}>
          {p.baldes.slice(0,5).map(b => {
            const del = jobs.filter(j => { const f = fechaJob(j); return f && f >= b.desde && f < b.hasta; }).sort((x,y) => x.hora.localeCompare(y.hora));
            const fr = cfg.horario[clave[(b.desde.getDay()+6)%7]] || [];
            const hoy = mismoDia(b.desde, HOY_D);
            return (
              <div key={b.k} style={{ ...DAY.col, border:`1px solid ${hoy?T.vBorder:T.g700}` }}>
                <div style={DAY.head}>
                  <span style={{ ...DAY.name, color:T.c100 }}>{b.k.slice(0,3).toUpperCase()} {b.desde.getDate()}</span>
                  <span style={DAY.count}>{!fr.length ? 'cerrado' : del.length ? `${del.length}` : '—'}</span>
                </div>
                {del.map(j => <Turno key={j.id} j={j} on={() => abrir(j.id)} />)}
              </div>
            );
          })}
        </div>
      ) : (
        <div style={{ ...W.card, gap:14 }}>
          <CardT extra={p.lbl}>Mes completo</CardT>
          <div style={{ display:'grid', gridTemplateColumns:'repeat(7,1fr)', gap:8 }}>
            {clave.map(d => <div key={d} style={{ textAlign:'center', fontFamily:T.mono, fontSize:9, letterSpacing:'.1em', color:T.c500, paddingBottom:4 }}>{d}</div>)}
            {(() => {
              const ini = p.desde, pd = (ini.getDay()+6)%7, nd = new Date(ini.getFullYear(), ini.getMonth()+1, 0).getDate();
              return [...Array(pd).fill(null), ...Array.from({ length:nd }, (_,k) => new Date(ini.getFullYear(), ini.getMonth(), k+1))].map((d,k) => {
                if (!d) return <div key={'e'+k} />;
                const del = jobs.filter(j => { const f = fechaJob(j); return f && mismoDia(f, d); });
                const abiertoD = (cfg.horario[clave[(d.getDay()+6)%7]] || []).length > 0;
                const hoy = mismoDia(d, HOY_D);
                return (
                  <button key={k} onClick={() => del.length && abrir(del[0].id)} style={{ minHeight:74, display:'flex', flexDirection:'column', alignItems:'flex-start', gap:5, padding:'8px 9px', borderRadius:10, cursor:del.length?'pointer':'default',
                    background:hoy?T.vSoft:'transparent', border:`1px solid ${hoy?T.vBorder:abiertoD?T.g800:'transparent'}`, textAlign:'left' }}>
                    <span style={{ fontFamily:T.mono, fontSize:11, color:abiertoD?(hoy?T.c100:T.c300):T.c500 }}>{d.getDate()}</span>
                    {del.slice(0,2).map(j => { const t = turnoTono(j);
                      return <span key={j.id} style={{ display:'block', width:'100%', fontFamily:T.sans, fontSize:9.5, color:T.c300, borderLeft:`2px solid ${t.bd}`, paddingLeft:5, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{j.hora} {j.cli.split(' ')[0]}</span>;
                    })}
                    {del.length > 2 && <span style={{ fontFamily:T.mono, fontSize:8.5, color:T.c500 }}>+{del.length-2}</span>}
                  </button>
                );
              });
            })()}
          </div>
        </div>
      )}
    </div>
  );
}

function WTickets({ jobs, abrir }) {
  const pres = jobs.filter(j => !conHora(j));
  const COLS = '90px 160px minmax(0,1fr) 130px 110px';
  return (
    <div style={{ ...W.card, padding:0, overflow:'hidden' }}>
      <TRow cols={COLS} head><span>Ticket</span><span>Cliente</span><span>Trabajo</span><span>Estado</span><span style={{ textAlign:'right' }}>Monto</span></TRow>
      {pres.length ? pres.map(j => (
        <TRow key={j.id} cols={COLS} on={() => abrir(j.id)}>
          <span style={{ fontFamily:T.mono, color:T.v600, fontWeight:500, minWidth:0 }}>#{j.id}</span>
          <span style={{ color:T.c100, fontWeight:600, minWidth:0, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{j.cli}</span>
          <span style={{ color:T.c300, minWidth:0, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{j.svc}</span>
          <span style={{ minWidth:0 }}><WBadge tsm={j.tsm} /></span>
          <span style={{ fontFamily:T.mono, fontSize:13, color:T.c100, textAlign:'right', minWidth:0 }}>{money(j.monto)}</span>
        </TRow>
      )) : <div style={{ padding:'20px 22px', fontFamily:T.sans, fontSize:13, color:T.c500 }}>No hay presupuestos abiertos.</div>}
    </div>
  );
}

function WClientes({ clientes, jobs, abrir }) {
  const [q, setQ] = React.useState('');
  const vis = clientes.filter(c => c.n.toLowerCase().includes(q.toLowerCase()));
  const orden = [...vis].sort((a,b) => clienteResumen(b, jobs).facturado - clienteResumen(a, jobs).facturado);
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <div style={G.toolbar}>
        <input value={q} onChange={e => setQ(e.target.value)} placeholder="Buscar por nombre o WhatsApp"
          style={{ flex:1, minWidth:0, height:38, boxSizing:'border-box', padding:'0 14px', background:T.g900, border:`1px solid ${T.g700}`, borderRadius:10, color:T.c100, fontFamily:T.sans, fontSize:13, outline:'none' }} />
        <span style={{ fontFamily:T.mono, fontSize:10, letterSpacing:'.12em', color:T.c500, flex:'none' }}>ORDENADO POR LTV</span>
      </div>
      <div style={G.clientes}>
        {orden.map(c => { const r = clienteResumen(c, jobs);
          return (
            <button key={c.n} onClick={() => abrir(c.n)} style={G.cliCard}
              onMouseEnter={e => { e.currentTarget.style.borderColor = T.c500; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,.28)'; }}
              onMouseLeave={e => { e.currentTarget.style.borderColor = T.g700; e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = 'none'; }}>
              <div style={{ display:'flex', alignItems:'center', gap:11 }}>
                <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, fontWeight:700, color:T.c100 }}>{c.n.split(' ').map(x=>x[0]).join('')}</span>
                <span style={{ flex:1, minWidth:0 }}>
                  <span style={{ display:'block', fontFamily:T.sans, fontSize:14.5, fontWeight:600, color:T.c100, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.n}</span>
                  <span style={{ display:'block', fontFamily:T.mono, fontSize:10.5, color:T.c500, marginTop:3 }}>{c.tel}</span>
                </span>
                {c.tag && <span style={{ flex:'none', fontFamily:T.mono, fontSize:8, letterSpacing:'.1em', textTransform:'uppercase', color:T.c500, background:T.g800, border:`1px solid ${T.g700}`, borderRadius:4, padding:'3px 6px' }}>{c.tag}</span>}
              </div>
              <div style={{ fontFamily:T.sans, fontSize:12.5, color:T.c500, lineHeight:1.4, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.dir || 'Sin dirección cargada'}</div>
              <div style={{ display:'flex', alignItems:'baseline', gap:14, paddingTop:12, borderTop:`1px solid ${T.g800}` }}>
                <span><span style={{ fontFamily:T.mono, fontSize:16, color:T.c100 }}>{r.visitas}</span><span style={{ fontFamily:T.sans, fontSize:11, color:T.c500, marginLeft:5 }}>visitas</span></span>
                <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:15, color:T.c100, letterSpacing:'-.3px' }}>{money(r.facturado)}</span>
              </div>
              {r.proxima !== '—' && <div style={{ fontFamily:T.mono, fontSize:9.5, letterSpacing:'.09em', color:T.green }}>PRÓXIMA · {r.proxima}</div>}
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* La serie sale del historial real de los clientes (HIST, con fecha por
   trabajo) más los trabajos vivos. Antes esta card se llamaba "Evolución" y
   mostraba una foto de estado sin eje de tiempo, con cuatro pills y dos
   flechas que no rescopaban nada: siete controles decorativos.           */
function WReportes({ jobs, cfg, rango, setRango, off, setOff, p }) {
  const cobros = cobrosCon(jobs);
  const dentro = cobros.filter(x => x.f >= p.desde && x.f < p.hasta);
  const total = dentro.reduce((a,x) => a + x.monto, 0);
  const enPeriodo = jobs.filter(j => { const f = fechaJob(j); return f && f >= p.desde && f < p.hasta; });
  const filas = p.baldes.map(b => [b.k, enBalde(cobros, b).reduce((a,x) => a + x.monto, 0)]);
  const maxS = Math.max(...filas.map(f => f[1]), 1);
  const aCobrar = enPeriodo.filter(j => !esCierre(j.tsm) && conHora(j) && j.tsm !== 'RELEVADO').reduce((s,j) => s + j.monto, 0);
  /* Los trabajos que caen dentro de la ventana, para las métricas del rubro. */
  const enP = enPeriodo;
  const svc = {};
  dentro.forEach(x => { svc[x.svc] = (svc[x.svc] || 0) + x.monto; });
  const porSvc = Object.entries(svc).sort((a,b) => b[1] - a[1]).slice(0, 6);
  const maxSvc = Math.max(...porSvc.map(x => x[1]), 1);
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <NavPeriodo rango={rango} setRango={setRango} off={off} setOff={setOff} lbl={p.lbl} />

      <Capa>Base común · todos los rubros</Capa>
      <div style={G.repKpis}>
        <KPI k="facturado en el período" v={money(total)} col={T.green} />
        <KPI k="por cobrar en el período" v={money(aCobrar)} col={T.v600} />
        <KPI k="trabajos cobrados" v={dentro.length} />
        <KPI k="ticket promedio" v={money(dentro.length ? Math.round(total / dentro.length) : 0)} />
      </div>

      <div style={G.repRow}>
        <div style={G.repCard}>
          <CardT extra={p.lbl}>Evolución de facturación</CardT>
          <div style={{ display:'flex', flexDirection:'column', flex:1, justifyContent:'center', overflowY:'auto', scrollbarWidth:'none' }}>
            {filas.map(([n,v],k) => <BarRow key={n+k} label={n} v={v} max={maxS} primera={!k} />)}
          </div>
        </div>
        <div style={G.repCard}>
          <CardT extra={porSvc.length ? `${porSvc.length}` : ''}>Facturación por servicio</CardT>
          <div style={{ display:'flex', flexDirection:'column', flex:1 }}>
            {porSvc.length ? porSvc.map(([n,v],k) => (
              <div key={n} style={{ display:'grid', gridTemplateColumns:'minmax(0,1fr) 70px', alignItems:'center', gap:12, padding:'9px 0', borderTop:k?`1px solid ${T.g700}`:'none' }}>
                <span style={{ minWidth:0 }}>
                  <span style={{ display:'block', fontFamily:T.sans, fontSize:12.5, color:T.c100, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{n}</span>
                  <span style={{ display:'block', height:6, background:T.g700, borderRadius:5, overflow:'hidden', marginTop:6 }}><i style={{ display:'block', height:'100%', width:`${Math.max(3, v / maxSvc * 100)}%`, background:'linear-gradient(90deg,#6C63FF 0%,#4036B5 100%)', borderRadius:5 }} /></span>
                </span>
                <span style={{ fontFamily:T.mono, fontSize:12, color:T.c300, textAlign:'right' }}>{money(v)}</span>
              </div>
            )) : <div style={{ fontFamily:T.sans, fontSize:13, color:T.c500 }}>Nada cobrado en este período.</div>}
          </div>
        </div>
      </div>

      {/* Estas cuatro salen del MISMO período que las de arriba. Eran cuatro
          literales debajo de un navegador vivo: la mitad de los KPI de la
          pantalla rescopaban y la otra mitad no, con el mismo componente. */}
      <Capa>Métricas de tu rubro · Patrón {cfg.patron} · {(FLUJOS[cfg.patron]||{}).t}</Capa>
      <div style={G.repKpis}>
        <KPI k="turnos que agendó Mia" v={enP.filter(j => (j.hist||[])[0] === 'mia').length} col={T.green} />
        <KPI k="presupuestos enviados" v={enP.filter(j => j.enviado).length} col={T.v600} />
        <KPI k="cancelados" v={enP.filter(j => j.tsm === 'CANCELADO').length} col={T.coral} />
        {/* Los eventos salen de la BITÁCORA, no del estado actual: contar por
            `extra` perdía toda reagenda que después se confirmó, y contaba una
            sola vez un turno movido tres veces. Lo que el reporte necesita es
            cuántas veces pasó, no en qué estado quedó. */}
        <KPI k="veces reprogramado" v={enP.reduce((s,j) => s + (j.bit||[]).filter(e => e.tipo === 'reagenda').length, 0)} col={T.amber} />
        <KPI k="avisos de demora" v={enP.reduce((s,j) => s + (j.bit||[]).filter(e => e.tipo === 'demora').length, 0)} col={T.amber} />
      </div>
    </div>
  );
}

/* Las 4 reseñas visibles son las recientes; `viejas` es el resto del histórico
   que no se lista pero SÍ cuenta. Los tres contadores y el promedio salen de
   sumar las dos cosas: estaban escritos a mano —"23 · 21 · 19" y "4,8"— así
   que responder una reseña no movía nada, y encima Inicio tenía su propia
   copia de la lista. Una sola fuente para las dos vistas. */
const RESENAS_0 = [
  { n:'Paula G.', d:'HACE 2 DÍAS', e:5, t:'Puntual y prolija. Volvería sin dudar.', r:true },
  { n:'Diego S.', d:'HACE 6 DÍAS', e:5, t:'Dejó todo limpio, mejor de lo que esperaba.', r:true },
  { n:'Marina R.', d:'HACE 12 DÍAS', e:4, t:'Muy buen trabajo, tardó un poco más de lo hablado.', r:false },
  { n:'Carlos F.', d:'HACE 20 DÍAS', e:5, t:'Cumplió con el presupuesto al peso. Lo recomiendo.', r:true },
];
const HIST_RES = { total:19, respondidas:18, cinco:15, suma:91 };
const resStats = rs => {
  const total = HIST_RES.total + rs.length;
  const resp = HIST_RES.respondidas + rs.filter(r => r.r).length;
  const cinco = HIST_RES.cinco + rs.filter(r => r.e === 5).length;
  const prom = (HIST_RES.suma + rs.reduce((s,r) => s + r.e, 0)) / total;
  return { total, resp, cinco, prom:prom.toFixed(1).replace('.', ',') };
};

function WResenas({ rs, setRs }) {
  /* "Responder" era decoración. Ahora abre el compositor, deja la respuesta
     y marca la reseña como respondida — así el filtro "Sin responder" baja. */
  const [abierta, setAbierta] = React.useState(null);
  const [txt, setTxt] = React.useState('');
  const [filtro, setFiltro] = React.useState('Todas');
  const vis = filtro === 'Todas' ? rs : filtro === '5★' ? rs.filter(r => r.e === 5) : rs.filter(r => !r.r);
  /* La estrella vacía usaba grafito-700, que es token de SUPERFICIE: 1,55:1,
     o sea el denominador de la nota no se leía. Va en cloud-500. */
  const est = n => <span style={{ color:T.v600, fontSize:11, letterSpacing:1 }}>{'★'.repeat(n)}<span style={{ color:T.c500 }}>{'★'.repeat(5-n)}</span></span>;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {/* Los filtros van en su propia toolbar, como en Reportes y Clientes.
          En el código viven adentro de .res-summary, pero ahí la fila pide
          922 px y el marco da 862: con flex-wrap —que en el código existe
          SOLO en @media (max-width:600px)— la barra se parte y deja 63 px de
          vacío con las pills colgando solas. Afuera entra en una fila. */}
      <div style={G.toolbar}>
        <span style={{ fontFamily:T.mono, fontSize:10, letterSpacing:'.12em', color:T.c500 }}>MOSTRAR</span>
        {['Todas','5★','Sin responder'].map(f => <Pill key={f} on={filtro===f} click={() => setFiltro(f)}>{f}</Pill>)}
        <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:10, letterSpacing:'.12em', color:T.c500 }}>{vis.length} DE {rs.length}</span>
      </div>
      <div style={{ display:'flex', alignItems:'center', gap:28, background:T.g900, border:`1px solid ${T.g700}`, borderRadius:18, padding:'22px 28px' }}>
        <div style={{ display:'flex', alignItems:'baseline', gap:10 }}>
          <span style={{ fontFamily:T.mono, fontWeight:500, fontSize:36, color:T.c100, lineHeight:1, letterSpacing:'-.6px' }}>{resStats(rs).prom}</span>
          <span style={{ color:T.v600, fontSize:18, letterSpacing:3 }}>★★★★★</span>
          <span style={{ fontFamily:T.sans, fontSize:12, color:T.c500, marginLeft:6 }}>promedio</span>
        </div>
        {(() => { const st = resStats(rs); return [[String(st.total),'reseñas totales',null],[String(st.resp),'respondidas',T.green],[String(st.cinco),'de 5★',null]]; })().map(([v,l,c],i) => (
          <div key={i} style={{ borderLeft:`1px solid ${T.g700}`, paddingLeft:22, display:'flex', alignItems:'baseline', gap:8 }}>
            <span style={{ fontFamily:T.mono, fontSize:18, color:c || T.c100 }}>{v}</span>
            <span style={{ fontFamily:T.sans, fontSize:12, color:T.c300 }}>{l}</span>
          </div>
        ))}
      </div>
      <Capa>Nombre visible · anonimato configurable (OFF)</Capa>
      <div style={G.resGrid}>
        {vis.map((r,i) => (
          <div key={i} style={G.resCard}>
            <div style={{ display:'flex', alignItems:'baseline', gap:9 }}>
              <span style={{ fontFamily:T.sans, fontSize:13.5, fontWeight:600, color:T.c100 }}>{r.n}</span>
              {est(r.e)}
              <span style={{ marginLeft:'auto', fontFamily:T.mono, fontSize:9, letterSpacing:'.09em', color:T.c500 }}>{r.d}</span>
            </div>
            <div style={{ fontFamily:T.sans, fontSize:13.5, lineHeight:1.5, color:T.c300, fontStyle:'italic', flex:1 }}>“{r.t}”</div>
            <div style={{ display:'flex', alignItems:'center', gap:8, paddingTop:10, borderTop:`1px solid ${T.g800}` }}>
              <span style={{ fontFamily:T.mono, fontSize:9, letterSpacing:'.1em', color:r.r?T.green:T.amber }}>{r.r ? 'RESPONDIDA' : 'SIN RESPONDER'}</span>
              {!r.r && <button onClick={() => { setAbierta(r.n); setTxt(''); }} style={{ marginLeft:'auto', minHeight:32, padding:'0 12px', borderRadius:8, background:abierta===r.n?T.vSoft:'transparent', border:`1px solid ${abierta===r.n?T.vBorder:T.g700}`, color:T.c100, fontFamily:T.sans, fontSize:12, fontWeight:600, cursor:'pointer' }}>Responder</button>}
            </div>
            {abierta === r.n && (
              <div style={{ display:'flex', flexDirection:'column', gap:8, paddingTop:10, borderTop:`1px solid ${T.g800}` }}>
                <textarea value={txt} onChange={e => setTxt(e.target.value)} rows={2} placeholder={`Gracias ${r.n.split(' ')[0]}…`}
                  style={{ width:'100%', boxSizing:'border-box', padding:'10px 12px', background:T.g950, border:`1px solid ${txt?T.vBorder:T.g700}`, borderRadius:10, color:T.c100, fontFamily:T.sans, fontSize:13, lineHeight:1.5, outline:'none', resize:'none' }} />
                <div style={{ display:'flex', gap:8 }}>
                  <button onClick={() => setAbierta(null)} style={{ flex:'none', minHeight:36, padding:'0 12px', borderRadius:8, background:'transparent', border:`1px solid ${T.g700}`, color:T.c500, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>Cancelar</button>
                  <button onClick={() => { if (!txt.trim()) return; setRs(v => v.map(x => x.n === r.n ? { ...x, r:true, resp:txt.trim() } : x)); setAbierta(null); }} disabled={!txt.trim()}
                    style={{ flex:1, minHeight:36, borderRadius:8, background:txt.trim()?T.v600:T.g800, border:'none', color:txt.trim()?'#fff':T.c500, fontFamily:T.sans, fontSize:12.5, fontWeight:700, cursor:txt.trim()?'pointer':'default' }}>Publicar respuesta</button>
                </div>
              </div>
            )}
            {r.resp && <div style={{ paddingTop:10, borderTop:`1px solid ${T.g800}`, fontFamily:T.sans, fontSize:12.5, lineHeight:1.5, color:T.c500 }}><b style={{ color:T.c300 }}>Tu respuesta:</b> {r.resp}</div>}
          </div>
        ))}
      </div>
    </div>
  );
}

/* ── configuración + tour ─────────────────────────────────── */
function WConfig({ cfg, resumen, paso, setPaso, tour, setTour, gridRef, cardsRef, abrir, pend, aplicar }) {
  const p = tour ? PASOS_ONB[paso] : null;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      <div style={{ display:'flex', alignItems:'center', gap:10 }}>
        {!tour && <button onClick={() => { setPaso(0); setTour(true); }} style={{ minHeight:42, padding:'0 16px', borderRadius:10, background:'transparent', border:`1px solid ${T.g700}`, color:T.c300, fontFamily:T.sans, fontSize:13, fontWeight:600, cursor:'pointer' }}>Ver la guía</button>}
        {/* La barra de aplicar vive arriba y fija: en escritorio no hay
            MainButton, y esconder la acción al pie de una grilla de 10 cards
            es perderla. */}
        <div style={{ marginLeft:'auto', display:'flex', alignItems:'center', gap:12 }}>
          <span style={{ fontFamily:T.mono, fontSize:10, letterSpacing:'.09em', color:pend.length ? T.amber : T.c500 }}>{pend.length ? `${pend.length} SIN APLICAR` : 'MIA ESTÁ AL DÍA'}</span>
          <button onClick={aplicar} disabled={!pend.length} style={{ minHeight:42, padding:'0 18px', borderRadius:10, background:pend.length?T.v600:T.g800, border:'none', color:pend.length?'#fff':T.c500, fontFamily:T.sans, fontSize:13.5, fontWeight:700, cursor:pend.length?'pointer':'default' }}>Aplicar en Mia</button>
        </div>
      </div>
      <div ref={gridRef} style={G.cfg}>
        {CFG.map((c,i) => { const on = p && c.k === p.k;
          return (
            <button key={c.k} onClick={() => abrir(c.k)} ref={el => cardsRef.current[i] = el} style={{ ...W.card, alignItems:'stretch', textAlign:'left', cursor:'pointer', border:`1px solid ${on?TOUR_OFF.accent:pend.includes(c.k)?T.aBorder:T.g700}`, padding:'20px 22px 18px', gap:9, position:'relative', zIndex:on?3:1, boxShadow:on?`0 0 0 9999px rgba(15,13,11,.65), 0 0 0 3px ${TOUR_OFF.accent}`:'0 6px 20px rgba(0,0,0,.22)' }}>
              <div style={W.lbl}>{c.k}</div>
              <div style={{ fontFamily:T.mono, fontSize:15, color:T.c100, letterSpacing:'-.3px' }}>{resumen[c.k]}</div>
              <div style={{ height:1, background:T.g800 }} />
              <div style={{ fontFamily:T.sans, fontSize:12, lineHeight:1.5, color:T.c500 }}>{((PASOS_ONB.find(x => x.k === c.k) || {}).x || 'Se configura desde acá y Mia lo toma al aplicar los cambios.').slice(0, 76)}…</div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* Sube hasta el ancestro que scrollea de verdad. Asumir el padre directo se
   rompe apenas se envuelve la grilla en otro div — y setear scrollTop en algo
   que no scrollea es un no-op silencioso. */
function scroller(el) {
  let n = el && el.parentElement;
  while (n && getComputedStyle(n).overflowY !== 'auto') n = n.parentElement;
  return n;
}

function PanelWeb({ onVista, inicial }) {
  /* `inicial` sólo elige con qué vista abre — lo usa la landing para
     mostrar la vista de la que habla cada sección. Sin prop, Inicio. */
  const [vista, setVista] = React.useState(inicial || 'inicio');
  React.useEffect(() => { onVista && onVista(vista); }, [vista]);
  const [tour, setTour] = React.useState(true);
  const [paso, setPaso] = React.useState(0);
  /* Estado real, no constantes: el panel gestiona igual que la mini-app. */
  /* Misma semilla de bitácora que la mini-app: sin esto el panel abriría el
     historial vacío aunque el trabajo tenga meses de movimientos. */
  const [jobs, setJobs] = React.useState(JOBS.map(j => ({ ...j, bit:BIT0[j.id] || [{ h:'08:00', t:'Trabajo creado', por:(j.hist||[])[0] === 'mia' ? 'mia' : 'vos', tono:'alta' }] })));
  const [clientes, setClientes] = React.useState(CLIENTES_INIT);
  const [cfg, setCfg] = React.useState(CFG_INIT);
  /* Un solo cajón dirigido por TIPO, igual que la pila de la mini-app. Antes
     eran dos estados sueltos y todo lo que no fuera trabajo o configuración
     quedaba en un toast que no abría nada. */
  const [pane, setPane] = React.useState(null);
  const [menu, setMenu] = React.useState(false);
  const [reag, setReag] = React.useState(false);
  const [visto, setVisto] = React.useState([]);
  const [rs, setRs] = React.useState(RESENAS_0);
  const [pend, setPend] = React.useState([]);
  const [draft, setDraft] = React.useState(null);
  /* El período vive en el shell, no en cada vista: el subtítulo del header lo
     lee de acá. Antes era un mapa estático que decía "esta semana" mientras
     el cuerpo mostraba agosto. */
  const [rangoAg, setRangoAg] = React.useState('Semana');
  const [offAg, setOffAg] = React.useState(0);
  const [rangoRep, setRangoRep] = React.useState('Mes');
  const [offRep, setOffRep] = React.useState(0);
  const [form, setForm] = React.useState({ cli:'', svc:'', monto:'', tipo:'Confirmado', dia:'HOY' });
  const [matePaso, setMatePaso] = React.useState(0);
  const [docRango, setDocRango] = React.useState('Mes');
  const [docOff, setDocOff] = React.useState(0);
  const [docFiltro, setDocFiltro] = React.useState('Todos');
  const abrir = (type, extra) => setPane({ type, ...extra });
  const setSelJob = id => abrir('job', { id });
  const setSelCfg = k => abrir('cfg', { k });
  const nuevo = tipo => { const f = { cli:'', svc:'', monto:'', tipo:tipo || 'Confirmado', dia:'HOY' };
    setForm(f); setMatePaso(primerPaso(pregsDe(cfg, f.tipo, f), f)); abrir('nuevo'); };
  const [toast, setToast] = React.useState(null);
  const resumen = cfgResumen(cfg);

  const avisar = (k, msg) => { setToast({ k, msg, tono:/success/.test(k) ? 'ok' : /error|warning/.test(k) ? 'mal' : 'info', id:Date.now() });
    window.clearTimeout(avisar._t); avisar._t = window.setTimeout(() => setToast(null), 2600); };
  /* Ver marcar() en la mini-app: cada paso lleva su dueño, no el del salto. */
  /* Ver marcar() en la mini-app: sólo se rellena lo que tiene evidencia. */
  const marcar2 = (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; }
      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 job = pane && pane.type === 'job' ? jobs.find(j => j.id === pane.id) || null : null;
  const jobDoc = pane && (pane.type === 'doc' || pane.type === 'items') ? jobs.find(j => j.id === pane.id) || null : null;
  const cli = pane && (pane.type === 'cli' || pane.type === 'hist') ? clientes.find(c => c.n === pane.n) || null : null;

  /* Un solo handler de transición TSM. El live-job y el menú del cajón usan
     este mismo: antes los pasos rápidos movían la escalera de progreso sin
     tocar el estado, o sea que "En camino" hacía dos cosas distintas según
     dónde lo tocaras — y la línea de abajo prometía que iba al bot. */
  const accionJob = (j, est) => {
    const mal = est === 'CANCELADO' || est === 'AUSENTE';
    const pp = pasosDe(cfg), nn = pp.indexOf(est);
    setJobs(v => v.map(x => x.id === j.id ? anotar({ ...x, tsm:est, paso:nn >= 0 ? Math.max(x.paso, nn) : x.paso,
      hist:nn >= 0 ? marcar2(x, nn, 'vos') : x.hist, 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));
    setMenu(false);
    avisar(est === 'CANCELADO' || est === 'AUSENTE' || est === 'DEMORADO' ? 'notificationOccurred · warning' : 'notificationOccurred · success',
      `${EST_LBL[est]} · Mia le avisó a ${j.cli.split(' ')[0]}`);
  };
  const accion = est => accionJob(job, est);
  const relevar = () => {
    setJobs(v => v.map(x => x.id === job.id ? anotar({ ...x, tsm:'RELEVADO', paso:3, hist:marcar2(x, 3, 'vos'), extra:'Relevado · falta presupuestar' }, { t:'Relevado', d:'Falta presupuestar', por:'vos', tono:'cambio' }) : x));
    setMenu(false);
    avisar('notificationOccurred · success', 'Visita cerrada · falta el presupuesto');
  };
  /* El motivo decide si la visita se conserva o se limpia. Hardcodearlo a
     "el cliente no podía" borraba el relevamiento de un pintor que sí fue. */
  const reagendar = mk => {
    const mot = MOTIVOS.find(x => x.k === mk) || MOTIVOS[0];
    setJobs(v => v.map(x => x.id === job.id ? anotar({ ...x, dia:x.dia === 'HOY' ? 'MAÑANA' : 'SAB', 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 ${x.dia === 'HOY' ? 'mañana' : 'el sábado'}`, por:'vos', tono:'cambio', tipo:'reagenda' }) : x));
    setMenu(false); setReag(false);
    avisar('notificationOccurred · success', mot.mantiene ? `Visita registrada · nueva fecha para ${job.cli.split(' ')[0]}` : `Mia le propone otro horario a ${job.cli.split(' ')[0]}`);
  };
  const onPaso = (id, n) => {
    const j = jobs.find(x => x.id === id);
    if (!j || esCierre(j.tsm)) return;
    const ult = pasosDe(cfg).length - 1;
    /* Igual que en la mini-app: si el peldaño ES un estado del TSM, mueve el
       TSM. Sin esto el panel tildaba "En camino" y el badge seguía diciendo
       CONFIRMADO, así que el menú no ofrecía "Me demoré". */
    const kk = pasosDe(cfg)[n];
    if (n < ult && EST_LBL[kk] && kk !== 'AGENDADO') { accionJob(j, kk); return; }
    if (n >= ult) { setJobs(v => v.map(x => x.id === id ? anotar({ ...x, tsm:'FINALIZADO', paso:ult, hist:marcar2(x, ult, 'vos'), extra:'Cobrado' }, { t:`Cobrado ${money(j.monto)}`, d:'Mia le mandó el recibo', por:'vos', tono:'ok', tipo:'cobro' }) : x));
      avisar('notificationOccurred · success', `Cobrado ${money(j.monto)} · recibo enviado`); return; }
    const k = pasosDe(cfg)[n], dd = pasoDe(k, cfg.patron);
    setJobs(v => v.map(x => x.id === id ? anotar({ ...x, paso:n, hist:marcar2(x, n, 'vos') }, { t:dd.t, por:'vos', tono:'vos' }) : x));
  };
  const demora = j => accionJob(j, 'DEMORADO');
  const onFoto = id => { setJobs(v => v.map(x => x.id === id ? { ...x, notas:[...(x.notas||[]), { t:'Foto del lugar', foto:true, tono:(x.notas||[]).length % 3, cuando:'HOY · AHORA' }] } : x));
    avisar('notificationOccurred · success', 'Foto guardada en el trabajo'); };
  const onNota = (id, t) => { setJobs(v => v.map(x => x.id === id ? { ...x, notas:[...(x.notas||[]), { t, cuando:'HOY · AHORA' }] } : x));
    avisar('notificationOccurred · success', 'Anotación guardada en el trabajo'); };
  const enviarPres = id => { setJobs(v => v.map(x => x.id === id ? { ...x, enviado:true, extra:'Enviado hoy' } : x));
    avisar('notificationOccurred · success', 'Presupuesto enviado por WhatsApp'); };
  const total = PASOS_ONB.length;
  const p = PASOS_ONB[paso];
  const idx = Math.max(0, CFG.findIndex(c => c.k === p.k));
  const col = idx % 2;
  const wrap = React.useRef(null), cards = React.useRef([]), tip = React.useRef(null), grid = React.useRef(null);
  const [pos, setPos] = React.useState({ top:118, left:404 });

  /* El tour vive en Configuración: si el tipo navega a otra vista, se corta.
     Iluminar una card que no está en pantalla no es un tour, es un fantasma. */
  React.useEffect(() => { if (vista !== 'configuracion') setTour(false); }, [vista]);

  React.useLayoutEffect(() => {
    if (!tour || vista !== 'configuracion') return;
    const w = wrap.current, c = cards.current[idx], tp = tip.current, g = grid.current;
    if (!w || !c) return;
    const sc = scroller(g);
    if (sc) sc.scrollTop = Math.max(0, c.offsetTop - (sc.clientHeight - c.offsetHeight) / 2);
    const wr = w.getBoundingClientRect(), cr = c.getBoundingClientRect();
    const th = tp ? tp.offsetHeight : 190;
    const top = Math.max(12, Math.min(cr.top - wr.top + (cr.height - th) / 2, wr.height - th - 12));
    setPos({ top, left: col ? 404 : 726 });
  }, [idx, col, tour, vista]);

  const pAg = periodo(rangoAg, offAg), pRep = periodo(rangoRep, offRep);
  const titulo = (NAV_WEB.find(n => n.id === vista) || {}).t;
  const sub = { inicio:'jueves 3 de septiembre', agenda:pAg.lbl, tickets:'presupuestos y recibos', clientes:`${clientes.length} en la agenda`, reportes:pRep.lbl, resenas:`${resStats(rs).total} en total`, configuracion:'lo que Mia usa para atender' }[vista];

  return (
    <div ref={wrap} style={{ display:'flex', height:'100%', background:T.g950, fontFamily:T.sans, position:'relative', overflow:'hidden' }}>
      <aside style={{ width:214, flex:'none', background:T.g900, borderRight:`1px solid ${T.g800}`, display:'flex', flexDirection:'column' }}>
        <div style={{ display:'flex', alignItems:'center', gap:10, padding:'18px 18px 14px' }}>
          <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>
          <span style={{ fontSize:15.5, fontWeight:700, color:T.c100, letterSpacing:'-.3px' }}>Mi<span style={{ color:T.v600 }}>.</span>Asistente</span>
        </div>
        <nav style={{ display:'flex', flexDirection:'column', gap:1, padding:'0 10px' }}>
          {NAV_WEB.map(n => { const on = n.id === vista;
            return (
              <button key={n.id} onClick={() => setVista(n.id)} style={{ display:'flex', alignItems:'center', gap:10, minHeight:38, padding:'0 10px', borderRadius:9, border:'none', borderLeft:`2px solid ${on?T.v600:'transparent'}`, background:on?T.vSoft:'transparent', color:on?T.c100:T.c500, fontFamily:T.sans, fontSize:13, fontWeight:on?600:400, cursor:'pointer', textAlign:'left' }}>
                <NavIcon id={n.id} color={on?T.v600:T.c500} />{n.t}
              </button>
            );
          })}
        </nav>
        <div style={{ marginTop:'auto', padding:16, borderTop:`1px solid ${T.g800}`, fontFamily:T.mono, fontSize:9, letterSpacing:'.09em', color:T.c500, lineHeight:1.7 }}>PANEL DEL EMPRENDEDOR<br/>MISMO CONTENIDO QUE<br/>LA MINI-APP</div>
      </aside>

      <main style={{ flex:1, minWidth:0, display:'flex', flexDirection:'column' }}>
        <header style={{ flex:'none', height:60, borderBottom:`1px solid ${T.g800}`, display:'flex', alignItems:'center', gap:12, padding:'0 24px' }}>
          <div>
            <div style={{ ...W.h, fontSize:20 }}>{(titulo || '').toUpperCase()}</div>
            <div style={{ fontFamily:T.sans, fontSize:11.5, color:T.c500, marginTop:2 }}>{sub}</div>
          </div>
          <div style={{ marginLeft:'auto', display:'flex', alignItems:'center', gap:16 }}>
            <span style={{ display:'flex', alignItems:'center', gap:8, fontFamily:T.mono, fontSize:10, color:T.c500 }}>
              <i style={{ width:6, height:6, borderRadius:9, background:T.green, boxShadow:`0 0 0 3px ${T.gSoft}`, display:'block' }} />MIA EN LÍNEA
            </span>
            {/* Crear vive en el header y está en las 7 vistas: en escritorio la
                acción principal no puede depender de en qué pantalla estás. */}
            <div style={{ display:'flex', gap:8 }}>
              <button onClick={() => nuevo('Confirmado')} style={{ minHeight:36, padding:'0 14px', borderRadius:9, background:'transparent', border:`1px solid ${T.g700}`, color:T.c100, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>+ Trabajo</button>
              {hayPresupuesto(cfg) && <button onClick={() => nuevo('Presupuesto')} style={{ minHeight:36, padding:'0 14px', borderRadius:9, background:T.v600, border:'none', color:'#fff', fontFamily:T.sans, fontSize:12.5, fontWeight:700, cursor:'pointer' }}>+ Presupuesto</button>}
            </div>
          </div>
        </header>
        <div style={{ flex:1, minHeight:0, overflowY:'auto', padding:24, scrollbarWidth:'none' }}>
          {vista === 'inicio' && <WInicio jobs={jobs} cfg={cfg} abrir={setSelJob} rs={rs} visto={visto} onAlerta={a => {
             /* abrir(type, extra) espera un ID, no el trabajo entero, y el pane
                es un objeto, no un string. */
             if (a.acc === 'visto') { setVisto(v => [...v, a.id]); avisar('impactOccurred · light', 'Alerta marcada como vista'); }
             else if (a.acc === 'pasar') { abrir('job', { id:a.job }); setMenu(false); setReag(true); }
             else if (a.acc === 'enviar') { abrir('doc', { id:a.job, tipo:'presupuesto' }); }
           }} />}
          {vista === 'agenda' && <>
            <div style={{ marginBottom:14 }}><NavPeriodo rango={rangoAg} setRango={setRangoAg} off={offAg} setOff={setOffAg} lbl={pAg.lbl} rangos={['Semana','Mes']} /></div>
            <WAgenda jobs={jobs} cfg={cfg} abrir={setSelJob} onPaso={onPaso} onTsm={accionJob} onDemora={demora} onReag={j => { abrir('job', { id:j.id }); setMenu(false); setReag(true); }} rango={rangoAg} off={offAg} p={pAg} />
          </>}
          {vista === 'tickets' && <div style={{ ...W.card, gap:16 }}><Docs jobs={jobs} rango={docRango} setRango={setDocRango} off={docOff} setOff={setDocOff} filtro={docFiltro} setFiltro={setDocFiltro} abrir={(j,t) => abrir('doc', { id:j.id, tipo:t })} /></div>}
          {vista === 'clientes' && <WClientes clientes={clientes} jobs={jobs} abrir={n => abrir('cli', { n })} />}
          {vista === 'reportes' && <WReportes jobs={jobs} cfg={cfg} rango={rangoRep} setRango={setRangoRep} off={offRep} setOff={setOffRep} p={pRep} />}
          {vista === 'resenas' && <WResenas rs={rs} setRs={setRs} />}
          {vista === 'configuracion' && <WConfig cfg={cfg} resumen={resumen} paso={paso} setPaso={setPaso} tour={tour} setTour={setTour} gridRef={grid} cardsRef={cards} abrir={setSelCfg} pend={pend} aplicar={() => { setPend([]); avisar('notificationOccurred · success', 'Mia ya tiene la configuración nueva'); }} />}
        </div>
      </main>

      {toast && <PToast key={toast.id} {...toast} />}

      {pane && pane.type === 'cfg' && (
        <Drawer open onClose={() => { setDraft(null); setPane(null); }} title={pane.k}
          sub={draft && JSON.stringify(draft) !== JSON.stringify(cfg) ? 'sin guardar · se descarta si cerrás' : cfgResumen(draft || cfg)[pane.k]}
          footer={<>
            <PBtn on={() => { setDraft(null); setPane(null); }}>Descartar</PBtn>
            <PBtn wide tone="pri" on={() => {
              if (draft && JSON.stringify(draft) !== JSON.stringify(cfg)) {
                setCfg({ ...draft, tocado:draft.tocado.includes(pane.k) ? draft.tocado : [...draft.tocado, pane.k] });
                setPend(v => v.includes(pane.k) ? v : [...v, pane.k]);
                avisar('notificationOccurred · success', 'Guardado · falta aplicarlo en Mia');
              }
              setDraft(null); setPane(null);
            }}>Guardar</PBtn>
          </>}>
          {/* El editor toca un BORRADOR: cerrar sin guardar descarta. Antes
              escribía en la config viva y el cambio quedaba aplicado sin
              entrar en pendientes, o sea sin llegar nunca a Mia. */}
          <CfgEditor k={pane.k} cfg={draft || cfg} set={fn => setDraft(x => fn(x || cfg))} />
        </Drawer>
      )}

      {jobDoc && pane.type === 'doc' && (
        <Drawer open onClose={() => setPane(null)} title={pane.tipo === 'recibo' ? 'Recibo' : 'Presupuesto'} sub={`#${jobDoc.id} · ${jobDoc.cli}`}
          footer={pane.tipo === 'recibo'
            ? <PBtn wide tone="pri" on={() => avisar('notificationOccurred · success', 'Recibo reenviado')}>Mandárselo otra vez</PBtn>
            : jobDoc.enviado
              ? <PBtn wide tone="pri" on={() => avisar('notificationOccurred · success', `Mia le escribió a ${jobDoc.cli.split(' ')[0]}`)}>Insistirle a {jobDoc.cli.split(' ')[0]}</PBtn>
              : <PBtn wide tone="pri" on={() => enviarPres(jobDoc.id)}>Mandar el presupuesto</PBtn>}>
          <DocView job={jobDoc} tipo={pane.tipo} cfg={cfg} editar={cfg.presupuesto.items ? () => abrir('items', { id:jobDoc.id }) : null} />
        </Drawer>
      )}

      {jobDoc && pane.type === 'items' && (
        <Drawer open onClose={() => setPane(null)} title="Ítems del presupuesto" sub={`${(jobDoc.items||[]).length} · ${money(totalItems(jobDoc.items||[]))}`}
          footer={<PBtn wide tone="pri" on={() => { abrir('doc', { id:jobDoc.id, tipo:'presupuesto' }); avisar('notificationOccurred · success', 'Ítems actualizados'); }}>Listo</PBtn>}>
          <ItemsEditor items={jobDoc.items || []} set={n => setJobs(v => v.map(x => x.id === jobDoc.id ? { ...x, items:n, monto:totalItems(n) } : x))}
            ticket={jobDoc.ticket} setTicket={t => setJobs(v => v.map(x => x.id === jobDoc.id ? { ...x, ticket:t } : x))} />
        </Drawer>
      )}

      {cli && pane.type === 'cli' && (
        <Drawer open onClose={() => setPane(null)} title={cli.n} sub={`${clienteResumen(cli, jobs).visitas} visitas · ${money(clienteResumen(cli, jobs).facturado)}`}
          footer={<><PBtn on={() => abrir('hist', { n:cli.n })}>Historial</PBtn><PBtn wide tone="pri" 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)); abrir('nuevo'); }}>Cargarle un trabajo</PBtn></>}>
          <ClienteDet cli={cli} jobs={jobs} abrir={j => abrir('job', { id:j.id })} verHist={() => abrir('hist', { n:cli.n })} onMapa={() => avisar('openLink')} />
        </Drawer>
      )}

      {cli && pane.type === 'hist' && (
        <Drawer open onClose={() => setPane(null)} title="Historial" sub={`${cli.n} · ${money(clienteResumen(cli, jobs).facturado)} en total`}
          footer={<PBtn wide on={() => abrir('cli', { n:cli.n })}>Volver a la ficha</PBtn>}>
          <Historial cli={cli} hist={clienteResumen(cli, jobs).hist} jobs={jobs} abrirDoc={(id, tipo, real) => real ? abrir('doc', { id, tipo }) : avisar('archivo', `El ${tipo} #${id} está archivado`)} />
        </Drawer>
      )}

      {pane && pane.type === 'nuevo' && (() => {
        const pregs = pregsDe(cfg, form.tipo, form), fin = matePaso >= pregs.length;
        return (
          <Drawer open onClose={() => setPane(null)} title={form.tipo === 'Presupuesto' ? 'Nuevo presupuesto' : 'Nuevo trabajo'} sub={fin ? 'listo para guardar' : `con Mate · ${matePaso + 1} de ${pregs.length}`}
            footer={<PBtn wide tone="pri" on={() => { if (!fin) return;
              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), pres = form.tipo === 'Presupuesto';
              const nom = form.cli.trim(), nueva = nom && !clientes.some(c => c.n === nom);
              /* Mate promete "lo doy de alta de paso" — y hay que darlo de alta.
                 Sin esto la plata entra en los totales pero el cliente no existe
                 en la agenda, y su ficha y su historial quedan inalcanzables. */
              if (nueva) setClientes(v => [{ n:nom, tel:form.tel || '+54 9 341 ···', dir:form.donde || '', notas:'', v:0, ult:'—', total:0, tag:'Nueva' }, ...v]);
              setJobs(v => [{ 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'], extra:pres?'Sin enviar':'Mia le confirma', tel:form.tel||'+54 9 341 ···', donde:form.donde||'En tu local' }, ...v]);
              setPane(pres ? { type:'doc', id, tipo:'presupuesto' } : null);
              avisar('notificationOccurred · success',
                pres ? (nueva ? `Presupuesto armado · ${nom.split(' ')[0]} quedó en tu agenda` : 'Presupuesto armado, falta enviarlo')
                     : (nueva ? `Turno cargado · ${nom.split(' ')[0]} quedó en tu agenda` : `Turno cargado · Mia le confirma a ${nom.split(' ')[0]}`));
            }}>{fin ? (form.tipo === 'Presupuesto' ? 'Armar el presupuesto' : 'Guardar el trabajo') : 'Contestale a Mate'}</PBtn>}>
            <MateCarga cfg={cfg} form={form} setForm={setForm} paso={matePaso} clientes={clientes}
              responder={(k,v,p,c) => { setForm(f => ({ ...f, [k]:v, ...(p?{monto:String(p)}:{}) , ...(c?{tel:c.tel, donde:c.dir||f.donde}:{}) })); setMatePaso(n => n + 1); }} />
          </Drawer>
        );
      })()}

      <Drawer ancho={520} open={!!job} onClose={() => { setPane(null); setMenu(false); }}
        title={job ? job.svc : ''} sub={job ? `#${job.id} · ${job.cli} · ${job.donde || ''}` : ''}
        footer={job && (
          <div style={{ display:'flex', gap:10, width:'100%', position:'relative' }}>
            {menu && <AccMenu job={job} patron={cfg.patron} onAccion={accion} onRelevar={relevar} onPasar={() => { setMenu(false); setReag(true); }} onClose={() => setMenu(false)} />}
            {/* Mismo paso de motivo que la mini-app: sin esto el panel asumía
                que la visita nunca pasó y borraba el relevamiento. */}
            {reag && (
              <div style={{ position:'absolute', bottom:'calc(100% + 10px)', left:0, right:0, zIndex:12, background:T.g900, border:`1px solid ${T.g700}`, borderRadius:14, boxShadow:'0 18px 48px rgba(0,0,0,.55)', overflow:'hidden' }}>
                <div style={{ padding:'13px 16px 10px', fontFamily:T.mono, fontSize:9, letterSpacing:'.13em', textTransform:'uppercase', color:T.c500 }}>¿Por qué la pasás?</div>
                {MOTIVOS.map(x => (
                  <button key={x.k} onClick={() => reagendar(x.k)} style={{ display:'flex', flexDirection:'column', alignItems:'flex-start', gap:2, width:'100%', padding:'11px 16px', borderTop:`1px solid ${T.g800}`, background:'transparent', border:'none', cursor:'pointer', textAlign:'left' }}>
                    <span style={{ fontFamily:T.sans, fontSize:13.5, fontWeight:600, color:T.c100 }}>{x.t}</span>
                    <span style={{ fontFamily:T.sans, fontSize:11.5, lineHeight:1.45, color:T.c500 }}>{x.d}</span>
                  </button>
                ))}
                <button onClick={() => setReag(false)} style={{ width:'100%', minHeight:40, borderTop:`1px solid ${T.g800}`, background:'transparent', border:'none', color:T.c500, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer' }}>Cancelar</button>
              </div>
            )}
            {esCierre(job.tsm)
              ? <PBtn wide on={() => setPane(null)}>Cerrar</PBtn>
              : job.dia === 'PRESUPUESTO'
                ? (job.enviado
                    ? <PBtn wide tone="pri" on={() => avisar('notificationOccurred · success', `Mia le escribió a ${job.cli.split(' ')[0]}`)}>Insistirle a {job.cli.split(' ')[0]}</PBtn>
                    : <PBtn wide tone="pri" on={() => enviarPres(job.id)}>Mandar el presupuesto</PBtn>)
                : <><PBtn wide tone="pri" on={() => setMenu(v => !v)}>Avisarle algo a {job.cli.split(' ')[0]}</PBtn></>}
          </div>
        )}>
        {job && <DrawerJob job={job} cfg={cfg} onPaso={onPaso} onNota={onNota} onFoto={onFoto} onDoc={t => abrir('doc', { id:job.id, tipo:t })} />}
      </Drawer>

      {tour && vista === 'configuracion' && (
        <div ref={tip} style={{ position:'absolute', zIndex:5, width:272, left:pos.left, top:pos.top,
          background:TOUR_OFF.bg, color:TOUR_OFF.ink, border:'1px solid rgba(255,255,255,.14)', borderRadius:14, padding:'14px 16px', boxShadow:'0 18px 48px rgba(0,0,0,.55)' }}>
          <div style={{ fontFamily:T.mono, fontSize:9, letterSpacing:'.13em', textTransform:'uppercase', color:TOUR_OFF.accent, marginBottom:6 }}>{paso + 1} de {total}</div>
          <div style={{ fontSize:15, fontWeight:700, marginBottom:6 }}>{p.t}</div>
          <div style={{ fontSize:13, lineHeight:1.5, color:TOUR_OFF.mut, marginBottom:14 }}>{p.x}</div>
          <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8 }}>
            <button onClick={() => setPaso(Math.max(0, paso - 1))} style={{ visibility:paso?'visible':'hidden', background:'transparent', border:'none', color:TOUR_OFF.mut, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer', padding:'8px 4px' }}>← Atrás</button>
            <button onClick={() => paso === total - 1 ? setTour(false) : setPaso(paso + 1)} style={{ background:TOUR_OFF.accent, color:'#fff', border:'none', borderRadius:9, fontFamily:T.sans, fontSize:12.5, fontWeight:600, cursor:'pointer', padding:'9px 14px' }}>{paso === total - 1 ? 'Listo ✓' : 'Siguiente →'}</button>
          </div>
        </div>
      )}
    </div>
  );
}
Object.assign(window, { PanelWeb, TOUR_OFF, NAV_WEB });
