// ═══ GENESIS BUILD rbac-logbook-bridge · 2026-07-10 ═══
// ═══ Logbook Operacional · registro (direita) + frases rápidas (centro) ══════
const _logFmtHora = (iso) => { const d = new Date(iso); return isNaN(d) ? '--:--' : d.toLocaleTimeString('pt-BR', {hour:'2-digit', minute:'2-digit'}); };
const _logIsoToLocal = (iso) => { if (!iso) return ''; const d = new Date(iso); if (isNaN(d)) return ''; return new Date(d.getTime() - d.getTimezoneOffset()*60000).toISOString().slice(0,16); };
// padrão de botão do logbook: caixa de quinas arredondadas, altura 34, laranja, Fraunces branca
const _logBtnPri = { height:34, padding:'0 16px', borderRadius:9, border:'none', background:'var(--accent)', color:'#fff', fontFamily:'var(--fs)', fontSize:14.5, cursor:'pointer', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:7, boxSizing:'border-box', flexShrink:0 };
const _logCorHex = (c) => ({verde:'var(--leaf)', cinza:'var(--muted)', amarelo:'var(--sun)', vermelho:'var(--rose)', azul:'var(--sky)'}[c] || null);
const _logHojeStr = () => { const d = new Date(); return new Date(d.getTime() - d.getTimezoneOffset()*60000).toISOString().slice(0,10); };
const _logDataLonga = (str) => { const d = new Date(str + 'T12:00'); return isNaN(d) ? str : d.toLocaleDateString('pt-BR', {weekday:'long', day:'2-digit', month:'long'}); };
const _LOG_CORES = [ {v:null, nome:'sem cor'}, {v:'verde', nome:'normal'}, {v:'azul', nome:'info'}, {v:'cinza', nome:'desligado'}, {v:'amarelo', nome:'manutenção'}, {v:'vermelho', nome:'avaria'} ];
const _LOG_COR_RAW = { verde:'#5C7E4F', cinza:'#6B6258', amarelo:'#C68A1F', vermelho:'#B14E5C', azul:'#5C8DAE' };
// modo noturno (night vision): redefine os tokens Tidal pra tons escuros avermelhados
const _LOG_TEMA_NOTURNO = {
  '--cream':'#04070A', '--cream-2':'#0A1014', '--cream-3':'#141C22', '--surface':'#0B1116', '--surface-2':'#080D11',
  '--ink':'#D9E5DC', '--ink-2':'#B9C8BF', '--muted':'#7E9A8B', '--muted-2':'#54695D', '--muted-3':'#2C3B33',
  '--line':'rgba(150,235,165,0.18)', '--line-soft':'rgba(150,235,165,0.10)', '--line-strong':'rgba(150,235,165,0.30)',
  '--accent':'#8FE87B', '--accent-2':'#5FBF4E', '--accent-soft':'rgba(143,232,123,0.14)',
  '--teal':'#5AD8CB', '--sky':'#6EC1FF',
  '--leaf':'#7DF77D', '--sun':'#FFD65C', '--rose':'#FF6B6B',
  '--shadow-sm':'none', '--shadow-md':'0 0 0 1px rgba(150,235,165,0.08)',
};
const _thLog = {padding:'7px 10px',fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:0.5,color:'var(--muted)',textTransform:'uppercase',textAlign:'center',borderRight:'1px solid var(--cream-3)',borderBottom:'1px solid var(--cream-3)'};
const _tdLog = {padding:'7px 10px',borderRight:'1px solid var(--cream-3)',color:'var(--ink)',verticalAlign:'top',fontSize:13};

// Config de apresentação dos gráficos meteo (topo do eixo Y + intervalo da linha cinza), por gráfico. Persiste em localStorage.
const _LOG_GRAPH_DEFAULTS = { vento:{topo:40,passo:20}, corrente:{topo:4,passo:2}, swell:{topo:4,passo:2} };
const _logGraphCfg = () => { try { const s = JSON.parse(localStorage.getItem('genesis_graph_cfg')||'null'); if(!s) return JSON.parse(JSON.stringify(_LOG_GRAPH_DEFAULTS)); return { vento:{..._LOG_GRAPH_DEFAULTS.vento,...(s.vento||{})}, corrente:{..._LOG_GRAPH_DEFAULTS.corrente,...(s.corrente||{})}, swell:{..._LOG_GRAPH_DEFAULTS.swell,...(s.swell||{})} }; } catch(_) { return JSON.parse(JSON.stringify(_LOG_GRAPH_DEFAULTS)); } };
const _logSaveGraphCfg = (cfg) => { try { localStorage.setItem('genesis_graph_cfg', JSON.stringify(cfg)); } catch(_){} };

// ─── Grid do painel (v2): widgets livres estilo home de celular ─────────────
// Cada widget: {id, on, x(0-2), y, w(1-3), h(1+)}. 3 colunas; linhas de altura fixa.
const _LOG_WIDGETS = {
  log:       { nome: 'Registro do dia', fixo: true, minW: 5 },
  meteo3:    { nome: 'Condições ambientais', minW: 3 },
  frases:    { nome: 'Frases rápidas', minW: 3 },
  dp:        { nome: 'Planta de força (unifilar)', minW: 4 },
  refsens:   { nome: 'Posicionamento & sensores', minW: 3 },
  asog:      { nome: 'ASOG (matriz operacional)', minW: 3 },
};
const _LOG_LAYOUT_DEFAULT = { v: 3, widgets: [
  { id: 'dp',        on: true,  x: 0, y: 0,   w: 12, h: 30 },
  { id: 'meteo3',    on: true,  x: 0, y: 30,  w: 4,  h: 68 },
  { id: 'log',       on: true,  x: 4, y: 30,  w: 8,  h: 102 },
  { id: 'frases',    on: true,  x: 0, y: 98,  w: 4,  h: 34 },
  { id: 'asog',      on: false, x: 0, y: 0,   w: 5,  h: 34 },
  { id: 'refsens',   on: false, x: 0, y: 0,   w: 3,  h: 50 },
]};
const _gridColide = (a, b) => a.id !== b.id && a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
// Arruma o grid: quem colide com o prioritário desce; depois todo mundo compacta pra cima.
const _gridArrumar = (widgets, priorityId) => {
  const vis = widgets.filter(w => w.on);
  const ordem = vis.slice().sort((a, b) => (a.id === priorityId ? -1 : b.id === priorityId ? 1 : (a.y - b.y) || (a.x - b.x)));
  const placed = [];
  for (const w of ordem) {
    while (placed.some(p => _gridColide(p, w))) w.y++;
    if (w.id !== priorityId) { while (w.y > 0) { const t = { ...w, y: w.y - 1 }; if (placed.some(p => _gridColide(p, t))) break; w.y--; } }
    placed.push(w);
  }
  return widgets;
};
const _gridCompactarTudo = (widgets) => {
  const vis = widgets.filter(w => w.on).sort((a, b) => (a.y - b.y) || (a.x - b.x));
  const placed = [];
  for (const w of vis) {
    while (placed.some(p => _gridColide(p, w))) w.y++;
    while (w.y > 0) { const t = { ...w, y: w.y - 1 }; if (placed.some(p => _gridColide(p, t))) break; w.y--; }
    placed.push(w);
  }
  return widgets;
};
// v3: 12 colunas de largura · linhas de 8px (snap fino = manuseio "livre" com alinhamento exato)
const _logSanitizarW = (w) => {
  const minW = (_LOG_WIDGETS[w.id] || {}).minW || 2;
  const larg = Math.min(12, Math.max(minW, parseInt(w.w) || minW));
  return { id: w.id, on: _LOG_WIDGETS[w.id] && _LOG_WIDGETS[w.id].fixo ? true : !!w.on,
    w: larg, h: Math.min(240, Math.max(6, parseInt(w.h) || 6)),
    x: Math.min(12 - larg, Math.max(0, parseInt(w.x) || 0)), y: Math.max(0, parseInt(w.y) || 0) };
};
const _logMigrarV1 = (v1) => {
  const out = []; let y = 0;
  v1.filter(w => w.pos === 'topo').forEach(w => { out.push({ id: w.id, on: !!w.on, x: 0, y: y++, w: 3, h: 1 }); });
  const yTop = y; let yl = y;
  v1.filter(w => w.pos === 'lado').forEach(w => {
    out.push({ id: w.id, on: !!w.on, x: 0, y: yl++, w: 1, h: 1 });
    if (w.id === 'horas' && w.on && !out.some(o => o.id === 'meteo3')) out.push({ id: 'meteo3', on: true, x: 0, y: yl, w: 1, h: 2 }), yl += 2;
  });
  let ym = yTop;
  v1.filter(w => w.pos === 'principal').forEach(w => { out.push({ id: w.id, on: !!w.on, x: 1, y: ym, w: 2, h: Math.max(2, yl - yTop) }); ym += 2; });
  return out;
};
const _logSanitizarLayout = (obj) => {
  let ws = null, escalar = false;
  if (Array.isArray(obj)) { ws = _logMigrarV1(obj); escalar = true; }                       // v1 (fase 1)
  else if (obj && obj.v === 2 && Array.isArray(obj.widgets)) { ws = obj.widgets; escalar = true; }  // v2 (células gordas)
  else if (obj && obj.v === 3 && Array.isArray(obj.widgets)) ws = obj.widgets;
  if (!ws) return null;
  if (escalar) ws = ws.map(w => ({ ...w, x: (parseInt(w.x) || 0) * 4, w: (parseInt(w.w) || 1) * 4, y: (parseInt(w.y) || 0) * 36, h: (parseInt(w.h) || 1) * 36 })); // 3→12 col · célula 288px→36 linhas de 8px
  const valid = ws.filter(w => w && _LOG_WIDGETS[w.id]).map(_logSanitizarW);
  const vistos = new Set(valid.map(w => w.id));
  for (const d of _LOG_LAYOUT_DEFAULT.widgets) if (!vistos.has(d.id)) valid.push({ ...d, on: d.id === 'log' ? true : false });
  const dedup = []; const ids = new Set();
  for (const w of valid) if (!ids.has(w.id)) { ids.add(w.id); dedup.push(w); }
  _gridCompactarTudo(dedup);
  return { v: 3, ts: (obj && obj.ts) || 0, widgets: dedup };
};
const _logLayoutCfg = () => {
  try {
    const s = JSON.parse(localStorage.getItem('genesis_logbook_layout') || 'null');
    return _logSanitizarLayout(s) || JSON.parse(JSON.stringify(_LOG_LAYOUT_DEFAULT));
  } catch (_) { return JSON.parse(JSON.stringify(_LOG_LAYOUT_DEFAULT)); }
};
const _logSaveLayoutCfg = (cfg) => { try { localStorage.setItem('genesis_logbook_layout', JSON.stringify(cfg)); } catch(_){} };

// Cache compartilhado dos horários do dia (evita fetch duplicado entre widgets de gráfico)
const _logHorariosCache = {};
const _logFetchHorarios = (data, token) => {
  if (!_logHorariosCache[data]) {
    _logHorariosCache[data] = fetch(`/api/logbook/horarios?data=${data}`, { headers: { 'Authorization': `Bearer ${token()}` } }).then(r => r.ok ? r.json() : []).catch(() => []);
  }
  return _logHorariosCache[data];
};
const _logInvalidarHorarios = (data) => { delete _logHorariosCache[data]; try { window.dispatchEvent(new CustomEvent('log-horarios-changed', { detail: { data } })); } catch (_) {} };

// frase-template: slots {rótulo} preenchidos antes de registrar
// DSL: {distância:1-1000} → slider numérico · {rumo:0-360} · {vel:0.1-2.0} (decimal) · {lado:BB|BE|meia-nau} → chips
const _logSlots = (texto) => (String(texto||'').match(/\{[^}]*\}/g) || []).map((s, i) => {
  const cru = s.slice(1, -1).trim();
  const di = cru.lastIndexOf(':');
  if (di > 0) {
    const spec = cru.slice(di + 1).trim();
    const label = cru.slice(0, di).trim() || `campo ${i+1}`;
    const mRange = spec.match(/^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)$/);
    if (mRange) {
      const min = parseFloat(mRange[1]), max = parseFloat(mRange[2]);
      if (!isNaN(min) && !isNaN(max) && max > min) {
        const dec = Math.max((mRange[1].split('.')[1]||'').length, (mRange[2].split('.')[1]||'').length);
        return { raw:s, label, tipo:'range', min, max, step: dec ? parseFloat((1/Math.pow(10,dec)).toFixed(dec)) : 1, dec };
      }
    }
    if (spec.includes('|')) {
      const opcoes = spec.split('|').map(x=>x.trim()).filter(Boolean);
      if (opcoes.length >= 2) return { raw:s, label, tipo:'opcoes', opcoes };
    }
  }
  return { raw:s, label: cru || `campo ${i+1}`, tipo:'texto' };
});
const _logPreenche = (texto, vals) => { let i = 0; return String(texto||'').replace(/\{[^}]*\}/g, () => (vals[i++] || '____')); };

// ══ DPR Genesis — relatório diário cliente-facing (corporativo, sóbrio) ══
const _dprCss = () => `
  *{box-sizing:border-box;margin:0;padding:0}
  body{font-family:'Inter',-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#16232B;background:#fff;font-size:12.5px;line-height:1.5;padding:34px 40px}
  .mono{font-family:'JetBrains Mono',ui-monospace,Menlo,monospace}
  .dhead{border-bottom:3px solid #16324A;padding-bottom:14px;margin-bottom:16px;display:flex;justify-content:space-between;align-items:flex-end;gap:20px}
  .dhead h1{font-size:19px;letter-spacing:-0.3px;color:#16324A}
  .dhead .sub{font-family:'JetBrains Mono',monospace;font-size:9.5px;letter-spacing:2px;text-transform:uppercase;color:#5E6E7A;margin-top:3px}
  .dhead .num{font-family:'JetBrains Mono',monospace;font-size:11px;color:#16324A;text-align:right;white-space:nowrap}
  .dmeta{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #D4DCE2;border-radius:6px;overflow:hidden;margin-bottom:18px}
  .dmeta>div{padding:8px 12px;border-right:1px solid #E4EAEE}
  .dmeta>div:last-child{border-right:none}
  .dmeta span{display:block;font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:1.5px;text-transform:uppercase;color:#7C8A94}
  .dmeta b{font-size:12.5px;font-weight:600;color:#16232B}
  h2.dsec{font-size:11px;font-family:'JetBrains Mono',monospace;letter-spacing:2px;text-transform:uppercase;color:#16324A;border-bottom:1px solid #D4DCE2;padding-bottom:5px;margin:20px 0 10px}
  .dresumo{background:#F4F7F9;border-left:3px solid #16324A;border-radius:0 6px 6px 0;padding:12px 15px;font-size:13px;line-height:1.65;white-space:pre-wrap}
  .dstrip{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:14px 0}
  .dstrip>div{border:1px solid #D4DCE2;border-radius:6px;padding:10px 12px;text-align:center}
  .dstrip b{display:block;font-size:20px;font-weight:700;color:#16324A;font-family:'JetBrains Mono',monospace}
  .dstrip span{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:1.2px;text-transform:uppercase;color:#7C8A94}
  table.dt{width:100%;border-collapse:collapse;font-size:11.5px}
  table.dt th{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:1.2px;text-transform:uppercase;color:#5E6E7A;text-align:left;padding:6px 8px;border-bottom:1.5px solid #16324A;background:#F4F7F9}
  table.dt td{padding:6px 8px;border-bottom:1px solid #E9EEF1;vertical-align:top}
  table.dt tr:nth-child(even) td{background:#FAFBFC}
  .dfoot{margin-top:26px;padding-top:10px;border-top:1px solid #D4DCE2;display:flex;justify-content:space-between;font-family:'JetBrains Mono',monospace;font-size:8.5px;color:#7C8A94}
  .dsign{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-top:34px}
  .dsign>div{border-top:1px solid #16232B;padding-top:6px;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:1px;text-transform:uppercase;color:#5E6E7A;text-align:center}
  .dsign b{display:block;font-family:'Inter',sans-serif;font-size:11.5px;letter-spacing:0;text-transform:none;color:#16232B;margin-bottom:2px}
  @media print { body{padding:16px 20px} .dsec{page-break-after:avoid} table.dt{page-break-inside:auto} tr{page-break-inside:avoid} }
`;
const _dprHead = (cfg, data, deServico) => {
  const dt = new Date(data + 'T12:00');
  const num = `DPR-${data.replace(/-/g,'')}`;
  return `<div class="dhead">
    <div><h1>Daily Progress Report</h1><div class="sub">${_pvEsc(cfg.navio || 'MPSV Genesis I')} · Marine Operations</div></div>
    <div class="num">${num}<br>${_pvEsc(dt.toLocaleDateString('pt-BR', {weekday:'long', day:'2-digit', month:'long', year:'numeric'}))}</div>
  </div>
  <div class="dmeta">
    <div><span>Vessel</span><b>${_pvEsc(cfg.navio || 'MPSV Genesis I')}</b></div>
    <div><span>Client</span><b>${_pvEsc(cfg.cliente || '—')}</b></div>
    <div><span>Field / Project</span><b>${_pvEsc(cfg.campo || '—')}</b></div>
    <div><span>Period</span><b class="mono" style="font-size:11px">00:00 – 24:00</b></div>
  </div>`;
};
const _dprResumoExec = (resumo) => resumo ? `<h2 class="dsec">1 · Executive Summary</h2><div class="dresumo">${_pvEsc(resumo)}</div>` : '';
const _dprStrip = (lista, npts, pob, movDia) => {
  const nptMin = npts.reduce((acc, e) => { const i = new Date(e.inicio), f = e.fim ? new Date(e.fim) : new Date(); return acc + Math.max(0, (f - i) / 60000); }, 0);
  const nptH = Math.floor(nptMin / 60), nptM = Math.round(nptMin % 60);
  return `<div class="dstrip">
    <div><b>${lista.filter(e=>!e.cancelada).length}</b><span>Log entries</span></div>
    <div><b>${npts.length ? `${nptH}h${String(nptM).padStart(2,'0')}` : '0h00'}</b><span>Downtime (NPT)</span></div>
    <div><b>${pob != null ? pob : '—'}</b><span>POB</span></div>
    <div><b>${movDia != null ? movDia : '—'}</b><span>Pers. transfers</span></div>
  </div>`;
};
const _dprLog = (lista) => {
  const linhas = lista.filter(e => !e.cancelada).map(e => `<tr>
    <td class="mono" style="width:52px;font-weight:600">${_logFmtHora(e.ts)}</td>
    <td>${_pvEsc(e.texto)}</td>
    <td style="width:110px;color:#5E6E7A">${_pvEsc(e.autor_nome || '—')}</td>
  </tr>`).join('');
  return `<h2 class="dsec">2 · Operations Log</h2><table class="dt"><thead><tr><th>Time</th><th>Event</th><th>By</th></tr></thead><tbody>${linhas}</tbody></table>`;
};
const _dprNpt = (npts) => {
  if (!npts.length) return `<h2 class="dsec">3 · Downtime / NPT</h2><div style="font-size:11.5px;color:#5E6E7A;padding:6px 2px">No non-productive time recorded this day.</div>`;
  const linhas = npts.map(e => {
    const i = new Date(e.inicio), f = e.fim ? new Date(e.fim) : null;
    const durMin = Math.max(0, ((f || new Date()) - i) / 60000);
    return `<tr>
      <td class="mono" style="width:100px">${_logFmtHora(e.inicio)}–${f ? _logFmtHora(e.fim) : 'ongoing'}</td>
      <td class="mono" style="width:64px;font-weight:600">${Math.floor(durMin/60)}h${String(Math.round(durMin%60)).padStart(2,'0')}</td>
      <td><b>${_pvEsc(e.causa_nome || 'Unclassified')}</b>${e.motivo ? ` — ${_pvEsc(e.motivo)}` : ''}${e.operacao_paralela ? `<div style="font-size:10px;color:#1F5755;margin-top:2px">✔ parallel ops maintained: ${_pvEsc(e.operacao_paralela)}</div>` : ''}</td>
    </tr>`;
  }).join('');
  return `<h2 class="dsec">3 · Downtime / NPT</h2><table class="dt"><thead><tr><th>Period</th><th>Duration</th><th>Cause & remarks</th></tr></thead><tbody>${linhas}</tbody></table>`;
};
const _dprMeteo = (horarios, graphCfg) => {
  const cheios = (horarios || []).filter(r => r.preenchido);
  if (!cheios.length) return '';
  const cfg = graphCfg || { vento: {topo:30, passo:10}, corrente: {topo:2, passo:0.5}, swell: {topo:3, passo:1} };
  const ventoPts = cheios.filter(r => r.vento_int != null).map(r => ({h:r.hora, v:r.vento_int, dir:r.vento_dir}));
  const corrPts = cheios.filter(r => r.corrente_int != null).map(r => ({h:r.hora, v:r.corrente_int, dir:r.corrente_dir}));
  const swellPts = cheios.filter(r => r.swell_int != null).map(r => ({h:r.hora, v:r.swell_int, dir:r.swell_dir}));
  const graficos = `<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px">${_pvGraficoDir(ventoPts,'#5C8DAE','Wind',"'",cfg.vento.topo,cfg.vento.passo)}${_pvGraficoDir(corrPts,'#1F5755','Current',"'",cfg.corrente.topo,cfg.corrente.passo)}${_pvGraficoDir(swellPts,'#5C7E4F','Swell','m',cfg.swell.topo,cfg.swell.passo)}</div>`;
  const _mkGraficos = graficos;
  const linhas = cheios.map(r => `<tr>
    <td class="mono" style="font-weight:600">${String(r.hora).padStart(2,'0')}:00</td>
    <td class="mono">${r.vento_int != null ? `${r.vento_int} kt${r.vento_dir != null ? ` / ${r.vento_dir}°` : ''}` : '—'}</td>
    <td class="mono">${r.corrente_int != null ? `${r.corrente_int} kt${r.corrente_dir != null ? ` / ${r.corrente_dir}°` : ''}` : '—'}</td>
    <td class="mono">${r.swell_int != null ? `${r.swell_int} m${r.swell_dir != null ? ` / ${r.swell_dir}°` : ''}` : '—'}</td>
  </tr>`).join('');
  return `<h2 class="dsec">4 · Environmental Conditions</h2>${_mkGraficos}<table class="dt"><thead><tr><th>Hour</th><th>Wind</th><th>Current</th><th>Swell</th></tr></thead><tbody>${linhas}</tbody></table>`;
};
const _dprGaleria = (lista) => {
  const comFoto = (lista || []).filter(e => e.foto_url && !e.cancelada);
  if (!comFoto.length) return '';
  const itens = comFoto.map(e => `<div style="border:1px solid #D4DCE2;border-radius:6px;overflow:hidden;page-break-inside:avoid">
    <img src="${_pvEsc(e.foto_url)}" alt="" style="width:100%;height:190px;object-fit:cover;display:block"/>
    <div style="padding:7px 10px">
      <div class="mono" style="font-size:9px;color:#5E6E7A;text-transform:uppercase;letter-spacing:1px">${_logFmtHora(e.ts)} · ${_pvEsc(e.autor_nome || '—')}</div>
      <div style="font-size:11px;margin-top:2px">${_pvEsc(e.texto)}</div>
    </div>
  </div>`).join('');
  return `<h2 class="dsec">5 · Photographic Record</h2><div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">${itens}</div>`;
};
const _dprAssin = () => `<div class="dsign">
  <div><b>&nbsp;</b>Prepared by (SDPO/OOW)</div>
  <div><b>&nbsp;</b>Master</div>
  <div><b>&nbsp;</b>Client Representative</div>
</div>`;

// ══ Pavão — exportação rica do dia (1a: capa + resumo + log + assinaturas) ══
const _pvEsc = (s) => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const _pvCss = () => `
  *{box-sizing:border-box;margin:0;padding:0}
  body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1F2A2E;background:#fff;font-size:13px;line-height:1.5}
  .mono{font-family:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace}
  .cover{background:linear-gradient(135deg,#1F5755 0%,#1F2A2E 100%);color:#fff;padding:46px 46px 40px}
  .cover .brand{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:#C9B79C}
  .cover h1{font-family:'Fraunces',Georgia,'Times New Roman',serif;font-size:34px;font-weight:600;margin:10px 0 3px;letter-spacing:-.5px}
  .cover .op{font-family:'Fraunces',Georgia,serif;font-style:italic;font-size:15px;color:#E7C9A0}
  .cover .meta{margin-top:24px;display:flex;gap:30px;flex-wrap:wrap}
  .cover .meta span{display:block;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#9FB8B0;margin-bottom:3px}
  .cover .meta b{font-weight:500;font-size:14px}
  .wrap{padding:2px 46px}
  .section-t{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#6B6258;margin:26px 0 12px;padding-bottom:6px;border-bottom:1px solid #ECE3D0}
  .kpis{display:flex;gap:10px;flex-wrap:wrap}
  .kpi{flex:1;min-width:92px;border:1px solid #ECE3D0;border-radius:12px;padding:12px 14px}
  .kpi .n{font-family:'Fraunces',Georgia,serif;font-size:25px;font-weight:600;line-height:1}
  .kpi .l{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:1px;text-transform:uppercase;color:#6B6258;margin-top:6px}
  table{width:100%;border-collapse:collapse}
  th{text-align:left;font-family:'JetBrains Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#6B6258;border-bottom:2px solid #ECE3D0;padding:7px 8px}
  td{padding:8px;border-bottom:1px solid #F0E9DA;vertical-align:top;font-size:12.5px}
  td.h{font-family:'JetBrains Mono',monospace;font-weight:700;white-space:nowrap;width:44px;border-left:3px solid transparent}
  td.a{color:#6B6258;font-size:10.5px;white-space:nowrap;text-align:right}
  tr{page-break-inside:avoid}
  .canc{text-decoration:line-through;color:#B14E5C}
  .cnote{font-family:'JetBrains Mono',monospace;font-size:9px;color:#B14E5C;margin-top:2px}
  .enote{font-family:'JetBrains Mono',monospace;font-size:9px;color:#5C8DAE;margin-top:2px}
  .clip{font-family:'JetBrains Mono',monospace;font-size:9px;color:#6B6258;margin-top:2px}
  .sign{display:flex;gap:16px;flex-wrap:wrap}
  .sign .card{flex:1;min-width:190px;border:1px solid #ECE3D0;border-radius:10px;padding:11px 13px}
  .foot{margin:32px 46px 0;color:#9a8f80;font-size:9.5px;font-family:'JetBrains Mono',monospace;border-top:1px solid #ECE3D0;padding:10px 0 30px}
  .gbreak{page-break-before:auto}
  .gal{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .gcard{border:1px solid #ECE3D0;border-radius:12px;overflow:hidden;page-break-inside:avoid}
  .gcard img{width:100%;height:200px;object-fit:cover;display:block;background:#F5EFE3}
  .gcap{padding:9px 12px}
  .gcap .gh{font-family:'JetBrains Mono',monospace;font-size:9px;color:#6B6258;text-transform:uppercase;letter-spacing:.5px;display:flex;align-items:center;gap:5px}
  .gcap .gdot{width:8px;height:8px;border-radius:50%;display:inline-block}
  .gcap .gtx{font-size:12px;margin-top:4px}
  @page{margin:12mm;size:A4}
  @media print{.cover,.kpi{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
`;
const _pvCapa = (data, deServico) => {
  const d = new Date(data + 'T12:00:00');
  const dow = isNaN(d) ? '' : d.toLocaleDateString('pt-BR', {weekday:'long'});
  return `<div class="cover">
    <div class="brand">Portal Genesis</div>
    <h1>MPSV Genesis I</h1>
    <div class="op">Operational Logbook — Diário de Operações</div>
    <div class="meta">
      <div><span>{lang==='pt'?'Data':'Date'}</span><b class="mono">${_pvEsc(data)}</b></div>
      <div><span>Dia</span><b style="text-transform:capitalize">${_pvEsc(dow)}</b></div>
      ${deServico ? `<div><span>{lang==='pt'?'De serviço':'On watch'}</span><b>${_pvEsc(deServico)}</b></div>` : ''}
    </div>
  </div>`;
};
const _pvResumo = (lista, npts) => {
  lista = lista.filter(e => !e.cancelada); // removidas não aparecem no relatório
  const ativos = lista;
  const fotos = ativos.filter(e => e.foto_url).length;
  const avarias = ativos.filter(e => e.cor === 'vermelho').length;
  const autores = [...new Set(ativos.map(e => e.autor_nome).filter(Boolean))].length;
  const horas = ativos.map(e => _logFmtHora(e.ts)).sort();
  const periodo = horas.length ? `${horas[0]}–${horas[horas.length-1]}` : '—';
  const nptMin = (npts || []).reduce((a, e) => a + (Number(e.duracao_min) || 0), 0);
  const nptTxt = nptMin ? `${Math.floor(nptMin/60)}h${String(Math.round(nptMin%60)).padStart(2,'0')}` : '0h';
  const kpi = (n,l,mono,cor) => `<div class="kpi"><div class="n" ${mono?`style="font-size:16px;font-family:'JetBrains Mono',monospace${cor?`;color:${cor}`:''}"`:(cor?`style="color:${cor}"`:'')}>${n}</div><div class="l">${l}</div></div>`;
  return `<div class="wrap"><div class="section-t">Resumo do dia</div><div class="kpis">
    ${kpi(ativos.length,'Registros')}${kpi(periodo,'Período',true)}${kpi(autores,'Operadores')}${kpi(fotos,'Fotos')}${kpi(avarias,'Avarias',false,avarias?'#B14E5C':'')}${kpi(nptTxt,'NPT',true,nptMin?'#C68A1F':'')}
  </div></div>`;
};
const _pvRosaSVG = (vento, corrente, swell) => {
  const cx=80, cy=80, r=62;
  const itens = [
    (vento && vento.dir!=null && vento.int!=null) ? {dir:vento.dir, int:vento.int, max:35, cor:'#5C8DAE'} : null,
    (corrente && corrente.dir!=null && corrente.int!=null) ? {dir:corrente.dir, int:corrente.int, max:3, cor:'#1F5755'} : null,
    (swell && swell.dir!=null && swell.int!=null) ? {dir:swell.dir, int:swell.int, max:5, cor:'#5C7E4F'} : null,
  ].filter(Boolean);
  const ponta=(dir,frac)=>{ const L=r*Math.max(0.16,Math.min(1,frac)); const rad=dir*Math.PI/180; return {x:(cx+L*Math.sin(rad)).toFixed(1), y:(cy-L*Math.cos(rad)).toFixed(1)}; };
  let vet='';
  itens.forEach(it=>{ const p=ponta(it.dir,it.int/it.max); vet+=`<line x1="${cx}" y1="${cy}" x2="${p.x}" y2="${p.y}" stroke="${it.cor}" stroke-width="3" stroke-linecap="round"/><circle cx="${p.x}" cy="${p.y}" r="4" fill="${it.cor}"/>`; });
  const card=[['N',cx,cy-r-5],['E',cx+r+8,cy+4],['S',cx,cy+r+14],['W',cx-r-9,cy+4]];
  const labels=card.map(([c,x,y])=>`<text x="${x}" y="${y}" text-anchor="middle" font-family="monospace" font-size="10" fill="#6B6258">${c}</text>`).join('');
  return `<svg width="164" height="164" viewBox="0 0 160 160">
    <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#ECE3D0" stroke-width="1.5"/>
    <circle cx="${cx}" cy="${cy}" r="${(r*0.5).toFixed(0)}" fill="none" stroke="#ECE3D0" stroke-width="1"/>
    <line x1="${cx}" y1="${cy-r}" x2="${cx}" y2="${cy+r}" stroke="#ECE3D0"/>
    <line x1="${cx-r}" y1="${cy}" x2="${cx+r}" y2="${cy}" stroke="#ECE3D0"/>
    ${labels}${vet}<circle cx="${cx}" cy="${cy}" r="3" fill="#6B6258"/>
  </svg>`;
};
const _pvGrafico = (pontos, cor, titulo, unidade) => {
  const w=176, h=62, padL=4, padB=13, padT=6;
  const ord=(pontos||[]).slice().sort((a,b)=>a.h-b.h);
  if(!ord.length) return `<div style="flex:1;min-width:150px;border:1px solid #ECE3D0;border-radius:10px;padding:8px 10px"><span style="font-family:monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#6B6258">${titulo}</span><div style="font-family:monospace;font-size:10px;color:#9a8f80;padding:10px 0">{lang==='pt'?'sem dados':'no data'}</div></div>`;
  const vs=ord.map(p=>p.v); const min=Math.min(...vs), max=Math.max(...vs), span=(max-min)||1;
  const X=(hh)=>padL+(hh/23)*(w-padL-4);
  const Y=(v)=>padT+(1-(v-min)/span)*(h-padT-padB);
  const dl=ord.map((p,i)=>(i?'L':'M')+X(p.h).toFixed(1)+' '+Y(p.v).toFixed(1)).join(' ');
  const dots=ord.map(p=>`<circle cx="${X(p.h).toFixed(1)}" cy="${Y(p.v).toFixed(1)}" r="1.6" fill="${cor}"/>`).join('');
  const ticks=[0,6,12,18,23].map(hh=>`<text x="${X(hh).toFixed(1)}" y="${h-2}" text-anchor="middle" font-family="monospace" font-size="7" fill="#9a8f80">${String(hh).padStart(2,'0')}</text>`).join('');
  return `<div style="flex:1;min-width:150px;border:1px solid #ECE3D0;border-radius:10px;padding:8px 10px">
    <div style="display:flex;justify-content:space-between;align-items:baseline">
      <span style="font-family:monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#6B6258">${titulo}</span>
      <span style="font-family:'Fraunces',Georgia,serif;font-size:16px;color:${cor}">${ord[ord.length-1].v}<span style="font-size:8.5px;color:#6B6258"> ${unidade} · máx ${max}</span></span>
    </div>
    <svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" style="margin-top:2px;max-width:100%">
      <path d="${dl}" fill="none" stroke="${cor}" stroke-width="1.5" stroke-linejoin="round"/>${dots}${ticks}
    </svg>
  </div>`;
};
const _pvNpt = (npts) => {
  if (!npts || !npts.length) return '';
  const fmtDur = (min) => { const m=Math.round(Number(min)||0); return m>=60 ? `${Math.floor(m/60)}h${String(m%60).padStart(2,'0')}` : `${m} min`; };
  const fmtHora = (iso) => { const d=new Date(iso); return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0'); };
  const ord = npts.slice().sort((a,b)=>new Date(a.inicio)-new Date(b.inicio));
  const totalMin = npts.reduce((a,e)=>a+(Number(e.duracao_min)||0),0);
  const totalTxt = totalMin>=60 ? `${Math.floor(totalMin/60)}h${String(Math.round(totalMin%60)).padStart(2,'0')}` : `${Math.round(totalMin)} min`;
  const parciais = npts.filter(e=>e.operacao_paralela).length;
  const rows = ord.map(e => {
    const parcial = e.operacao_paralela
      ? `<div style="display:inline-flex;align-items:center;gap:5px;margin-top:5px;font-family:'JetBrains Mono',monospace;font-size:9px;color:#1F5755;background:rgba(31,87,85,0.08);border:1px solid #1F5755;border-radius:999px;padding:2px 9px">✔ seguiu operacional: <b>${_pvEsc(e.operacao_paralela)}</b></div>`
      : '';
    const motivo = e.motivo ? `<div style="font-size:11.5px;color:#6B6258;margin-top:3px">${_pvEsc(e.motivo)}</div>` : '';
    return `<tr>
      <td class="h" style="border-left-color:#C68A1F">${fmtHora(e.inicio)}<br><span style="color:#9a8f80;font-weight:400">${fmtHora(e.fim)}</span></td>
      <td><span style="font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;color:#8A5A12;background:rgba(198,138,31,0.12);padding:2px 8px;border-radius:6px">${_pvEsc(e.causa_nome||'Sem classe')}</span>${motivo}${parcial}</td>
      <td class="a" style="font-weight:700;color:#8A5A12">${fmtDur(e.duracao_min)}</td>
    </tr>`;
  }).join('');
  const resumoParcial = parciais ? ` · <b style="color:#1F5755">${parciais}</b> com operação mantida` : '';
  return `<div class="wrap gbreak"><div class="section-t">{lang==='pt'?'Tempo não-produtivo — NPT':'Non-productive time — NPT'}</div>
    <div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#6B6258;margin-bottom:10px"><b style="color:#8A5A12">${npts.length}</b> evento(s) · total <b style="color:#8A5A12">${totalTxt}</b>${resumoParcial}</div>
    <table><thead><tr><th style="width:44px">{lang==='pt'?'Hora':'Time'}</th><th>Classe &amp; descrição</th><th style="text-align:right;width:60px">{lang==='pt'?'Duração':'Duration'}</th></tr></thead><tbody>${rows}</tbody></table>
  </div>`;
};

const _pvGraficoDir = (pontos, cor, titulo, unidade, base, passo) => {
  const w=176, H=74, padT=9, padB=14, padL=9, padR=9;
  const plotH = H - padT - padB;
  const ord = (pontos||[]).slice().sort((a,b)=>a.h-b.h);
  if(!ord.length) return `<div style="flex:1;min-width:150px;border:1px solid #ECE3D0;border-radius:10px;padding:8px 10px"><span style="font-family:monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#6B6258">${titulo}</span><div style="font-family:monospace;font-size:10px;color:#9a8f80;padding:12px 0">{lang==='pt'?'sem dados':'no data'}</div></div>`;
  let topo=base; const maxV=Math.max(...ord.map(p=>p.v)); while(maxV>topo) topo+=passo;
  const X=(h)=>padL+(h/23)*(w-padL-padR);
  const Y=(v)=>padT+(1-Math.min(v,topo)/topo)*plotH;
  const grid=[]; for(let g=passo; g<topo-0.0001; g+=passo) grid.push(g);
  const ult=ord[ord.length-1];
  const GRID='#E3DAC9';
  const hlines=grid.map(g=>`<line x1="${padL}" y1="${Y(g).toFixed(1)}" x2="${w-padR}" y2="${Y(g).toFixed(1)}" stroke="${GRID}" stroke-width="1"/>`).join('');
  const glabels=[...grid, topo].map(g=>`<text x="${(w-padR-1).toFixed(1)}" y="${(Y(g)-2).toFixed(1)}" text-anchor="end" font-family="monospace" font-size="6" fill="#B5AA98">${g}</text>`).join('');
  const vlines=Array.from({length:24},(_,h)=>`<line x1="${X(h).toFixed(1)}" y1="${padT}" x2="${X(h).toFixed(1)}" y2="${H-padB}" stroke="${GRID}" stroke-width="0.7" stroke-dasharray="2 3" opacity="0.55"/>`).join('');
  const ticks=[0,6,12,18,23].map(h=>`<text x="${X(h).toFixed(1)}" y="${H-3}" text-anchor="middle" font-family="monospace" font-size="7" fill="#6B6258">${String(h).padStart(2,'0')}</text>`).join('');
  const marks=ord.map(p=>{
    const arrow = p.dir!=null ? `<g transform="rotate(${(Number(p.dir)+180)%360})"><path d="M 0 -9.5 L 3.6 -4.3 L -3.6 -4.3 Z" fill="${cor}"/></g>` : '';
    return `<g transform="translate(${X(p.h).toFixed(1)},${Y(p.v).toFixed(1)})">${arrow}<circle cx="0" cy="0" r="3.2" fill="${cor}"/></g>`;
  }).join('');
  return `<div style="flex:1;min-width:150px;border:1px solid #ECE3D0;border-radius:10px;padding:8px 10px">
    <div style="display:flex;justify-content:space-between;align-items:baseline">
      <span style="font-family:monospace;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#6B6258">${titulo}</span>
      <span style="font-family:'Fraunces',Georgia,serif;font-size:14px;color:${cor}">${ult.v}<span style="font-size:8px;color:#6B6258"> ${unidade}${ult.dir!=null?` · ${String(ult.dir).padStart(3,'0')}°`:''}</span></span>
    </div>
    <svg width="${w}" height="${H}" viewBox="0 0 ${w} ${H}" style="margin-top:2px;max-width:100%">${hlines}${vlines}${glabels}${ticks}${marks}</svg>
  </div>`;
};

const _pvMeteo = (horarios, cfg) => {
  cfg = cfg || _LOG_GRAPH_DEFAULTS;
  const cheios=(horarios||[]).filter(r=>r.preenchido);
  const ventoPts=cheios.filter(r=>r.vento_int!=null).map(r=>({h:r.hora,v:r.vento_int,dir:r.vento_dir}));
  const corrPts=cheios.filter(r=>r.corrente_int!=null).map(r=>({h:r.hora,v:r.corrente_int,dir:r.corrente_dir}));
  const swellPts=cheios.filter(r=>r.swell_int!=null).map(r=>({h:r.hora,v:r.swell_int,dir:r.swell_dir}));
  if(!(ventoPts.length||corrPts.length||swellPts.length)) return '';
  const leg=[['Vento','#5C8DAE'],['Corrente','#1F5755'],['Swell','#5C7E4F']].map(([n,c])=>`<span style="display:inline-flex;align-items:center;gap:4px;font-family:monospace;font-size:9px;color:#6B6258;margin:0 8px 0 0"><span style="width:8px;height:8px;border-radius:50%;background:${c};display:inline-block"></span>${n}</span>`).join('');
  return `<div class="wrap"><div class="section-t">{lang==='pt'?'Meteorologia & ambiente':'Weather & environment'}</div>
    <div style="display:flex;align-items:center;gap:2px;margin-bottom:9px;flex-wrap:wrap">${leg}<span style="font-family:monospace;font-size:8px;color:#9a8f80;margin-left:4px">seta = direção pra onde vai · ponto = intensidade por hora</span></div>
    <div style="display:flex;gap:12px;flex-wrap:wrap">
      ${_pvGraficoDir(ventoPts,'#5C8DAE','Vento',"'",cfg.vento.topo,cfg.vento.passo)}${_pvGraficoDir(corrPts,'#1F5755','Corrente',"'",cfg.corrente.topo,cfg.corrente.passo)}${_pvGraficoDir(swellPts,'#5C7E4F','Swell','m',cfg.swell.topo,cfg.swell.passo)}
    </div>
  </div>`;
};
const _pvTimeline = (lista) => {
  const col = lista.filter(e => !e.cancelada && e.cor).map(e => ({h: parseInt(_logFmtHora(e.ts).slice(0,2)) || 0, cor: e.cor})).sort((a,b)=>a.h-b.h);
  if (!col.length) return '';
  const estados = [];
  for (let h=0; h<24; h++) { const ate = col.filter(c=>c.h<=h); estados[h] = ate.length ? ate[ate.length-1].cor : null; }
  const conta = {verde:0,amarelo:0,vermelho:0,cinza:0}; let info=0;
  estados.forEach(c=>{ if(c){conta[c]=(conta[c]||0)+1; info++;} });
  const oper = info ? Math.round((conta.verde/info)*100) : 0;
  const blocos = estados.map((c,h)=>`<div style="flex:1;height:28px;background:${c?_LOG_COR_RAW[c]:'#F0E9DA'};border-right:1px solid #fff" title="${String(h).padStart(2,'0')}h"></div>`).join('');
  const rot = `<div style="display:flex;margin-top:3px">`+Array.from({length:24},(_,h)=>`<div style="flex:1;text-align:center;font-family:monospace;font-size:7px;color:#9a8f80">${(h%6===0||h===23)?String(h).padStart(2,'0'):''}</div>`).join('')+`</div>`;
  const leg=[['verde','Operando'],['amarelo','Manutenção'],['vermelho','Parado'],['cinza','Desligado']].map(([c,n])=>`<span style="display:inline-flex;align-items:center;gap:4px;font-family:monospace;font-size:9px;color:#6B6258;margin-right:12px"><span style="width:9px;height:9px;border-radius:2px;background:${_LOG_COR_RAW[c]};display:inline-block"></span>${n}</span>`).join('');
  const kpi=(n,l,cor)=>`<div class="kpi"><div class="n"${cor?` style="color:${cor}"`:''}>${n}</div><div class="l">${l}</div></div>`;
  return `<div class="wrap"><div class="section-t">{lang==='pt'?'Operacionalidade':'Uptime'}</div>
    <div class="kpis" style="margin-bottom:14px">
      ${kpi(oper+'%','Operacionalidade','#5C7E4F')}${kpi(conta.verde+'h','Operando')}${kpi(conta.vermelho+'h','Parado','#B14E5C')}${kpi(conta.amarelo+'h','Manutenção','#C68A1F')}
    </div>
    <div style="display:flex;border-radius:6px;overflow:hidden;border:1px solid #ECE3D0">${blocos}</div>
    ${rot}
    <div style="margin-top:9px">${leg}</div>
    <div style="font-family:monospace;font-size:8px;color:#9a8f80;margin-top:8px">Estado vigente por hora, derivado dos registros coloridos do log.</div>
  </div>`;
};
const _pvLog = (lista) => {
  const linhas = lista.map(e => {
    const cor = _LOG_COR_RAW[e.cor] || 'transparent';
    const canc = e.cancelada;
    const marca = canc
      ? `<div class="cnote">✕ cancelada${e.cancelada_por?` por ${_pvEsc(e.cancelada_por)}`:''}${e.cancelada_motivo?` · ${_pvEsc(e.cancelada_motivo)}`:''}</div>`
      : (e.editada ? `<div class="enote">✎ editada</div>` : '');
    const clip = (e.foto_url && !canc) ? `<div class="clip">📎 foto anexada</div>` : '';
    return `<tr>
      <td class="h" style="border-left-color:${canc?'#6B6258':cor}">${_logFmtHora(e.ts)}</td>
      <td><span class="${canc?'canc':''}">${_pvEsc(e.texto)}</span>${marca}${clip}</td>
      <td class="a">${_pvEsc(e.autor_nome||'—')}</td>
    </tr>`;
  }).join('');
  return `<div class="wrap"><div class="section-t">Registro operacional — ${lista.length} entrada(s)</div>
    <table><thead><tr><th>{lang==='pt'?'Hora':'Time'}</th><th>Registro</th><th>{lang==='pt'?'Autor':'Author'}</th></tr></thead><tbody>${linhas}</tbody></table></div>`;
};
const _pvGaleria = (lista) => {
  const comFoto = lista.filter(e => e.foto_url && !e.cancelada);
  if (!comFoto.length) return '';
  const cards = comFoto.map(e => {
    const cor = _LOG_COR_RAW[e.cor] || '#ECE3D0';
    return `<div class="gcard">
      <img src="${_pvEsc(e.foto_url)}" alt=""/>
      <div class="gcap">
        <div class="gh"><span class="gdot" style="background:${cor}"></span>${_logFmtHora(e.ts)} · ${_pvEsc(e.autor_nome||'—')}</div>
        <div class="gtx">${_pvEsc(e.texto)}</div>
      </div>
    </div>`;
  }).join('');
  return `<div class="wrap gbreak"><div class="section-t">Registro fotográfico — ${comFoto.length} foto(s)</div>
    <div class="gal">${cards}</div></div>`;
};
const _pvAssinaturas = (turnos) => {
  if (!turnos || !turnos.length) return '';
  const cards = turnos.map(t => {
    const h = new Date(t.assumido_em); const hh = isNaN(h) ? '' : h.toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'});
    return `<div class="card">
      <div class="mono" style="font-size:9px;color:#6B6258;text-transform:uppercase;letter-spacing:1px">Turno assumido · ${hh}</div>
      <div style="font-size:14px;margin-top:4px"><b>${_pvEsc(t.operador_nome)}</b></div>
      ${t.entregue_por?`<div style="font-size:11px;color:#6B6258">recebido de ${_pvEsc(t.entregue_por)}</div>`:''}
      ${t.observacoes?`<div style="font-size:11px;margin-top:5px">${_pvEsc(t.observacoes)}</div>`:''}
    </div>`;
  }).join('');
  return `<div class="wrap"><div class="section-t">Passagens de turno</div><div class="sign">${cards}</div></div>`;
};

const LogAvatar = ({foto, initials, nome, size = 30}) => (
  <div title={nome||''} style={{width:size,height:size,borderRadius:'50%',flexShrink:0,backgroundImage:foto?`url(${foto})`:'none',backgroundColor:foto?'transparent':'var(--teal)',backgroundSize:'cover',backgroundPosition:'center',display:'grid',placeItems:'center',fontFamily:'var(--fm)',fontSize:Math.round(size*0.4),color:'#fff'}}>{foto?'':(initials||(nome||'?').slice(0,1).toUpperCase())}</div>
);

const LogHistModal = ({entrada, token, onClose}) => {
  const lang = useT().lang;
  const [hist, setHist] = React.useState(null);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  React.useEffect(() => { fetch(`/api/logbook/entradas/${entrada.id}/hist`, {headers:H()}).then(r=>r.ok?r.json():[]).then(setHist).catch(()=>setHist([])); }, []);
  const quando = (q) => { const d = new Date(q); return isNaN(d) ? '' : d.toLocaleString('pt-BR', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); };
  return (
    <div style={{position:'fixed',inset:0,zIndex:255,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:440,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20}}>Histórico da <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'entrada':'entry'}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'10px 12px',marginBottom:14}}>
          <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',textTransform:'uppercase',letterSpacing:0.4,marginBottom:2}}>versão atual{entrada.cancelada?' (cancelada)':''}</div>
          <div style={{fontFamily:'var(--f)',fontSize:13.5,textDecoration:entrada.cancelada?'line-through':'none'}}>{entrada.texto}</div>
        </div>
        {hist === null ? <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)'}}>{lang==='pt'?'Carregando…':'Loading…'}</div> :
         hist.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)'}}>Sem alterações registradas.</div> : (
          <div style={{display:'grid',gap:8}}>
            {hist.map(h => (
              <div key={h.id} style={{borderLeft:`3px solid ${h.acao==='cancelada'?'var(--rose)':'var(--sun)'}`,paddingLeft:10}}>
                <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:h.acao==='cancelada'?'var(--rose)':'var(--sun)'}}>{h.acao==='cancelada'?'✕ cancelada':'✎ editada'} · {h.por_nome||'—'} · {quando(h.quando)}</div>
                {h.acao==='editada' && <div style={{fontFamily:'var(--f)',fontSize:12.5,color:'var(--muted)',marginTop:2}}>antes: "{h.texto}"</div>}
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

// ── Fila offline (Mod 02): post falhou por rede → guarda aqui e reenvia sozinho ──
const _LOG_OUTBOX_KEY = 'genesis_log_outbox_v1';
const _logOutboxLoad = () => { try { const q = JSON.parse(localStorage.getItem(_LOG_OUTBOX_KEY) || '[]'); return Array.isArray(q) ? q : []; } catch (e) { return []; } };
const _logOutboxSave = (q) => { try { localStorage.setItem(_LOG_OUTBOX_KEY, JSON.stringify(q.slice(0, 50))); } catch (e) {} };

// ── Modais genéricos (Mod 02): substituem window.prompt / window.confirm ──
const LogPromptModal = ({ titulo, label, valor, placeholder, permiteVazio, onOk, onClose }) => {
  const lang = useT().lang;
  const [txt, setTxt] = React.useState(valor || '');
  const ok = () => { if (txt.trim() || permiteVazio) onOk(txt.trim()); };
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:270,background:'rgba(31,42,46,0.5)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:360,background:'var(--cream)',borderRadius:18,padding:'18px 20px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:18,marginBottom:10}}>{titulo}</div>
        {label && <div className="label">{label}</div>}
        <input className="input" value={txt} autoFocus placeholder={placeholder||''} onChange={e=>setTxt(e.target.value)} onKeyDown={e=>{ if (e.key==='Enter') ok(); }} style={{width:'100%',boxSizing:'border-box',marginBottom:14}}/>
        <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
          <button className="btn btn-primary" onClick={ok} disabled={!txt.trim() && !permiteVazio}>OK</button>
        </div>
      </div>
    </div>
  );
};

const LogConfirmModal = ({ titulo, corpo, perigo, textoOk, onOk, onClose }) => {
  const lang = useT().lang;
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:270,background:'rgba(31,42,46,0.5)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:360,background:'var(--cream)',borderRadius:18,padding:'18px 20px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:18,marginBottom:corpo?8:14}}>{titulo}</div>
        {corpo && <div style={{fontFamily:'var(--f)',fontSize:13,color:'var(--ink-2)',lineHeight:1.5,marginBottom:14}}>{corpo}</div>}
        <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Voltar':'Back'}</button>
          <button className="btn btn-primary" onClick={onOk} style={perigo?{background:'var(--rose)'}:undefined}>{textoOk || (lang==='pt'?'Confirmar':'Confirm')}</button>
        </div>
      </div>
    </div>
  );
};

// Cancelamento de entrada (rastro IMCA): motivo OBRIGATÓRIO, com atalhos de motivo
const LogCancelEntradaModal = ({ entrada, onOk, onClose }) => {
  const lang = useT().lang;
  const [motivo, setMotivo] = React.useState('');
  const chips = lang==='pt'
    ? ['hora errada','duplicado','equipamento errado','texto com erro','lançamento de teste']
    : ['wrong time','duplicate','wrong equipment','typo','test entry'];
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:270,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:400,background:'var(--cream)',borderRadius:20,padding:'20px 22px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:19,marginBottom:6}}>Cancelar <em style={{fontStyle:'italic',color:'var(--rose)'}}>{lang==='pt'?'entrada':'entry'}</em></div>
        <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:10,lineHeight:1.5}}>{lang==='pt'?'Ela fica riscada no log, não some — o motivo entra no rastro de auditoria.':'It stays struck through in the log — the reason goes into the audit trail.'}</div>
        <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'8px 11px',fontFamily:'var(--f)',fontSize:12.5,color:'var(--ink-2)',marginBottom:12,maxHeight:70,overflowY:'auto'}}>{entrada.texto}</div>
        <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:10}}>
          {chips.map(c => <button key={c} onClick={()=>setMotivo(c)} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 11px',borderRadius:999,cursor:'pointer',border:'1px solid '+(motivo===c?'var(--rose)':'var(--line-soft)'),background:motivo===c?'rgba(177,78,92,0.1)':'transparent',color:motivo===c?'var(--rose)':'var(--muted)'}}>{c}</button>)}
        </div>
        <input className="input" value={motivo} autoFocus placeholder={lang==='pt'?'Motivo (obrigatório)…':'Reason (required)…'} onChange={e=>setMotivo(e.target.value)} onKeyDown={e=>{ if (e.key==='Enter' && motivo.trim()) onOk(motivo.trim()); }} style={{width:'100%',boxSizing:'border-box',marginBottom:14}}/>
        <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Voltar':'Back'}</button>
          <button className="btn btn-primary" disabled={!motivo.trim()} onClick={()=>onOk(motivo.trim())} style={{background:'var(--rose)'}}>{lang==='pt'?'Cancelar entrada':'Cancel entry'}</button>
        </div>
      </div>
    </div>
  );
};

// ── Busca textual no log (Mod 02): resultados agrupados por dia, salto direto ──
const LogBuscaModal = ({ token, onIrParaDia, onClose }) => {
  const lang = useT().lang;
  const [q, setQ] = React.useState('');
  const [de, setDe] = React.useState('');
  const [ate, setAte] = React.useState('');
  const [res, setRes] = React.useState(null);
  const [buscando, setBuscando] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const buscar = async () => {
    if (q.trim().length < 2 || buscando) return;
    setBuscando(true);
    try {
      const p = new URLSearchParams({ q: q.trim() });
      if (de) p.set('de', de); if (ate) p.set('ate', ate);
      const r = await fetch(`/api/logbook/entradas/busca?${p}`, {headers:H()});
      setRes(r.ok ? await r.json() : []);
    } catch (e) { setRes([]); }
    finally { setBuscando(false); }
  };
  const marcar = (texto) => {
    const t = String(texto || ''); const i = t.toLowerCase().indexOf(q.trim().toLowerCase());
    if (i < 0 || !q.trim()) return t;
    return <>{t.slice(0,i)}<mark style={{background:'rgba(198,138,31,0.35)',borderRadius:3,padding:'0 1px'}}>{t.slice(i, i+q.trim().length)}</mark>{t.slice(i+q.trim().length)}</>;
  };
  const porDia = {};
  (res || []).forEach(e => { const d = String(e.ts || '').slice(0,10); (porDia[d] = porDia[d] || []).push(e); });
  const dias = Object.keys(porDia).sort((a,b)=>b.localeCompare(a));
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:260,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'6vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:520,background:'var(--cream)',borderRadius:22,padding:'20px 22px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20}}>Buscar no <em style={{fontStyle:'italic',color:'var(--accent)'}}>log</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{display:'flex',gap:6,marginBottom:8}}>
          <input className="input" value={q} autoFocus placeholder={lang==='pt'?'ex.: DGPS, guindaste, corrente…':'e.g.: DGPS, crane, current…'} onChange={e=>setQ(e.target.value)} onKeyDown={e=>{ if (e.key==='Enter') buscar(); }} style={{flex:1,minWidth:0}}/>
          <button className="btn btn-primary" onClick={buscar} disabled={buscando || q.trim().length<2}>{buscando?'…':(lang==='pt'?'Buscar':'Search')}</button>
        </div>
        <div style={{display:'flex',gap:6,alignItems:'center',marginBottom:12}}>
          <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>{lang==='pt'?'período (opcional):':'range (optional):'}</span>
          <input className="input" type="date" value={de} onChange={e=>setDe(e.target.value)} style={{fontSize:11,padding:'4px 6px',width:'auto'}}/>
          <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>→</span>
          <input className="input" type="date" value={ate} onChange={e=>setAte(e.target.value)} style={{fontSize:11,padding:'4px 6px',width:'auto'}}/>
        </div>
        {res === null ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Busca no log inteiro — canceladas aparecem riscadas.':'Searches the whole log — cancelled entries appear struck through.'}</div> :
         res.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>{lang==='pt'?'Nada encontrado.':'Nothing found.'}</div> : (
          <div style={{display:'grid',gap:12,maxHeight:'52vh',overflowY:'auto',paddingRight:2}}>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)'}}>{res.length}{res.length>=100?'+':''} {lang==='pt'?'resultado(s)':'result(s)'}</div>
            {dias.map(d => (
              <div key={d}>
                <button onClick={()=>onIrParaDia(d)} title={lang==='pt'?'abrir este dia no log':'open this day in the log'} style={{display:'inline-flex',alignItems:'center',gap:6,background:'var(--cream-2)',border:'1px solid var(--line-soft)',borderRadius:8,padding:'3px 10px',cursor:'pointer',fontFamily:'var(--fm)',fontSize:11,color:'var(--ink)',marginBottom:5}}>
                  📅 {d.split('-').reverse().join('/')} <span style={{color:'var(--accent)'}}>→</span>
                </button>
                <div style={{display:'grid',gap:4}}>
                  {porDia[d].map(e => (
                    <div key={e.id} onClick={()=>onIrParaDia(d)} style={{cursor:'pointer',background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`3px solid ${e.cancelada?'var(--muted)':(_logCorHex(e.cor)||'var(--line-soft)')}`,borderRadius:9,padding:'7px 10px',opacity:e.cancelada?0.6:1}}>
                      <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',marginBottom:2}}>{_logFmtHora(e.ts)} · {e.autor_nome||'—'}{e.cancelada ? (lang==='pt'?' · CANCELADA':' · CANCELLED') : ''}{e.editada && !e.cancelada ? (lang==='pt'?' · editada':' · edited') : ''}</div>
                      <div style={{fontFamily:'var(--f)',fontSize:12.5,lineHeight:1.4,textDecoration:e.cancelada?'line-through':'none'}}>{marcar(e.texto)}</div>
                    </div>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

const LogEditModal = ({entrada, token, toast, porNome, onClose, onSaved}) => {
  const lang = useT().lang;
  const [texto, setTexto] = React.useState(entrada.texto || '');
  const [ts, setTs] = React.useState(_logIsoToLocal(entrada.ts));
  const [foto, setFoto] = React.useState(entrada.foto_url || null);
  const [saving, setSaving] = React.useState(false);
  const [upLoading, setUpLoading] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const upFoto = async (file) => {
    if (!file) return;
    setUpLoading(true);
    try {
      const fd = new FormData(); fd.append('photo', file);
      const r = await fetch('/api/upload/photo', {method:'POST', headers:H(), body:fd});
      const d = await r.json();
      if (r.ok && d.url) { setFoto(d.url); toast.push({icon:'check', title:'Foto anexada'}); }
      else toast.push({icon:'x', title:(lang==='pt'?'Erro no upload':'Upload error')});
    } catch(e){ toast.push({icon:'x', title:(lang==='pt'?'Erro no upload':'Upload error')}); }
    finally { setUpLoading(false); }
  };
  const salvar = async () => {
    if (!texto.trim()) { toast.push({icon:'x', title:'Texto vazio'}); return; }
    setSaving(true);
    try {
      const body = { texto: texto.trim(), ts: ts ? new Date(ts).toISOString() : entrada.ts, foto_url: foto, por_nome: porNome || null };
      const r = await fetch(`/api/logbook/entradas/${entrada.id}`, {method:'PATCH', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify(body)});
      if (r.ok) { toast.push({icon:'check', title:(lang==='pt'?'Entrada atualizada':'Entry updated')}); onSaved(); }
      else toast.push({icon:'x', title:(lang==='pt'?'Erro ao salvar':'Error saving')});
    } catch(e){ toast.push({icon:'x', title:(lang==='pt'?'Erro de conexão':'Connection error')}); }
    finally { setSaving(false); }
  };
  return (
    <div style={{position:'fixed',inset:0,zIndex:220,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:440,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Editar <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'entrada':'entry'}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div className="label">{lang==='pt'?'Hora':'Time'}</div>
        <input className="input" type="datetime-local" value={ts} onChange={e=>setTs(e.target.value)} style={{width:'100%',boxSizing:'border-box',marginBottom:12}}/>
        <div className="label">{lang==='pt'?'Texto':'Text'}</div>
        <textarea className="input" value={texto} onChange={e=>setTexto(e.target.value)} style={{width:'100%',boxSizing:'border-box',minHeight:80,resize:'vertical',marginBottom:14,fontFamily:'var(--f)',fontSize:13}}/>
        <div className="label">{lang==='pt'?'Anexo':'Attachment'}</div>
        {foto ? (
          <div style={{marginBottom:16,display:'flex',alignItems:'center',gap:10}}>
            <img src={foto} onClick={()=>window.open(foto,'_blank')} style={{width:60,height:60,borderRadius:8,objectFit:'cover',cursor:'pointer',border:'1px solid var(--line-soft)'}}/>
            <div style={{display:'flex',gap:6}}>
              <label className="btn btn-sm" style={{cursor:'pointer',margin:0}}>{upLoading?'…':'trocar'}<input type="file" accept="image/*" style={{display:'none'}} onChange={e=>{ upFoto(e.target.files[0]); e.target.value=''; }}/></label>
              <button className="btn btn-sm" onClick={()=>setFoto(null)}>{lang==='pt'?'remover':'remove'}</button>
            </div>
          </div>
        ) : (
          <label className="btn btn-sm" style={{cursor:'pointer',margin:'0 0 16px',display:'inline-block'}}>{upLoading?'enviando…':'📷 adicionar foto'}<input type="file" accept="image/*" style={{display:'none'}} onChange={e=>{ upFoto(e.target.files[0]); e.target.value=''; }}/></label>
        )}
        <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
          <button className="btn btn-primary" onClick={salvar} disabled={saving}>{saving ? 'Salvando…' : 'Salvar'}</button>
        </div>
      </div>
    </div>
  );
};

// ── Painel central: abas + frases como botões ──
// Modal de um grupo de frases: toca no botão do grupo → escolhe a frase aqui
const LogFrasesGrupoModal = ({ titulo, cor, frases, macros, onFrase, onMacro, onClose }) => {
  const lang = useT().lang;
  const subs = frases ? [...new Set(frases.map(f => f.subgrupo).filter(Boolean))] : [];
  const temGeral = frases ? frases.some(f => !f.subgrupo) : false;
  const [sub, setSub] = React.useState('__todas');
  const frasesVis = (frases || []).filter(f => sub === '__todas' ? true : (sub === '__geral' ? !f.subgrupo : f.subgrupo === sub));
  const subPill = (on) => ({fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 11px',borderRadius:999,cursor:'pointer',border:'1px solid '+(on?'var(--teal)':'var(--line-soft)'),background:on?'var(--teal)':'transparent',color:on?'#fff':'var(--muted)',whiteSpace:'nowrap'});
  return (
  <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.45)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:460,background:'var(--cream)',borderRadius:20,padding:'18px 20px',boxShadow:'var(--shadow-lg)',display:'flex',flexDirection:'column',maxHeight:'78vh'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12,flexShrink:0}}>
        <div style={{display:'flex',alignItems:'center',gap:8}}>
          <span style={{width:10,height:10,borderRadius:'50%',background:cor,flexShrink:0}}/>
          <span style={{fontFamily:'var(--fs)',fontSize:19}}>{titulo}</span>
        </div>
        <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
      </div>
      {!macros && subs.length > 0 && (
        <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:10,flexShrink:0}}>
          <button style={subPill(sub==='__todas')} onClick={()=>setSub('__todas')}>{lang==='pt'?'todas':'all'}</button>
          {subs.map(sg => <button key={sg} style={subPill(sub===sg)} onClick={()=>setSub(sg)}>{sg}</button>)}
          {temGeral && <button style={subPill(sub==='__geral')} onClick={()=>setSub('__geral')}>gerais</button>}
        </div>
      )}
      <div style={{flex:1,minHeight:0,overflowY:'auto',display:'grid',gap:7,paddingRight:2,alignContent:'start'}}>
        {macros ? (
          macros.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Nenhuma sequência.':'No sequences.'}</div> :
          macros.map(m => (
            <button key={m.id} onClick={()=>{ onMacro(m); onClose(); }} style={{textAlign:'left',fontFamily:'var(--f)',fontSize:13,padding:'11px 12px',borderRadius:10,cursor:'pointer',background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:'4px solid var(--accent)',display:'flex',alignItems:'center',gap:8,color:'var(--ink)'}}>
              <span style={{flex:1}}>⚡ {m.nome}</span>
              <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',flexShrink:0}}>{m.passos.length} passos</span>
            </button>
          ))
        ) : (
          frasesVis.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>Sem frases neste grupo.</div> :
          frasesVis.map(f => {
            const c = _logCorHex(f.cor);
            const temTpl = _logSlots(f.texto).length > 0;
            const grupo = f.alvos && f.alvos.length;
            return (
              <button key={f.id} onClick={()=>{ onFrase(f); onClose(); }} style={{textAlign:'left',fontFamily:'var(--f)',fontSize:13.5,padding:'11px 12px',borderRadius:10,cursor:'pointer',background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`4px solid ${c||'var(--line-soft)'}`,display:'flex',alignItems:'center',gap:8,color:'var(--ink)'}}>
                <span style={{flex:1}}>{f.texto}{temTpl && <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--sky)',marginLeft:6}}>preenchível</span>}{grupo?<span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--teal)',marginLeft:6}}>🎨{grupo}</span>:''}</span>
                <span style={{fontFamily:'var(--fm)',fontSize:14,color:'var(--muted)',flexShrink:0}}>＋</span>
              </button>
            );
          })
        )}
      </div>
    </div>
  </div>
);
};

// Frases rápidas: grade de botões-grupo (se o widget é estreito vira coluna; largo vira linha)
const LogFrasesPanel = ({abas, abaAtiva, setAbaAtiva, onFrase, macros, onMacro, canManage, onGerenciar, isMobile, colapsavel}) => {
  const lang = useT().lang;
  const [aberto, setAberto] = React.useState(!colapsavel);
  const [grupoAberto, setGrupoAberto] = React.useState(null); // {tipo:'aba',id} | {tipo:'rec'} | {tipo:'mac'}
  const usadas = abas.flatMap(a => a.frases).filter(f => f.ultimo_uso).sort((a,b)=> new Date(b.ultimo_uso) - new Date(a.ultimo_uso)).slice(0, 8);
  const temRec = usadas.length > 0;
  const temMacros = macros && macros.length > 0;

  const cardBtn = (corTopo) => ({background:'var(--cream)',border:'1px solid var(--line-soft)',borderTop:`3px solid ${corTopo}`,borderRadius:12,padding:'12px 10px 10px',cursor:'pointer',textAlign:'center',color:'var(--ink)',display:'flex',flexDirection:'column',alignItems:'center',gap:4,minWidth:0,transition:'background .12s'});
  const nomeSt = {fontFamily:'var(--f)',fontSize:12.5,fontWeight:600,lineHeight:1.2,maxWidth:'100%',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'};
  const contSt = {fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'};

  const modal = grupoAberto && (() => {
    if (grupoAberto.tipo === 'mac') return <LogFrasesGrupoModal titulo="Sequências" cor="var(--accent)" macros={macros} onMacro={onMacro} onClose={()=>setGrupoAberto(null)}/>;
    if (grupoAberto.tipo === 'rec') return <LogFrasesGrupoModal titulo="Recentes" cor="var(--sun)" frases={usadas} onFrase={onFrase} onClose={()=>setGrupoAberto(null)}/>;
    const aba = abas.find(a => a.id === grupoAberto.id);
    if (!aba) return null;
    const fr = aba.frases.slice().sort((a,b)=> (b.uso_count||0)-(a.uso_count||0) || (a.ordem-b.ordem));
    return <LogFrasesGrupoModal titulo={aba.nome} cor="var(--teal)" frases={fr} onFrase={onFrase} onClose={()=>setGrupoAberto(null)}/>;
  })();

  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:14,padding:'12px 14px',height:'100%',boxSizing:'border-box',display:'flex',flexDirection:'column',minHeight:0}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:aberto?12:0,flexShrink:0}}>
        <div className="t-eyebrow" onClick={colapsavel?()=>setAberto(v=>!v):undefined} style={{cursor:colapsavel?'pointer':'default'}}>{colapsavel?(aberto?'▾ ':'▸ '):''}{lang==='pt'?'frases rápidas':'quick phrases'}</div>
        {canManage && aberto && <button onClick={onGerenciar} title="biblioteca e sequências" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:30,height:30,cursor:'pointer',fontSize:15,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>}
      </div>
      {aberto && (abas.length === 0 && !temMacros ? (
        <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)',padding:'6px 0'}}>Nenhuma aba ainda. {canManage && 'Crie na ⚙ biblioteca.'}</div>
      ) : (
        <div style={{flex:1,minHeight:0,overflowY:'auto'}}>
          <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(118px, 1fr))',gap:8,alignContent:'start'}}>
            {temRec && (
              <button onClick={()=>setGrupoAberto({tipo:'rec'})} style={cardBtn('var(--sun)')} onMouseEnter={e=>e.currentTarget.style.background='var(--cream-2)'} onMouseLeave={e=>e.currentTarget.style.background='var(--cream)'}>
                <span style={{fontSize:15,lineHeight:1}}>★</span>
                <span style={nomeSt}>Recentes</span>
                <span style={contSt}>{usadas.length} frases</span>
              </button>
            )}
            {temMacros && (
              <button onClick={()=>setGrupoAberto({tipo:'mac'})} style={cardBtn('var(--accent)')} onMouseEnter={e=>e.currentTarget.style.background='var(--cream-2)'} onMouseLeave={e=>e.currentTarget.style.background='var(--cream)'}>
                <span style={{fontSize:15,lineHeight:1}}>⚡</span>
                <span style={nomeSt}>{lang==='pt'?'Sequências':'Sequences'}</span>
                <span style={contSt}>{macros.length} {macros.length===1?'sequência':'sequências'}</span>
              </button>
            )}
            {abas.map(a => (
              <button key={a.id} onClick={()=>setGrupoAberto({tipo:'aba', id:a.id})} style={cardBtn('var(--teal)')} onMouseEnter={e=>e.currentTarget.style.background='var(--cream-2)'} onMouseLeave={e=>e.currentTarget.style.background='var(--cream)'}>
                <span style={{display:'flex',gap:3,height:15,alignItems:'center'}}>
                  {a.frases.slice(0,4).map(f => <span key={f.id} style={{width:7,height:7,borderRadius:'50%',background:_logCorHex(f.cor)||'var(--line)',display:'inline-block'}}/>)}
                  {a.frases.length===0 && <span style={{width:7,height:7,borderRadius:'50%',background:'var(--line)',display:'inline-block'}}/>}
                </span>
                <span style={nomeSt}>{a.nome}</span>
                <span style={contSt}>{a.frases.length} {a.frases.length===1?'frase':'frases'}</span>
              </button>
            ))}
          </div>
        </div>
      ))}
      {modal}
    </div>
  );
};

// ── Seletor de cor (bolinhas) — escopo do módulo ──
const LogCorDots = ({sel, onPick}) => (
  <div style={{display:'flex',gap:5,alignItems:'center'}}>
    {_LOG_CORES.map(c => {
      const hex = _logCorHex(c.v); const on = sel === c.v;
      return <button key={c.nome} title={c.nome} onClick={()=>onPick(c.v)} style={{width:16,height:16,borderRadius:'50%',cursor:'pointer',flexShrink:0,background:hex||'var(--cream-2)',border:on?'2px solid var(--ink)':'1px solid var(--line-soft)',position:'relative',padding:0}}>{!c.v && <span style={{position:'absolute',inset:0,display:'grid',placeItems:'center',fontSize:9,color:'var(--muted)'}}>∅</span>}</button>;
    })}
  </div>
);

// ── Modal: gerenciar a biblioteca (abas + frases) ──
const LogFrasesGerenciarModal = ({token, toast, onClose, onChanged, abaGer, setAbaGer}) => {
  const lang = useT().lang;
  const [abas, setAbas] = React.useState(null);
  const [abaSel, setAbaSel] = React.useState(null);
  const [novaAba, setNovaAba] = React.useState('');
  const [novoTexto, setNovoTexto] = React.useState('');
  const [novoSub, setNovoSub] = React.useState('');
  const [novaCor, setNovaCor] = React.useState(null);
  const [equips, setEquips] = React.useState([]);
  const [alvoFrase, setAlvoFrase] = React.useState(null);
  const H = () => ({'Authorization':`Bearer ${token()}`});

  const load = () => fetch('/api/logbook/abas', {headers:H()}).then(r=>r.ok?r.json():[]).then(d => {
    setAbas(d);
    setAbaSel(prev => { const still = d.find(a => a.id === prev); return still ? prev : (d[0] ? d[0].id : null); });
  }).catch(()=>setAbas([]));
  React.useEffect(() => { load(); fetch('/api/logbook/equipdp', {headers:H()}).then(r=>r.ok?r.json():[]).then(d=>setEquips(Array.isArray(d)?d:[])).catch(()=>{}); }, []);

  const jset = (extra) => ({...H(),'Content-Type':'application/json', ...extra});
  const post = (url, body) => fetch(url, {method:'POST', headers:jset(), body:JSON.stringify(body)});
  const patch = (url, body) => fetch(url, {method:'PATCH', headers:jset(), body:JSON.stringify(body)});
  const del = (url) => fetch(url, {method:'DELETE', headers:H()});
  const changed = () => { load(); onChanged && onChanged(); };

  const addAba = async () => { const n = novaAba.trim(); if (!n) return; const r = await post('/api/logbook/abas', {nome:n}); if (r.ok) { setNovaAba(''); changed(); } };
  // Mod 02 · modais próprios no lugar de window.prompt/confirm
  const [pedir, setPedir] = React.useState(null); // {tipo:'renAba'|'delAba'|'subFrase'|'editFrase', alvo}
  const executarPedido = {
    renAba: async (a, n) => { if (n !== a.nome) { await patch(`/api/logbook/abas/${a.id}`, {nome:n}); changed(); } },
    delAba: async (a) => { await del(`/api/logbook/abas/${a.id}`); changed(); },
    subFrase: async (f, sg) => { await patch(`/api/logbook/frases/${f.id}`, {subgrupo: sg || null}); changed(); },
    editFrase: async (f, t) => { if (t !== f.texto) { await patch(`/api/logbook/frases/${f.id}`, {texto:t}); changed(); } },
  };
  const renAba = (a) => setPedir({tipo:'renAba', alvo:a});
  const delAba = (a) => setPedir({tipo:'delAba', alvo:a});
  const addFrase = async () => { const t = novoTexto.trim(); if (!t || !abaSel) return; const r = await post('/api/logbook/frases', {aba_id:abaSel, texto:t, cor:novaCor, subgrupo:novoSub.trim()||null}); if (r.ok) { setNovoTexto(''); setNovaCor(null); changed(); } };
  const subFrase = (f) => setPedir({tipo:'subFrase', alvo:f});
  const editFrase = (f) => setPedir({tipo:'editFrase', alvo:f});
  const corFrase = async (f, cor) => { await patch(`/api/logbook/frases/${f.id}`, {cor}); changed(); };
  const delFrase = async (f) => { await del(`/api/logbook/frases/${f.id}`); changed(); };

  const aba = abas ? abas.find(a => a.id === abaSel) : null;

  return (
    <div style={{position:'fixed',inset:0,zIndex:230,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:720,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6,gap:10}}>
          <div style={{display:'flex',gap:4,background:'var(--cream-2)',borderRadius:10,padding:3}}>
            <button onClick={()=>setAbaGer&&setAbaGer('biblioteca')} style={{fontFamily:'var(--fm)',fontSize:12,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:'var(--surface)',color:'var(--ink)',fontWeight:600}}>⚙ Biblioteca</button>
            <button onClick={()=>setAbaGer&&setAbaGer('sequencias')} style={{fontFamily:'var(--fm)',fontSize:12,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:'transparent',color:'var(--muted)'}}>⚡ Sequências</button>
          </div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:16}}>A frase é fonte única: a cor mora nela. Editar aqui não altera o que já foi registrado no log.</div>

        <div style={{display:'flex',flexDirection:'column',gap:16}}>
          {/* abas */}
          <div>
            <div className="t-eyebrow" style={{marginBottom:8}}>{lang==='pt'?'Abas':'Tabs'}</div>
            <div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:10}}>
              {abas === null ? <span style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Carregando…':'Loading…'}</span> :
               abas.map(a => {
                const on = a.id === abaSel;
                return (
                  <div key={a.id} style={{display:'flex',alignItems:'center',gap:4,padding:'4px 6px 4px 12px',borderRadius:999,border:'1px solid '+(on?'var(--teal)':'var(--line-soft)'),background:on?'rgba(31,87,85,0.08)':'transparent'}}>
                    <span onClick={()=>setAbaSel(a.id)} onDoubleClick={()=>renAba(a)} title="clique: selecionar · duplo-clique: renomear" style={{cursor:'pointer',fontFamily:'var(--fm)',fontSize:12,color:on?'var(--teal)':'var(--ink)',fontWeight:on?600:400}}>{a.nome}</span>
                    <button onClick={()=>delAba(a)} title="apagar aba" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:11}}>✕</button>
                  </div>
                );
              })}
            </div>
            <div style={{display:'flex',gap:8}}>
              <input className="input" value={novaAba} placeholder="Nova aba (ex: Winch)…" onChange={e=>setNovaAba(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') addAba(); }} style={{flex:1}}/>
              <button className="btn btn-primary" onClick={addAba}>+ Aba</button>
            </div>
          </div>

          {/* frases da aba selecionada */}
          <div style={{borderTop:'1px solid var(--cream-2)',paddingTop:16}}>
            <div className="t-eyebrow" style={{marginBottom:8}}>Frases {aba ? `· ${aba.nome}` : ''}</div>
            {!aba ? <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>Selecione ou crie uma aba.</div> : (
              <>
                <div style={{display:'grid',gap:6,marginBottom:12}}>
                  {aba.frases.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>{lang==='pt'?'Nenhuma frase nesta aba.':'No phrases in this tab.'}</div> :
                   aba.frases.map(f => (
                    <div key={f.id} style={{display:'flex',alignItems:'center',gap:10,background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`4px solid ${_logCorHex(f.cor)||'var(--line-soft)'}`,borderRadius:10,padding:'8px 12px'}}>
                      <span onClick={()=>editFrase(f)} title="clique pra editar" style={{flex:1,fontFamily:'var(--f)',fontSize:13,cursor:'pointer'}}>{f.texto}</span>
                      <button onClick={()=>setAlvoFrase(f)} title="colorir vários botões (grupo)" style={{background:(f.alvos&&f.alvos.length)?'var(--teal)':'transparent',color:(f.alvos&&f.alvos.length)?'#fff':'var(--muted)',border:'1px solid '+((f.alvos&&f.alvos.length)?'var(--teal)':'var(--line-soft)'),borderRadius:6,cursor:'pointer',fontSize:10,fontFamily:'var(--fm)',padding:'2px 6px',whiteSpace:'nowrap'}}>🎨{f.alvos&&f.alvos.length?` ${f.alvos.length}`:''}</button>
                      <LogCorDots sel={f.cor||null} onPick={(c)=>corFrase(f, c)}/>
                      <button onClick={()=>subFrase(f)} title={f.subgrupo?`subgrupo: ${f.subgrupo} — clique pra mudar`:'definir subgrupo (abas dentro do modal do grupo)'} style={{background:f.subgrupo?'var(--teal)':'transparent',border:'1px solid '+(f.subgrupo?'var(--teal)':'var(--line-soft)'),color:f.subgrupo?'#fff':'var(--muted)',borderRadius:999,cursor:'pointer',fontFamily:'var(--fm)',fontSize:9,padding:'2px 8px',maxWidth:90,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',flexShrink:0}}>{f.subgrupo||'+sub'}</button>
                      <button onClick={()=>delFrase(f)} title="apagar frase" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:12}}>✕</button>
                    </div>
                  ))}
                </div>
                <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:12,padding:'10px 12px'}}>
                  <div style={{display:'flex',gap:6,marginBottom:8}}>
                    <input className="input" value={novoTexto} placeholder="Nova frase…" onChange={e=>setNovoTexto(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') addFrase(); }} style={{flex:1,minWidth:0,boxSizing:'border-box'}}/>
                    <input className="input" value={novoSub} placeholder="subgrupo" list="genesis-subgrupos" onChange={e=>setNovoSub(e.target.value)} style={{width:110,boxSizing:'border-box'}}/>
                    <datalist id="genesis-subgrupos">{[...new Set((aba?aba.frases:[]).map(x=>x.subgrupo).filter(Boolean))].map(sg=><option key={sg} value={sg}/>)}</datalist>
                  </div>
                  <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:10}}>
                    <div style={{display:'flex',alignItems:'center',gap:8}}>
                      <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>cor:</span>
                      <LogCorDots sel={novaCor} onPick={setNovaCor}/>
                    </div>
                    <button className="btn btn-primary btn-sm" onClick={addFrase}>+ Frase</button>
                  </div>
                </div>
              </>
            )}
          </div>
        </div>
      </div>
      {alvoFrase && <LogAlvosModal frase={alvoFrase} equipamentos={equips} token={token} toast={toast} onClose={()=>setAlvoFrase(null)} onSaved={()=>{ setAlvoFrase(null); load(); onChanged&&onChanged(); }}/>}
      {pedir && pedir.tipo === 'renAba' && <LogPromptModal titulo="Renomear aba" valor={pedir.alvo.nome} onOk={async (n)=>{ setPedir(null); await executarPedido.renAba(pedir.alvo, n); }} onClose={()=>setPedir(null)}/>}
      {pedir && pedir.tipo === 'delAba' && <LogConfirmModal titulo={`Apagar a aba "${pedir.alvo.nome}"?`} corpo={lang==='pt'?'Todas as frases dela vão junto.':'All its phrases go with it.'} textoOk={lang==='pt'?'Apagar':'Delete'} perigo onOk={async ()=>{ setPedir(null); await executarPedido.delAba(pedir.alvo); }} onClose={()=>setPedir(null)}/>}
      {pedir && pedir.tipo === 'subFrase' && <LogPromptModal titulo="Subgrupo da frase" label={lang==='pt'?'vazio remove o subgrupo':'empty removes the subgroup'} valor={pedir.alvo.subgrupo || ''} permiteVazio onOk={async (sg)=>{ setPedir(null); await executarPedido.subFrase(pedir.alvo, sg); }} onClose={()=>setPedir(null)}/>}
      {pedir && pedir.tipo === 'editFrase' && <LogPromptModal titulo="Editar frase" valor={pedir.alvo.texto} onOk={async (t)=>{ setPedir(null); await executarPedido.editFrase(pedir.alvo, t); }} onClose={()=>setPedir(null)}/>}
    </div>
  );
};
const LogEquipEditModal = ({equip, abas, token, toast, onClose, onSaved, onDeleted}) => {
  const lang = useT().lang;
  const [nome, setNome] = React.useState(equip.nome || '');
  const [img, setImg] = React.useState(equip.img_url || null);
  const [sel, setSel] = React.useState(() => new Set((equip.frases || []).map(f => f.id)));
  const [saving, setSaving] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const toggle = (id) => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const upImg = async (file) => {
    if (!file) return;
    const fd = new FormData(); fd.append('photo', file);
    const r = await fetch('/api/upload/photo', {method:'POST', headers:H(), body:fd});
    const d = await r.json();
    if (r.ok && d.url) { setImg(d.url); toast.push({icon:'check', title:'Imagem anexada'}); } else toast.push({icon:'x', title:(lang==='pt'?'Erro no upload':'Upload error')});
  };
  const salvar = async () => {
    if (!nome.trim()) { toast.push({icon:'x', title:'Nome obrigatório'}); return; }
    setSaving(true);
    try {
      await fetch(`/api/logbook/equipamentos/${equip.id}`, {method:'PATCH', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({nome:nome.trim(), img_url:img})});
      await fetch(`/api/logbook/equipamentos/${equip.id}/frases`, {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({frase_ids:[...sel]})});
      toast.push({icon:'check', title:(lang==='pt'?'Equipamento salvo':'Equipment saved')}); onSaved();
    } catch(e){ toast.push({icon:'x', title:(lang==='pt'?'Erro ao salvar':'Error saving')}); }
    finally { setSaving(false); }
  };
  const [confirmApagar, setConfirmApagar] = React.useState(false); // Mod 02
  const apagar = async () => { setConfirmApagar(false); await fetch(`/api/logbook/equipamentos/${equip.id}`, {method:'DELETE', headers:H()}); onDeleted(); };
  return (
    <div style={{position:'fixed',inset:0,zIndex:240,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}} onClick={onClose}>
      {confirmApagar && <div onClick={e=>e.stopPropagation()}><LogConfirmModal titulo={`Apagar o botão "${equip.nome}"?`} textoOk={lang==='pt'?'Apagar':'Delete'} perigo onOk={apagar} onClose={()=>setConfirmApagar(false)}/></div>}
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:460,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Botão do <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'sinótico':'synoptic'}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div className="label">{lang==='pt'?'Nome':'Name'}</div>
        <input className="input" value={nome} onChange={e=>setNome(e.target.value)} style={{width:'100%',boxSizing:'border-box',marginBottom:12}}/>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:14}}>
          <label className="btn btn-sm" style={{cursor:'pointer',margin:0}}>🖼️ Imagem (opcional)
            <input type="file" accept="image/*" style={{display:'none'}} onChange={e=>{ upImg(e.target.files[0]); e.target.value=''; }}/>
          </label>
          {img && <><img src={img} style={{width:34,height:34,borderRadius:6,objectFit:'cover'}}/><button className="btn btn-sm" onClick={()=>setImg(null)}>{lang==='pt'?'remover':'remove'}</button></>}
        </div>
        <div className="label" style={{marginBottom:6}}>Frases deste botão</div>
        <div style={{maxHeight:220,overflowY:'auto',border:'1px solid var(--line-soft)',borderRadius:12,padding:'8px 10px',marginBottom:16}}>
          {(!abas || abas.length === 0) ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Cadastre frases na biblioteca primeiro.':'Add phrases to the library first.'}</div> :
           abas.map(a => (
            <div key={a.id} style={{marginBottom:8}}>
              <div style={{fontFamily:'var(--fm)',fontSize:10,letterSpacing:0.5,color:'var(--muted)',textTransform:'uppercase',marginBottom:3}}>{a.nome}</div>
              {(a.frases||[]).map(f => {
                const on = sel.has(f.id);
                return (
                  <div key={f.id} onClick={()=>toggle(f.id)} style={{display:'flex',alignItems:'center',gap:8,padding:'5px 6px',borderRadius:8,cursor:'pointer',background:on?'rgba(31,87,85,0.08)':'transparent'}}>
                    <span style={{width:15,height:15,borderRadius:4,border:'1px solid '+(on?'var(--teal)':'var(--line-soft)'),background:on?'var(--teal)':'transparent',color:'#fff',fontSize:11,display:'grid',placeItems:'center',flexShrink:0}}>{on?'✓':''}</span>
                    {_logCorHex(f.cor) && <span style={{width:8,height:8,borderRadius:'50%',background:_logCorHex(f.cor),flexShrink:0}}/>}
                    <span style={{fontFamily:'var(--f)',fontSize:12.5}}>{f.texto}</span>
                  </div>
                );
              })}
            </div>
          ))}
        </div>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
          <button className="btn btn-sm" onClick={()=>setConfirmApagar(true)} style={{color:'var(--rose)',borderColor:'var(--rose)'}}>Apagar botão</button>
          <div style={{display:'flex',gap:8}}>
            <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
            <button className="btn btn-primary" onClick={salvar} disabled={saving}>{saving ? 'Salvando…' : 'Salvar'}</button>
          </div>
        </div>
      </div>
    </div>
  );
};

// ── Painel esquerdo: o sinótico (planta + botões, operação/edição) ──
const LogSinotico = ({token, toast, canManage, isMobile, abas, equipCores, onRegistrar, colapsavel}) => {
  const lang = useT().lang;
  const [sin, setSin] = React.useState(null);
  const [modo, setModo] = React.useState('operacao');
  const [editEquip, setEditEquip] = React.useState(null);
  const [opEquip, setOpEquip] = React.useState(null);
  const [aberto, setAberto] = React.useState(!colapsavel);
  const containerRef = React.useRef(null);
  const dragRef = React.useRef(null);
  const H = () => ({'Authorization':`Bearer ${token()}`});

  const load = () => fetch('/api/logbook/sinotico', {headers:H()}).then(r=>r.ok?r.json():{bg_url:null,equipamentos:[]}).then(setSin).catch(()=>setSin({bg_url:null,equipamentos:[]}));
  React.useEffect(() => { load(); }, []);

  // largura da planta (redimensionável tipo Word, persistida no backend)
  const [larg, setLarg] = React.useState(null);
  React.useEffect(() => { if (sin && sin.largura) setLarg(sin.largura); }, [sin && sin.largura]);
  const largRef = React.useRef(null);
  const largDown = (e) => { e.stopPropagation(); e.currentTarget.setPointerCapture(e.pointerId); largRef.current = { x0:e.clientX, w0:(containerRef.current ? containerRef.current.offsetWidth : (larg||300)) }; };
  const largMove = (e) => { if (!largRef.current) return; setLarg(Math.max(150, Math.min(1000, largRef.current.w0 + (e.clientX - largRef.current.x0)))); };
  const largUp = () => { if (!largRef.current) return; largRef.current = null; if (containerRef.current) fetch('/api/logbook/sinotico/largura', {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({largura:Math.round(containerRef.current.offsetWidth)})}); };

  const patchEquip = (id, body) => fetch(`/api/logbook/equipamentos/${id}`, {method:'PATCH', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify(body)});

  const upBg = async (file) => {
    if (!file) return;
    const fd = new FormData(); fd.append('photo', file);
    const r = await fetch('/api/upload/photo', {method:'POST', headers:H(), body:fd});
    const d = await r.json();
    if (r.ok && d.url) { await fetch('/api/logbook/sinotico/bg', {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({bg_url:d.url})}); toast.push({icon:'check', title:'Planta atualizada'}); load(); }
    else toast.push({icon:'x', title:(lang==='pt'?'Erro no upload':'Upload error')});
  };
  const [pedindoNome, setPedindoNome] = React.useState(false); // Mod 02
  const addEquip = async (nome) => {
    if (!nome || !nome.trim()) return;
    const r = await fetch('/api/logbook/equipamentos', {method:'POST', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({nome:nome.trim(), x:42, y:42, w:16, h:12})});
    if (r.ok) { load(); toast.push({icon:'check', title:(lang==='pt'?'Botão criado — arraste pra posicionar':'Button created — drag to position')}); }
  };

  const rectPct = (e) => { const rect = containerRef.current.getBoundingClientRect(); return { px:(e.clientX-rect.left)/rect.width*100, py:(e.clientY-rect.top)/rect.height*100 }; };
  const onDown = (equip, e, mode) => {
    if (modo !== 'edicao') return;
    e.stopPropagation(); e.currentTarget.setPointerCapture(e.pointerId);
    const { px, py } = rectPct(e);
    dragRef.current = { id: equip.id, mode, offX: px - equip.x, offY: py - equip.y, moved:false, last:{x:equip.x,y:equip.y,w:equip.w,h:equip.h} };
  };
  const onMove = (e) => {
    const d = dragRef.current; if (!d) return;
    const { px, py } = rectPct(e); d.moved = true;
    setSin(s => ({...s, equipamentos: s.equipamentos.map(eq => {
      if (eq.id !== d.id) return eq;
      let n;
      if (d.mode === 'resize') n = {...eq, w: Math.max(5, Math.min(100-eq.x, px-eq.x)), h: Math.max(5, Math.min(100-eq.y, py-eq.y))};
      else n = {...eq, x: Math.max(0, Math.min(100-eq.w, px-d.offX)), y: Math.max(0, Math.min(100-eq.h, py-d.offY))};
      d.last = {x:n.x, y:n.y, w:n.w, h:n.h}; return n;
    })}));
  };
  const onUp = (equip) => {
    const d = dragRef.current; if (!d) return; dragRef.current = null;
    if (d.moved) patchEquip(d.id, d.last);
    else setEditEquip(equip);
  };

  const escolherFrase = (equip, frase) => { onRegistrar(equip, frase); setOpEquip(null); };

  const header = (
    <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:aberto?12:0,gap:8,flexWrap:'wrap'}}>
      <div className="t-eyebrow" onClick={colapsavel?()=>setAberto(v=>!v):undefined} style={{cursor:colapsavel?'pointer':'default'}}>{colapsavel?(aberto?'▾ ':'▸ '):''}sinótico</div>
      {aberto && canManage && (
        <div style={{display:'flex',gap:6,alignItems:'center'}}>
          <div className="toggle" style={{display:'inline-flex'}}>
            <button className={modo==='operacao'?'active':''} onClick={()=>{setModo('operacao');setOpEquip(null);}}>Operação</button>
            <button className={modo==='edicao'?'active':''} onClick={()=>{setModo('edicao');setOpEquip(null);}}>Edição</button>
          </div>
        </div>
      )}
    </div>
  );

  const eqs = sin ? sin.equipamentos : [];

  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:14,padding:'12px 14px'}}>
      {header}
      {aberto && (
        <>
          {modo === 'edicao' && (
            <div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:10}}>
              <label className="btn btn-sm" style={{cursor:'pointer',margin:0}}>🖼️ {sin && sin.bg_url ? 'Trocar planta' : 'Definir planta'}
                <input type="file" accept="image/*" style={{display:'none'}} onChange={e=>{ upBg(e.target.files[0]); e.target.value=''; }}/>
              </label>
              <button className="btn btn-sm" onClick={addEquip}>＋ Equipamento</button>
              <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',alignSelf:'center'}}>arraste pra mover · canto pra redimensionar · clique pra editar</span>
            </div>
          )}
          {sin === null ? <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)',padding:20}}>{lang==='pt'?'Carregando…':'Loading…'}</div> : (
            <div ref={containerRef} style={{position:'relative',width:isMobile?'100%':((larg||sin.largura)?((larg||sin.largura)+'px'):'100%'),maxWidth:'100%',borderRadius:10,overflow:'hidden',background:'var(--cream-2)',minHeight:sin.bg_url?undefined:220,touchAction:'none',border:'1px solid var(--line-soft)'}}>
              {sin.bg_url ? <img src={sin.bg_url} style={{width:'100%',display:'block'}}/> :
                <div style={{padding:'50px 20px',textAlign:'center'}}>
                  <div style={{fontSize:26,marginBottom:6}}>🛳️</div>
                  <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)'}}>{canManage ? 'Defina a planta do navio no botão acima.' : 'Planta ainda não configurada.'}</div>
                </div>}
              {eqs.map(eq => {
                const cor = _logCorHex(equipCores[eq.id]);
                const editing = modo === 'edicao';
                return (
                  <div key={eq.id}
                    onClick={editing ? undefined : () => { (eq.frases && eq.frases.length) ? setOpEquip(eq) : toast.push({icon:'x', title:'Sem frases', body:'Vincule frases a este botão na Edição'}); }}
                    onPointerDown={editing ? (e)=>onDown(eq, e, 'move') : undefined}
                    onPointerMove={editing ? onMove : undefined}
                    onPointerUp={editing ? ()=>onUp(eq) : undefined}
                    title={eq.nome}
                    style={{position:'absolute',left:eq.x+'%',top:eq.y+'%',width:eq.w+'%',height:eq.h+'%',minWidth:32,minHeight:24,borderRadius:8,border:`3px solid ${cor||'var(--ink)'}`,background:eq.img_url?`center/cover url(${eq.img_url})`:'var(--surface)',boxShadow:cor?`0 0 12px ${cor}66`:'var(--shadow-sm)',cursor:editing?'grab':'pointer',display:'flex',alignItems:'flex-end',justifyContent:'center',overflow:'hidden',touchAction:'none',userSelect:'none'}}>
                    <span style={{width:'100%',textAlign:'center',fontFamily:'var(--fm)',fontSize:10.5,fontWeight:600,color:'var(--ink)',background:'rgba(255,255,255,0.82)',padding:'2px 3px',lineHeight:1.15,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{eq.nome}</span>
                    {editing && <span onPointerDown={(e)=>onDown(eq, e, 'resize')} title="redimensionar" style={{position:'absolute',right:-1,bottom:-1,width:18,height:18,background:'var(--accent)',border:'2px solid var(--surface)',borderRadius:'6px 2px 6px 2px',cursor:'nwse-resize',touchAction:'none'}}/>}
                  </div>
                );
              })}
              {/* handle pra redimensionar a planta (só edição, com fundo definido) */}
              {modo === 'edicao' && sin.bg_url && <span onPointerDown={largDown} onPointerMove={largMove} onPointerUp={largUp} title="arraste pra redimensionar a planta" style={{position:'absolute',right:0,bottom:0,width:24,height:24,background:'var(--sky)',border:'2px solid var(--surface)',borderRadius:'8px 0 0 0',cursor:'ew-resize',touchAction:'none',zIndex:5,display:'grid',placeItems:'center',color:'#fff',fontSize:11}}>⇔</span>}
            </div>
          )}

          {/* popover de frases (operação) */}
          {opEquip && (
            <div style={{position:'fixed',inset:0,zIndex:235,background:'rgba(31,42,46,0.45)',display:'flex',alignItems:'center',justifyContent:'center',padding:16} } onClick={()=>setOpEquip(null)}>
              <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:360,background:'var(--cream)',borderRadius:20,padding:'20px 22px'}}>
                <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
                  <div style={{fontFamily:'var(--fs)',fontSize:19}}>{opEquip.nome}</div>
                  <button className="btn btn-sm" onClick={()=>setOpEquip(null)}>{lang==='pt'?'fechar':'close'}</button>
                </div>
                <div style={{display:'grid',gap:7}}>
                  {opEquip.frases.map(f => {
                    const cor = _logCorHex(f.cor);
                    return (
                      <button key={f.id} onClick={()=>escolherFrase(opEquip, f)} style={{textAlign:'left',fontFamily:'var(--f)',fontSize:13,padding:'10px 12px',borderRadius:10,cursor:'pointer',background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`4px solid ${cor||'var(--line-soft)'}`}}>{f.texto}</button>
                    );
                  })}
                </div>
              </div>
            </div>
          )}
        </>
      )}

      {editEquip && <LogEquipEditModal equip={editEquip} abas={abas} token={token} toast={toast} onClose={()=>setEditEquip(null)} onSaved={()=>{ setEditEquip(null); load(); }} onDeleted={()=>{ setEditEquip(null); load(); }}/>}
      {pedindoNome && <LogPromptModal titulo={lang==='pt'?'Novo equipamento':'New equipment'} placeholder={lang==='pt'?'Nome (ex: Guindaste)':'Name (e.g.: Crane)'} onOk={async (n)=>{ setPedindoNome(false); await addEquip(n); }} onClose={()=>setPedindoNome(false)}/>}
    </div>
  );
};

// ── Registros horários: painel 00–23 + captura relâmpago ──
const _LOG_HORA_CAMPOS = [
  { grupo: 'Meteorologia', campos: [
    {k:'vento_dir', l:'Vento dir', u:'°', dec:0, max:360, ph:'###'},
    {k:'vento_int', l:'Vento int', u:"'", dec:0, max:99, ph:'##'},
    {k:'corrente_dir', l:'Corrente dir', u:'°', dec:0, max:360, ph:'###'},
    {k:'corrente_int', l:'Corrente int', u:"'", dec:1, max:6, ph:'#.#'},
    {k:'swell_dir', l:'Swell dir', u:'°', dec:0, max:360, ph:'###'},
    {k:'swell_int', l:'Swell alt', u:'m', dec:1, max:9.9, ph:'#.#'},
  ]},
  { grupo: 'Navegação', campos: [
    {k:'heading', l:'Heading', u:'°', dec:0, max:360, ph:'###'},
    {k:'speed', l:'Speed', u:"'", dec:1, max:40, ph:'#.#'},
    {k:'rot', l:'ROT', u:'°', dec:0, max:200, ph:'##'},
    {k:'rotation_center', l:'Rot. center', u:'', tipo:'texto', ph:'texto'},
  ]},
  { grupo: 'Ganhos de DP', campos: [
    {k:'dp_surge', l:'Surge', u:'', dec:0, neg:true, max:10, ph:'##'},
    {k:'dp_sway', l:'Sway', u:'', dec:0, neg:true, max:10, ph:'##'},
    {k:'dp_yaw', l:'Yaw', u:'', dec:0, neg:true, max:10, ph:'##'},
  ]},
];
// sanitiza conforme o formato: casas decimais, sem negativo (exceto DP), texto livre
const _logSanit = (campo, raw) => {
  if (campo.tipo === 'texto') return raw;
  let v = String(raw).replace(campo.neg ? /[^0-9.\-]/g : /[^0-9.]/g, '');
  if (campo.dec === 0) v = v.replace(/\./g, '');
  const parts = v.split('.'); if (parts.length > 2) v = parts[0] + '.' + parts.slice(1).join('');
  if (campo.neg) { const neg = v.indexOf('-') === 0; v = v.replace(/-/g, ''); if (neg) v = '-' + v; }
  if (campo.dec > 0) { const p = v.split('.'); if (p[1] && p[1].length > campo.dec) v = p[0] + '.' + p[1].slice(0, campo.dec); }
  return v;
};
const _logHH = (h) => String(h).padStart(2, '0') + ':00';

const LogScrubber = ({campo, value, onChange}) => {
  const max = campo.max != null ? campo.max : 99;
  const min = campo.neg ? -max : 0;
  const dec = campo.dec || 0;
  const step = dec === 1 ? 0.1 : 1;
  const num = (value === '' || value == null) ? null : parseFloat(value);
  const ref = React.useRef(null);
  const drag = React.useRef(null);
  const clamp = (v) => Math.min(max, Math.max(min, v));
  const snap = (v) => parseFloat((Math.round(v / step) * step).toFixed(dec));
  const setV = (v) => onChange(String(snap(clamp(v))));
  const onDown = (e) => {
    e.preventDefault();
    drag.current = { x: e.clientX, v: (num == null ? min : num), per: (max - min) / 300 };
    try { ref.current.setPointerCapture(e.pointerId); } catch(_){}
  };
  const onMove = (e) => { if (!drag.current) return; setV(drag.current.v + (e.clientX - drag.current.x) * drag.current.per); };
  const onUp = (e) => { drag.current = null; try { ref.current.releasePointerCapture(e.pointerId); } catch(_){} };
  const bump = (d) => setV((num == null ? min : num) + d * step);
  const pct = num == null ? 0 : ((clamp(num) - min) / (max - min)) * 100;
  const btn = {width:30,flexShrink:0,border:'1px solid var(--line-soft)',background:'var(--surface)',borderRadius:8,cursor:'pointer',fontFamily:'var(--fm)',fontSize:16,color:'var(--muted)',padding:'6px 0',lineHeight:1};
  return (
    <div style={{display:'flex',alignItems:'stretch',gap:5}}>
      <button type="button" onClick={()=>bump(-1)} style={btn}>−</button>
      <div ref={ref} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}
        title="arraste para ajustar"
        style={{flex:1,position:'relative',touchAction:'none',cursor:'ew-resize',background:'var(--cream-2)',border:'1px solid var(--line-soft)',borderRadius:8,padding:'8px 4px',textAlign:'center',userSelect:'none',overflow:'hidden',minWidth:0}}>
        <div style={{position:'absolute',left:0,top:0,bottom:0,width:pct+'%',background:campo.cor||'var(--accent)',opacity:0.16,pointerEvents:'none'}}/>
        <span style={{position:'relative',fontFamily:'var(--fm)',fontSize:15,fontWeight:600,color:num==null?'var(--muted)':'var(--ink)',pointerEvents:'none'}}>
          {num == null ? '—' : num.toFixed(dec)}{campo.u && <span style={{fontSize:10,color:'var(--muted)',marginLeft:2}}>{campo.u}</span>}
        </span>
      </div>
      <button type="button" onClick={()=>bump(1)} style={btn}>+</button>
    </div>
  );
};

// ─── Relógio circular de seleção de horário (24h) ───
const LogRelogio = ({ value, onChange, onClose }) => {
  const lang = useT().lang;
  const parse = (v) => { const m=/^(\d{1,2}):(\d{2})$/.exec(v||''); if(m) return [Math.min(23,+m[1]), Math.min(59,+m[2])]; const n=new Date(); return [n.getHours(), 0]; };
  const init = parse(value);
  const [hora, setHora] = React.useState(init[0]);
  const [minuto, setMinuto] = React.useState(init[1]);
  const [modo, setModo] = React.useState('hora'); // 'hora' | 'minuto'
  const [arrastando, setArrastando] = React.useState(false);
  const svgRef = React.useRef(null);
  const cx=130, cy=130, Re=94, Ri=58;
  const hh = String(hora).padStart(2,'0'), mm = String(minuto).padStart(2,'0');

  const posXY = (val, count, r) => { const ang=(val%count)*(2*Math.PI/count); return [cx + r*Math.sin(ang), cy - r*Math.cos(ang)]; };

  const aplicar = (clientX, clientY) => {
    const svg = svgRef.current; if(!svg) return;
    const rect = svg.getBoundingClientRect();
    const px = (clientX-rect.left)*(260/rect.width);
    const py = (clientY-rect.top)*(260/rect.height);
    const dx=px-cx, dy=py-cy;
    let ang=Math.atan2(dx,-dy); if(ang<0) ang+=2*Math.PI;
    if(modo==='hora'){
      const p=Math.round(ang/(2*Math.PI/12))%12;
      const raio=Math.hypot(dx,dy);
      const interno = raio < (Re+Ri)/2;
      setHora(interno ? p+12 : p);
    } else {
      setMinuto(Math.round(ang/(2*Math.PI/60))%60);
    }
  };
  const down=(e)=>{ setArrastando(true); const t=e.touches?e.touches[0]:e; aplicar(t.clientX,t.clientY); };
  const move=(e)=>{ if(!arrastando) return; const t=e.touches?e.touches[0]:e; aplicar(t.clientX,t.clientY); e.preventDefault&&e.preventDefault(); };
  const up=()=>{ if(!arrastando) return; setArrastando(false); if(modo==='hora') setModo('minuto'); };

  // ponteiro
  const [px,py] = modo==='hora' ? posXY(hora%12, 12, hora<12?Re:Ri) : posXY(minuto, 60, Re);
  const numsHoraExt = Array.from({length:12},(_,i)=>i);       // 0..11
  const numsHoraInt = Array.from({length:12},(_,i)=>i+12);    // 12..23
  const numsMin = Array.from({length:12},(_,i)=>i*5);         // 0,5,...,55

  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:300,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'center',justifyContent:'center',padding:'5vh 16px'}}>
      <div onClick={e=>e.stopPropagation()} style={{background:'var(--cream)',borderRadius:24,padding:'20px 22px',width:'100%',maxWidth:320,boxShadow:'var(--shadow-lg)'}}>
        {/* Display HH:MM */}
        <div style={{display:'flex',justifyContent:'center',alignItems:'baseline',gap:4,marginBottom:16}}>
          <span onClick={()=>setModo('hora')} style={{fontFamily:'var(--fs)',fontSize:44,lineHeight:1,cursor:'pointer',color:modo==='hora'?'var(--accent)':'var(--muted)',padding:'2px 8px',borderRadius:10,background:modo==='hora'?'rgba(217,105,75,0.08)':'transparent'}}>{hh}</span>
          <span style={{fontFamily:'var(--fs)',fontSize:40,color:'var(--muted)'}}>:</span>
          <span onClick={()=>setModo('minuto')} style={{fontFamily:'var(--fs)',fontSize:44,lineHeight:1,cursor:'pointer',color:modo==='minuto'?'var(--accent)':'var(--muted)',padding:'2px 8px',borderRadius:10,background:modo==='minuto'?'rgba(217,105,75,0.08)':'transparent'}}>{mm}</span>
        </div>
        {/* Mostrador */}
        <svg ref={svgRef} viewBox="0 0 260 260" style={{width:'100%',maxWidth:280,margin:'0 auto',display:'block',touchAction:'none',cursor:'pointer',userSelect:'none'}}
          onMouseDown={down} onMouseMove={move} onMouseUp={up} onMouseLeave={up}
          onTouchStart={down} onTouchMove={move} onTouchEnd={up}>
          <circle cx={cx} cy={cy} r={122} fill="var(--surface)" stroke="var(--line-soft)" strokeWidth="1"/>
          {/* ponteiro */}
          <line x1={cx} y1={cy} x2={px.toFixed(1)} y2={py.toFixed(1)} stroke="var(--accent)" strokeWidth="2"/>
          <circle cx={cx} cy={cy} r={3.5} fill="var(--accent)"/>
          <circle cx={px.toFixed(1)} cy={py.toFixed(1)} r={16} fill="var(--accent)"/>
          {modo==='hora' ? <>
            {numsHoraExt.map(n=>{ const [x,y]=posXY(n,12,Re); const on=hora===n; return <text key={'e'+n} x={x.toFixed(1)} y={(y+4).toFixed(1)} textAnchor="middle" style={{fontFamily:'var(--fm)',fontSize:14,fontWeight:on?700:400,fill:on?'#fff':'var(--ink)',pointerEvents:'none'}}>{String(n).padStart(2,'0')}</text>; })}
            {numsHoraInt.map(n=>{ const [x,y]=posXY(n-12,12,Ri); const on=hora===n; return <text key={'i'+n} x={x.toFixed(1)} y={(y+4).toFixed(1)} textAnchor="middle" style={{fontFamily:'var(--fm)',fontSize:12,fontWeight:on?700:400,fill:on?'#fff':'var(--muted)',pointerEvents:'none'}}>{n}</text>; })}
          </> : <>
            {numsMin.map(n=>{ const [x,y]=posXY(n,60,Re); const on=minuto===n; return <text key={'m'+n} x={x.toFixed(1)} y={(y+4).toFixed(1)} textAnchor="middle" style={{fontFamily:'var(--fm)',fontSize:14,fontWeight:on?700:400,fill:on?'#fff':'var(--ink)',pointerEvents:'none'}}>{String(n).padStart(2,'0')}</text>; })}
          </>}
        </svg>
        {/* Ações */}
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:16}}>
          <button className="btn btn-sm" onClick={()=>{ const n=new Date(); setHora(n.getHours()); setMinuto(n.getMinutes()); setModo('hora'); }}>{lang==='pt'?'agora':'now'}</button>
          <div style={{display:'flex',gap:8}}>
            <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'cancelar':'cancel'}</button>
            <button className="btn btn-primary btn-sm" onClick={()=>onChange(`${hh}:${mm}`)}>OK</button>
          </div>
        </div>
      </div>
    </div>
  );
};

// Campo que exibe HH:MM e abre o relógio ao clicar
const LogHoraField = ({ value, onChange, placeholder, style, allowClear }) => {
  const [aberto, setAberto] = React.useState(false);
  return (
    <span style={{display:'inline-flex',alignItems:'center',gap:4}}>
      <button type="button" onClick={()=>setAberto(true)} title="escolher horário" style={{display:'inline-flex',alignItems:'center',gap:6,fontFamily:'var(--fm)',fontSize:12,padding:'6px 10px',border:'1px solid var(--line-soft)',borderRadius:8,background:'var(--surface)',color:value?'var(--ink)':'var(--muted)',cursor:'pointer',...(style||{})}}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
        {value || placeholder || '--:--'}
      </button>
      {allowClear && value && <button type="button" onClick={()=>onChange('')} title="limpar" style={{border:'none',background:'transparent',color:'var(--muted)',cursor:'pointer',fontSize:15,lineHeight:1,padding:'0 2px'}}>×</button>}
      {aberto && <LogRelogio value={value} onChange={(v)=>{ onChange(v); setAberto(false); }} onClose={()=>setAberto(false)}/>}
    </span>
  );
};

const LogHoraModal = ({data, hora, registro, ultimo, token, toast, onClose, onSaved}) => {
  const lang = useT().lang;
  const [modo, setModo] = React.useState(registro && registro.motivo ? 'justificar' : 'preencher');
  const preencheuDeUltimo = !registro && !!ultimo;
  const [vals, setVals] = React.useState(() => {
    const base = registro || (ultimo || null);
    const v = {}; _LOG_HORA_CAMPOS.forEach(g => g.campos.forEach(c => { v[c.k] = (base && base[c.k] != null) ? String(base[c.k]) : ''; })); return v;
  });
  const [motivo, setMotivo] = React.useState(registro && registro.motivo ? registro.motivo : '');
  const [saving, setSaving] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const set = (k, val) => setVals(s => ({...s, [k]: val}));

  const salvar = async () => {
    setSaving(true);
    try {
      const body = { data, hora };
      if (modo === 'justificar') body.motivo = motivo.trim() || 'não preenchido';
      else {
        _LOG_HORA_CAMPOS.forEach(g => g.campos.forEach(c => {
          if (c.tipo === 'texto') { body[c.k] = vals[c.k] && vals[c.k].trim() ? vals[c.k].trim() : null; return; }
          if (vals[c.k] === '' || vals[c.k] === '-' || vals[c.k] === '.') { body[c.k] = null; return; }
          let n = Number(vals[c.k]);
          if (!isNaN(n) && c.max != null && n > c.max) n = c.max;
          body[c.k] = isNaN(n) ? null : n;
        }));
        body.preenchido = true;
      }
      const r = await fetch('/api/logbook/horarios', {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify(body)});
      if (r.ok) { toast.push({icon:'check', title:`${_logHH(hora)} salva`}); onSaved(); }
      else toast.push({icon:'x', title:(lang==='pt'?'Erro ao salvar':'Error saving')});
    } catch(e){ toast.push({icon:'x', title:(lang==='pt'?'Erro de conexão':'Connection error')}); }
    finally { setSaving(false); }
  };
  const [confirmLimpar, setConfirmLimpar] = React.useState(false); // Mod 02
  const limpar = async () => {
    setConfirmLimpar(false);
    await fetch(`/api/logbook/horarios?data=${data}&hora=${hora}`, {method:'DELETE', headers:H()});
    toast.push({icon:'check', title:'Hora limpa'}); onSaved();
  };

  return (
    <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:520,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}>
          <div style={{fontFamily:'var(--fs)',fontSize:22}}>Hora <em style={{fontStyle:'italic',color:'var(--accent)'}}>{_logHH(hora)}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>

        <div className="toggle" style={{display:'inline-flex',marginBottom:16}}>
          <button className={modo==='preencher'?'active':''} onClick={()=>setModo('preencher')}>Preencher</button>
          <button className={modo==='justificar'?'active':''} onClick={()=>setModo('justificar')}>Não preencher</button>
        </div>

        {modo === 'justificar' ? (
          <div style={{marginBottom:18}}>
            <div className="label">{lang==='pt'?'Motivo':'Reason'}</div>
            <div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:8}}>
              {['em porto','em trânsito'].map(m => <button key={m} className="btn btn-sm" onClick={()=>setMotivo(m)} style={motivo===m?{borderColor:'var(--sky)',color:'var(--sky)'}:{}}>{m}</button>)}
            </div>
            <input className="input" value={motivo} onChange={e=>setMotivo(e.target.value)} placeholder="motivo…" style={{width:'100%',boxSizing:'border-box'}}/>
          </div>
        ) : (
          <div style={{marginBottom:18}}>
            {preencheuDeUltimo && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--sky)',background:'rgba(92,141,174,0.1)',borderRadius:8,padding:'7px 10px',marginBottom:12}}>↻ Pré-preenchido com a última hora registrada — ajuste só o que mudou.</div>}
            {_LOG_HORA_CAMPOS.map(g => (
              <div key={g.grupo} style={{marginBottom:14}}>
                <div className="t-eyebrow" style={{marginBottom:8}}>{g.grupo}</div>
                <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8}}>
                  {g.campos.map(c => (
                    <div key={c.k}>
                      <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginBottom:3}}>{c.l}{c.u?` · ${c.u}`:''}</div>
                      {c.tipo === 'texto'
                        ? <input className="input" type="text" placeholder={c.ph} value={vals[c.k]} onChange={e=>set(c.k, e.target.value)} style={{width:'100%',boxSizing:'border-box',fontFamily:'var(--fm)'}}/>
                        : <LogScrubber campo={c} value={vals[c.k]} onChange={v=>set(c.k, v)}/>}
                    </div>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}

        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
          {registro ? <button className="btn btn-sm" onClick={()=>setConfirmLimpar(true)} style={{color:'var(--rose)',borderColor:'var(--rose)'}}>{lang==='pt'?'Limpar':'Clear'}</button> : <span/>}
          <div style={{display:'flex',gap:8}}>
            <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
            <button className="btn btn-primary" onClick={salvar} disabled={saving}>{saving ? 'Salvando…' : 'Salvar'}</button>
          </div>
        </div>
        {confirmLimpar && <LogConfirmModal titulo={lang==='pt'?`Limpar a hora ${_logHH(hora)}?`:`Clear the ${_logHH(hora)} hour?`} corpo={lang==='pt'?'Os dados dessa hora somem do painel e dos gráficos do dia.':'The data for this hour disappears from the panel and the day graphs.'} textoOk={lang==='pt'?'Limpar':'Clear'} perigo onOk={limpar} onClose={()=>setConfirmLimpar(false)}/>}
      </div>
    </div>
  );
};

// ── Gráfico de intensidade + direção ao longo do dia (altura fixa, escala dinâmica) ──
const LogGraficoDir = ({pontos, cor, titulo, unidade, base, passo, casas}) => {
  const ref = React.useRef(null);
  const [w, setW] = React.useState(268);
  React.useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const upd = () => setW(Math.max(160, el.clientWidth || 268));
    upd();
    let ro; try { ro = new ResizeObserver(upd); ro.observe(el); } catch(e){}
    return () => { try { ro && ro.disconnect(); } catch(e){} };
  }, []);
  const H = 72, padT = 9, padB = 14, padL = 9, padR = 9;
  const plotH = H - padT - padB;
  const ord = (pontos || []).slice().sort((a,b)=>a.h-b.h);
  let topo = base; const maxV = ord.length ? Math.max(...ord.map(p=>p.v)) : 0;
  while (maxV > topo) topo += passo;
  const X = (h) => padL + (h/23) * (w - padL - padR);
  const Y = (v) => padT + (1 - Math.min(v,topo)/topo) * plotH;
  const grid = []; for (let g = passo; g < topo - 0.0001; g += passo) grid.push(g);
  const ult = ord.length ? ord[ord.length-1] : null;
  const GRID = 'var(--line)';
  return (
    <div ref={ref} style={{width:'100%'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',marginBottom:1}}>
        <span style={{fontFamily:'var(--fm)',fontSize:9,textTransform:'uppercase',letterSpacing:0.5,color:'var(--muted)'}}>{titulo}</span>
        {ult && <span style={{fontFamily:'var(--fs)',fontSize:13,color:cor,lineHeight:1}}>{ult.v}<span style={{fontSize:8,color:'var(--muted)'}}> {unidade}{ult.dir!=null?` · ${String(ult.dir).padStart(3,'0')}°`:''}</span></span>}
      </div>
      <svg width={w} height={H} style={{display:'block',maxWidth:'100%'}}>
        {grid.map((g,i)=><line key={'h'+i} x1={padL} y1={Y(g).toFixed(1)} x2={w-padR} y2={Y(g).toFixed(1)} stroke={GRID} strokeWidth={1}/>)}
        {[...grid, topo].map((g,i)=><text key={'gl'+i} x={w-padR-1} y={(Y(g)-2).toFixed(1)} textAnchor="end" style={{fontFamily:'var(--fm)',fontSize:6,fill:'var(--muted)',opacity:0.6}}>{g}</text>)}
        {Array.from({length:24},(_,h)=><line key={'v'+h} x1={X(h).toFixed(1)} y1={padT} x2={X(h).toFixed(1)} y2={H-padB} stroke={GRID} strokeWidth={0.7} strokeDasharray="2 3" opacity={0.55}/>)}
        {[0,6,12,18,23].map(h=><text key={'t'+h} x={X(h).toFixed(1)} y={H-3} textAnchor="middle" style={{fontFamily:'var(--fm)',fontSize:7,fill:'var(--muted)'}}>{String(h).padStart(2,'0')}</text>)}
        {ord.map((p,i)=>(
          <g key={i} transform={`translate(${X(p.h).toFixed(1)},${Y(p.v).toFixed(1)})`}>
            {p.dir!=null && <g transform={`rotate(${(Number(p.dir)+180)%360})`}><path d="M 0 -9.5 L 3.6 -4.3 L -3.6 -4.3 Z" fill={cor}/></g>}
            <circle cx={0} cy={0} r={3.2} fill={cor}/>
          </g>
        ))}
      </svg>
    </div>
  );
};

const LogGraficoConfigModal = ({ cfg, onSave, onClose }) => {
  const lang = useT().lang;
  const [c, setC] = React.useState(JSON.parse(JSON.stringify(cfg)));
  const graficos = [
    {k:'vento', nome:'Vento', cor:'var(--sky)', un:'nós'},
    {k:'corrente', nome:'Corrente', cor:'var(--teal)', un:'nós'},
    {k:'swell', nome:'Swell', cor:'var(--leaf)', un:'m'},
  ];
  const upd = (g, campo, val) => { const n = parseFloat(val); setC(prev => ({...prev, [g]: {...prev[g], [campo]: isNaN(n)?'':n}})); };
  const salvar = () => {
    const limpo = {};
    for (const g of graficos) {
      let topo = Number(c[g.k].topo)||_LOG_GRAPH_DEFAULTS[g.k].topo;
      let passo = Number(c[g.k].passo)||_LOG_GRAPH_DEFAULTS[g.k].passo;
      if (passo <= 0) passo = _LOG_GRAPH_DEFAULTS[g.k].passo;
      if (topo < passo) topo = passo;
      limpo[g.k] = { topo, passo };
    }
    onSave(limpo);
  };
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:240,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'6vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:440,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Ajustar <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'gráficos':'charts'}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:16,lineHeight:1.5}}>Vale pra tela e pro relatório. <b>Topo do Y</b> = até quanto a escala vai (sobe sozinha se um valor passar). <b>Linha</b> = de quanto em quanto aparece a linha cinza.</div>
        <div style={{display:'grid',gap:12}}>
          {graficos.map(g => (
            <div key={g.k} style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`3px solid ${g.cor}`,borderRadius:12,padding:'12px 14px'}}>
              <div style={{display:'flex',alignItems:'center',gap:7,marginBottom:9}}>
                <span style={{width:9,height:9,borderRadius:'50%',background:g.cor}}/>
                <span style={{fontFamily:'var(--fm)',fontSize:13,fontWeight:600}}>{g.nome}</span>
                <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>({g.un})</span>
              </div>
              <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10}}>
                <div>
                  <div className="label" style={{marginBottom:3}}>Topo do Y</div>
                  <input className="input" type="number" min="1" value={c[g.k].topo} onChange={e=>upd(g.k,'topo',e.target.value)} style={{width:'100%',boxSizing:'border-box'}}/>
                </div>
                <div>
                  <div className="label" style={{marginBottom:3}}>Linha a cada</div>
                  <input className="input" type="number" min="1" value={c[g.k].passo} onChange={e=>upd(g.k,'passo',e.target.value)} style={{width:'100%',boxSizing:'border-box'}}/>
                </div>
              </div>
            </div>
          ))}
        </div>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:16}}>
          <button className="btn btn-sm" onClick={()=>setC(JSON.parse(JSON.stringify(_LOG_GRAPH_DEFAULTS)))}>{lang==='pt'?'restaurar padrão':'restore default'}</button>
          <div style={{display:'flex',gap:8}}>
            <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
            <button className="btn btn-primary" onClick={salvar}>Salvar</button>
          </div>
        </div>
      </div>
    </div>
  );
};

const LogHorasBar = ({data, token, toast, soHoras}) => {
  const lang = useT().lang;
  const [regs, setRegs] = React.useState(null);
  const [ultimo, setUltimo] = React.useState(null);
  const [horaSel, setHoraSel] = React.useState(null);
  const [graphCfg, setGraphCfg] = React.useState(_logGraphCfg());
  const [cfgOpen, setCfgOpen] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const load = () => {
    fetch(`/api/logbook/horarios?data=${data}`, {headers:H()}).then(r=>r.ok?r.json():[]).then(setRegs).catch(()=>setRegs([]));
    fetch('/api/logbook/horarios/ultimo', {headers:H()}).then(r=>r.ok?r.json():null).then(setUltimo).catch(()=>{});
  };
  React.useEffect(() => { load(); }, [data]);
  const hoje = _logHojeStr();
  const horaAgora = new Date().getHours();
  const regDe = (h) => (regs || []).find(r => r.hora === h) || null;
  const estado = (h) => { const r = regDe(h); if (r && r.preenchido) return 'preenchido'; if (r && r.motivo) return 'justificado'; if (data < hoje) return 'atrasado'; if (data > hoje) return 'futuro'; return h <= horaAgora ? 'atrasado' : 'futuro'; }; // a hora corrente já é exigível
  const CORES = { preenchido:'var(--leaf)', justificado:'var(--sky)', atrasado:'var(--rose)', futuro:'var(--cream-3)' };
  const feitas = (regs || []).filter(r => r.preenchido || r.motivo).length;
  const cheios = (regs || []).filter(r => r.preenchido);
  const ventoPts = cheios.filter(r => r.vento_int != null).map(r => ({h:r.hora, v:r.vento_int, dir:r.vento_dir}));
  const corrPts = cheios.filter(r => r.corrente_int != null).map(r => ({h:r.hora, v:r.corrente_int, dir:r.corrente_dir}));
  const swellPts = cheios.filter(r => r.swell_int != null).map(r => ({h:r.hora, v:r.swell_int, dir:r.swell_dir}));
  const temMeteo = ventoPts.length || corrPts.length || swellPts.length;
  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:12,padding:'11px 13px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:9}}>
        <div className="t-eyebrow">{lang==='pt'?'registros horários':'hourly records'} · {feitas}/24</div>
        <div style={{display:'flex',gap:6}}>
          {Object.keys(CORES).map(st => <span key={st} title={st} style={{width:9,height:9,borderRadius:2,background:CORES[st],border:st==='futuro'?'1px solid var(--line-soft)':'none'}}/>)}
        </div>
      </div>
      <div style={{display:'flex',flexDirection:'column',gap:4,alignItems:'center'}}>
        {[0,1,2].map(row => (
          <div key={row} style={{display:'flex',gap:4,width:'100%'}}>
            {Array.from({length:8}, (_, i) => {
              const h = row*8 + i;
              const st = estado(h); const claro = st === 'futuro';
              return <button key={h} onClick={()=>setHoraSel(h)} title={`${String(h).padStart(2,'0')}:00 · ${st}`} style={{flex:'1 1 0',minWidth:0,maxWidth:30,height:28,borderRadius:5,cursor:'pointer',border:'1px solid '+(claro?'var(--line-soft)':CORES[st]),background:claro?'var(--surface)':CORES[st],color:claro?'var(--muted)':'#fff',fontFamily:'var(--fm)',fontSize:11,fontWeight:600,display:'flex',alignItems:'center',justifyContent:'center',padding:0}}>{String(h).padStart(2,'0')}</button>;
            })}
          </div>
        ))}
      </div>
      {temMeteo && !soHoras && (
        <div style={{marginTop:12,paddingTop:11,borderTop:'1px solid var(--cream-2)',display:'grid',gap:10}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
            <div className="t-eyebrow">{lang==='pt'?'meteo & ambiente':'weather & environment'}</div>
            <button onClick={()=>setCfgOpen(true)} title="ajustar escala dos gráficos" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:28,height:28,cursor:'pointer',fontSize:14,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>
          </div>
          <LogGraficoDir pontos={ventoPts} cor="var(--sky)" titulo="Vento" unidade="'" base={graphCfg.vento.topo} passo={graphCfg.vento.passo}/>
          <LogGraficoDir pontos={corrPts} cor="var(--teal)" titulo="Corrente" unidade="'" base={graphCfg.corrente.topo} passo={graphCfg.corrente.passo}/>
          <LogGraficoDir pontos={swellPts} cor="var(--leaf)" titulo="Swell" unidade="m" base={graphCfg.swell.topo} passo={graphCfg.swell.passo}/>
        </div>
      )}
      {cfgOpen && <LogGraficoConfigModal cfg={graphCfg} onClose={()=>setCfgOpen(false)} onSave={(c)=>{ setGraphCfg(c); _logSaveGraphCfg(c); setCfgOpen(false); toast.push({icon:'check', title:'Gráficos ajustados'}); }}/>}
      {horaSel !== null && <LogHoraModal data={data} hora={horaSel} registro={regDe(horaSel)} ultimo={ultimo} token={token} toast={toast} onClose={()=>setHoraSel(null)} onSaved={()=>{ setHoraSel(null); load(); _logInvalidarHorarios(data); }}/>}
    </div>
  );
};

const LogDprModal = ({data, token, toast, onGerar, onClose}) => {
  const lang = useT().lang;
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const [cfg, setCfg] = React.useState({ navio: '', cliente: '', campo: '' });
  const [resumo, setResumo] = React.useState('');
  const [preparadoPor, setPreparadoPor] = React.useState('');
  const [carregado, setCarregado] = React.useState(false);
  const [salvando, setSalvando] = React.useState(false);
  React.useEffect(() => {
    fetch(`/api/logbook/dpr?data=${data}`, {headers:H()}).then(r=>r.ok?r.json():null).then(d => {
      if (d) { setCfg({ navio: d.cfg.navio || 'MPSV Genesis I', cliente: d.cfg.cliente || '', campo: d.cfg.campo || '' }); setResumo(d.resumo || ''); setPreparadoPor(d.preparado_por || ''); }
      setCarregado(true);
    }).catch(()=>setCarregado(true));
  }, [data]);
  const salvarEGerar = async () => {
    setSalvando(true);
    try { await fetch('/api/logbook/dpr', {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({ data, resumo, preparado_por: preparadoPor, cfg })}); } catch(e){}
    setSalvando(false);
    onGerar(cfg, resumo, preparadoPor);
    onClose();
  };
  return (
    <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:520,background:'var(--cream)',borderRadius:20,padding:'22px 24px'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:20,marginBottom:2}}>📊 Daily Progress Report</div>
        <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:14}}>{lang==='pt'?`relatório cliente-facing do dia ${data} — visual corporativo, pronto pra PDF`:`client-facing report for ${data}`}</div>
        {!carregado ? <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)',padding:'20px 0'}}>carregando…</div> : (
          <div style={{display:'grid',gap:11}}>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:9}}>
              <div><div className="label">{lang==='pt'?'Navio':'Vessel'}</div><input className="input" value={cfg.navio} onChange={e=>setCfg(c=>({...c, navio:e.target.value}))} style={{width:'100%',boxSizing:'border-box'}}/></div>
              <div><div className="label">{lang==='pt'?'Cliente':'Client'}</div><input className="input" value={cfg.cliente} onChange={e=>setCfg(c=>({...c, cliente:e.target.value}))} placeholder="Petrobras, Shell…" style={{width:'100%',boxSizing:'border-box'}}/></div>
            </div>
            <div><div className="label">{lang==='pt'?'Campo / Projeto':'Field / Project'}</div><input className="input" value={cfg.campo} onChange={e=>setCfg(c=>({...c, campo:e.target.value}))} placeholder="Campo de Búzios…" style={{width:'100%',boxSizing:'border-box'}}/></div>
            <div>
              <div className="label">{lang==='pt'?'Resumo executivo (o que o cliente lê em 30 segundos)':'Executive summary'}</div>
              <textarea className="input" value={resumo} onChange={e=>setResumo(e.target.value)}
                placeholder={lang==='pt'?'Ex.: Operações de ROV no poço X-14 conforme programado. 2h15 de NPT por corrente acima do limite às 14h. POB estável, sem ocorrências de HSE.':'Ex.: ROV ops at well X-14 as planned…'}
                style={{width:'100%',boxSizing:'border-box',minHeight:96,resize:'vertical',fontSize:13,lineHeight:1.55}}/>
              <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',marginTop:3}}>{lang==='pt'?'fica salvo por dia — pode preencher ao longo do serviço e gerar no fim.':'saved per day.'}</div>
            </div>
            <div style={{display:'flex',justifyContent:'flex-end',gap:8,marginTop:2}}>
              <button className="btn" onClick={onClose}>{lang==='pt'?'cancelar':'cancel'}</button>
              <button className="btn btn-primary" disabled={salvando} onClick={salvarEGerar}>{salvando?'…':(lang==='pt'?'Salvar & gerar PDF':'Save & generate PDF')}</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

const LogTemplateModal = ({frase, onConfirm, onClose}) => {
  const lang = useT().lang;
  const slots = _logSlots(frase.texto);
  const chave = `genesis_log_tpl_${frase.id}`;
  const ultimos = (() => { try { return JSON.parse(localStorage.getItem(chave) || '[]'); } catch(e){ return []; } })();
  const [vals, setVals] = React.useState(() => slots.map((_, i) => ultimos[i] || ''));
  const set = (i, v) => setVals(a => { const n = a.slice(); n[i] = v; return n; });
  const preview = _logPreenche(frase.texto, vals);
  const confirmar = () => { try { localStorage.setItem(chave, JSON.stringify(vals)); } catch(e){} onConfirm(preview); };
  return (
    <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:420,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Preencher <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'frase':'phrase'}</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{display:'grid',gap:10,marginBottom:14}}>
          {slots.map((s, i) => (
            <div key={i}>
              <div className="label">{s.label}{s.tipo==='range'?<span style={{color:'var(--muted)',fontWeight:400,fontFamily:'var(--fm)',fontSize:10}}> · {s.min}–{s.max}</span>:''}{ultimos[i]?<span style={{color:'var(--muted)',fontWeight:400}}> · último: {ultimos[i]}</span>:''}</div>
              {s.tipo === 'range' ? (
                <div style={{display:'flex',alignItems:'center',gap:12}}>
                  <input type="range" min={s.min} max={s.max} step={s.step}
                    value={vals[i] === '' ? (parseFloat(ultimos[i]) || s.min) : parseFloat(vals[i])}
                    onChange={e=>set(i, s.dec ? parseFloat(e.target.value).toFixed(s.dec) : String(Math.round(parseFloat(e.target.value))))}
                    style={{flex:1,accentColor:'var(--accent)',height:26}}/>
                  <input className="input" inputMode="decimal" value={vals[i]} placeholder={ultimos[i]||String(s.min)}
                    onChange={e=>set(i, e.target.value.replace(',','.'))} onKeyDown={e=>{ if(e.key==='Enter') confirmar(); }}
                    style={{width:76,textAlign:'center',fontFamily:'var(--fm)',fontSize:14,flexShrink:0}}/>
                </div>
              ) : s.tipo === 'opcoes' ? (
                <div style={{display:'flex',gap:5,flexWrap:'wrap'}}>
                  {s.opcoes.map(op => (
                    <button key={op} onClick={()=>set(i, op)}
                      style={{padding:'7px 14px',borderRadius:999,cursor:'pointer',fontFamily:'var(--fm)',fontSize:12.5,border:'1.5px solid '+(vals[i]===op?'var(--accent)':'var(--line-soft)'),background:vals[i]===op?'var(--accent)':'var(--surface)',color:vals[i]===op?'#fff':'var(--ink)',fontWeight:vals[i]===op?600:400}}>{op}</button>
                  ))}
                </div>
              ) : (
                <input className="input" autoFocus={i===0} value={vals[i]} placeholder={ultimos[i]||''} onChange={e=>set(i, e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') confirmar(); }} style={{width:'100%',boxSizing:'border-box'}}/>
              )}
            </div>
          ))}
        </div>
        <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'10px 12px',fontFamily:'var(--f)',fontSize:13.5,marginBottom:16}}>{preview}</div>
        <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
          <button className="btn btn-primary" onClick={confirmar}>Registrar</button>
        </div>
      </div>
    </div>
  );
};

const LogAlvosModal = ({frase, equipamentos, token, toast, onClose, onSaved}) => {
  const lang = useT().lang;
  const [sel, setSel] = React.useState(() => new Set(frase.alvos || []));
  const [saving, setSaving] = React.useState(false);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const toggle = (id) => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const salvar = async () => {
    setSaving(true);
    try { await fetch(`/api/logbook/frases/${frase.id}/alvos`, {method:'PUT', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({equip_ids:[...sel]})}); toast.push({icon:'check', title:'Grupo salvo'}); onSaved(); }
    catch(e){ toast.push({icon:'x', title:(lang==='pt'?'Erro ao salvar':'Error saving')}); }
    finally { setSaving(false); }
  };
  const cats = ['thruster','gerador','rudder','prs','sensor','ops'].filter(c => (equipamentos||[]).some(i => i.categoria === c));
  return (
    <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'6vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:400,background:'var(--cream)',borderRadius:20,padding:'20px 22px'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:19,marginBottom:4}}>🎨 {lang==='pt'?'A frase muda o estado de…':'The phrase sets the state of…'}</div>
        <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:12,lineHeight:1.5}}>{lang==='pt'?<>Ao usar a frase, esses equipamentos do console ganham a cor dela ({frase.cor||'verde'}). Ex.: thrusters ligando, HIPAP descendo…</>:<>When used, these console items take the phrase color ({frase.cor||'green'}).</>}</div>
        {cats.length === 0 && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',fontStyle:'italic',marginBottom:10}}>{lang==='pt'?'nenhum equipamento no console DP ainda — cadastre no ⚙ do console.':'no DP console equipment yet.'}</div>}
        {cats.map(c => (
          <div key={c} style={{marginBottom:10}}>
            <div style={{fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)',marginBottom:5}}>{_DP_SECOES[c]||c}</div>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:4}}>
              {(equipamentos||[]).filter(i => i.categoria === c).map(eq => (
                <label key={eq.id} style={{display:'flex',alignItems:'center',gap:7,padding:'7px 9px',borderRadius:9,cursor:'pointer',border:'1px solid '+(sel.has(eq.id)?'var(--teal)':'var(--line-soft)'),background:sel.has(eq.id)?'rgba(31,87,85,0.08)':'var(--surface)',fontFamily:'var(--f)',fontSize:12.5}}>
                  <input type="checkbox" checked={sel.has(eq.id)} onChange={()=>toggle(eq.id)}/>
                  <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{eq.nome}</span>
                </label>
              ))}
            </div>
          </div>
        ))}
        <div style={{display:'flex',justifyContent:'flex-end',gap:8,marginTop:6}}>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'cancelar':'cancel'}</button>
          <button className="btn btn-primary btn-sm" disabled={saving} onClick={salvar}>{saving?'…':'OK'}{sel.size?` · ${sel.size}`:''}</button>
        </div>
      </div>
    </div>
  );
};

// ── DP Status do navio (níveis IMCA: verde/amarelo/vermelho) ──
const _DP_NIVEIS = {
  fora:      { cor: 'var(--muted-3)', corLog: 'cinza',   pt: 'Fora de DP',    en: 'Off DP' },
  joystick:  { cor: 'var(--sky)',     corLog: 'azul',    pt: 'DP Joystick',   en: 'DP Joystick' },
  autopos:   { cor: 'var(--leaf)',    corLog: 'verde',   pt: 'DP Auto Pos',   en: 'DP Auto Pos' },
  autopilot: { cor: 'var(--teal)',    corLog: 'verde',   pt: 'DP Auto Pilot', en: 'DP Auto Pilot' },
};
// status IMCA (criticidade do sistema DP): semáforo — coisa diferente do modo de operação
const _DP_IMCA = {
  cinza:    { cor: 'var(--muted-3)', corLog: 'cinza',    pt: 'Fora de DP',   en: 'Off DP' },
  verde:    { cor: 'var(--leaf)',    corLog: 'verde',    pt: 'DP · Verde',   en: 'DP · Green' },
  amarelo:  { cor: 'var(--sun)',     corLog: 'amarelo',  pt: 'DP · Amarelo', en: 'DP · Yellow' },
  vermelho: { cor: 'var(--rose)',    corLog: 'vermelho', pt: 'DP · Vermelho',en: 'DP · Red' },
};
const LogDPStatusBadge = ({ nivel, canManage, onMudar, niveis, fallback, titulo }) => {
  const lang = useT().lang;
  const [aberto, setAberto] = React.useState(false);
  const NV = niveis || _DP_NIVEIS;
  const n = NV[nivel] || NV[fallback || 'fora'];
  return (
    <div style={{position:'relative'}}>
      <button onClick={()=>canManage && setAberto(v=>!v)} title={titulo || (lang==='pt'?'modo DP do navio':'vessel DP mode')}
        style={{display:'inline-flex',alignItems:'center',gap:7,padding:'0 13px',height:34,borderRadius:9,cursor:canManage?'pointer':'default',border:`1.5px solid ${n.cor}`,background:'var(--surface)',fontFamily:'var(--fm)',fontSize:11.5,fontWeight:700,color:n.cor,letterSpacing:0.4}}>
        <span style={{width:9,height:9,borderRadius:'50%',background:n.cor,boxShadow:`0 0 6px ${n.cor}`}}/>{lang==='pt'?n.pt:n.en}
      </button>
      {aberto && (
        <div style={{position:'absolute',top:40,left:0,zIndex:120,background:'var(--cream)',border:'1px solid var(--line-soft)',borderRadius:12,padding:8,boxShadow:'var(--shadow-lg)',display:'grid',gap:4,minWidth:170}}>
          {Object.keys(NV).map(k => (
            <button key={k} onClick={()=>{ setAberto(false); if (k !== nivel) onMudar(k); }}
              style={{display:'flex',alignItems:'center',gap:8,padding:'7px 10px',borderRadius:8,cursor:'pointer',border:'1px solid '+(k===nivel?NV[k].cor:'transparent'),background:k===nivel?'var(--cream-2)':'transparent',fontFamily:'var(--fm)',fontSize:11.5,color:NV[k].cor,fontWeight:600,textAlign:'left'}}>
              <span style={{width:8,height:8,borderRadius:'50%',background:NV[k].cor}}/>{lang==='pt'?NV[k].pt:NV[k].en}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

// ── Alertas do serviço: bloco de notas compartilhado + histórico de edições ──
const LogAlertasModal = ({ token, toast, onClose, onSalvo }) => {
  const lang = useT().lang;
  const [aba, setAba] = React.useState('notas');
  const [dados, setDados] = React.useState(null);
  const [texto, setTexto] = React.useState('');
  const [salvando, setSalvando] = React.useState(false);
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });
  React.useEffect(() => {
    fetch('/api/logbook/alertas', { headers: H() }).then(r=>r.ok?r.json():null).then(d => { setDados(d || {atual:null,historico:[]}); setTexto((d && d.atual && d.atual.texto) || ''); }).catch(()=>setDados({atual:null,historico:[]}));
  }, []);
  const salvar = async () => {
    setSalvando(true);
    try {
      const r = await fetch('/api/logbook/alertas', { method:'PUT', headers:H(), body: JSON.stringify({ texto }) });
      if (!r.ok) throw new Error();
      toast.push({ icon:'check', title: (lang==='pt'?'Alertas do serviço atualizados':'Service notes updated') });
      onSalvo && onSalvo(texto); onClose();
    } catch (e) { toast.push({ icon:'x', title: (lang==='pt'?'Não consegui salvar':'Could not save') }); setSalvando(false); }
  };
  const quando = (ts) => { const d = new Date(ts); return d.toLocaleDateString(lang==='pt'?'pt-BR':'en-GB',{day:'2-digit',month:'short'}) + ' ' + d.toLocaleTimeString(lang==='pt'?'pt-BR':'en-GB',{hour:'2-digit',minute:'2-digit'}); };
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:520,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20}}>⚠ {lang==='pt'?<>Alertas do <em style={{fontStyle:'italic',color:'var(--accent)'}}>serviço</em></>:<>Service <em style={{fontStyle:'italic',color:'var(--accent)'}}>notes</em></>}</div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{display:'flex',gap:4,background:'var(--cream-2)',borderRadius:10,padding:3,marginBottom:12,width:'fit-content'}}>
          <button onClick={()=>setAba('notas')} style={{fontFamily:'var(--fm)',fontSize:11.5,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:aba==='notas'?'var(--surface)':'transparent',color:aba==='notas'?'var(--ink)':'var(--muted)',fontWeight:aba==='notas'?600:400}}>{lang==='pt'?'Notas':'Notes'}</button>
          <button onClick={()=>setAba('hist')} style={{fontFamily:'var(--fm)',fontSize:11.5,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:aba==='hist'?'var(--surface)':'transparent',color:aba==='hist'?'var(--ink)':'var(--muted)',fontWeight:aba==='hist'?600:400}}>{lang==='pt'?'Histórico':'History'}</button>
        </div>
        {dados === null ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'carregando…':'loading…'}</div> : aba === 'notas' ? (
          <>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:8,lineHeight:1.5}}>{lang==='pt'?'Coisas importantes do serviço: previsão virando, equipamento fora, avisos pro próximo turno. Todo mundo do passadiço lê e pode editar.':'Important watch notes: weather turning, equipment down, heads-up for the next shift. Everyone on the bridge can read and edit.'}</div>
            <textarea className="input" value={texto} onChange={e=>setTexto(e.target.value)} placeholder={lang==='pt'?'ex.: Previsão de tempo virando às 18h — atenção no swell…':'e.g.: Weather turning at 18:00 — watch the swell…'}
              style={{width:'100%',boxSizing:'border-box',minHeight:150,resize:'vertical',fontFamily:'var(--f)',fontSize:13.5,lineHeight:1.55}}/>
            {dados.atual && <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginTop:6}}>{lang==='pt'?'última edição':'last edit'}: {dados.atual.autor_nome || '?'} · {quando(dados.atual.created_at)}</div>}
            <button className="btn btn-primary" disabled={salvando} onClick={salvar} style={{width:'100%',marginTop:12}}>{salvando ? '…' : (lang==='pt'?'Salvar':'Save')}</button>
          </>
        ) : (
          <div style={{display:'grid',gap:8,maxHeight:'52vh',overflowY:'auto'}}>
            {dados.historico.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',fontStyle:'italic'}}>{lang==='pt'?'sem edições anteriores.':'no previous edits.'}</div> :
             dados.historico.map(h => (
              <div key={h.id} style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'10px 12px'}}>
                <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginBottom:4}}>{h.autor_nome || '?'} · {quando(h.created_at)}</div>
                <div style={{fontFamily:'var(--f)',fontSize:12.5,lineHeight:1.5,whiteSpace:'pre-wrap',color:'var(--ink-2)'}}>{h.texto || <em style={{color:'var(--muted)'}}>{lang==='pt'?'(limpou as notas)':'(cleared the notes)'}</em>}</div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

// ── Picker multi-seleção de equipamentos (checkboxes por seção) ──
const LogEquipPickerModal = ({ itens, titulo, onOk, onClose }) => {
  const lang = useT().lang;
  const [sel, setSel] = React.useState({});
  const cats = ['thruster','gerador','rudder','prs','sensor','ops'].filter(c => (itens||[]).some(i => i.categoria === c));
  const marcados = Object.keys(sel).filter(k => sel[k]).map(Number);
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:260,background:'rgba(31,42,46,0.5)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:380,maxHeight:'78vh',overflowY:'auto',background:'var(--cream)',borderRadius:18,padding:'16px 18px'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:18,marginBottom:10}}>{titulo || (lang==='pt'?'Escolher equipamentos':'Pick equipment')}</div>
        {cats.map(c => (
          <div key={c} style={{marginBottom:10}}>
            <div style={{fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)',marginBottom:5}}>{_DP_SECOES[c]||c}</div>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:4}}>
              {(itens||[]).filter(i => i.categoria === c).map(eq => (
                <label key={eq.id} style={{display:'flex',alignItems:'center',gap:7,padding:'7px 9px',borderRadius:9,cursor:'pointer',border:'1px solid '+(sel[eq.id]?'var(--teal)':'var(--line-soft)'),background:sel[eq.id]?'rgba(31,87,85,0.08)':'var(--surface)',fontFamily:'var(--f)',fontSize:12.5}}>
                  <input type="checkbox" checked={!!sel[eq.id]} onChange={e=>setSel(p=>({...p,[eq.id]:e.target.checked}))}/>
                  <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{eq.nome}</span>
                </label>
              ))}
            </div>
          </div>
        ))}
        <div style={{display:'flex',justifyContent:'flex-end',gap:8,marginTop:6}}>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'cancelar':'cancel'}</button>
          <button className="btn btn-primary btn-sm" disabled={!marcados.length} onClick={()=>onOk(marcados)}>OK{marcados.length?` · ${marcados.length}`:''}</button>
        </div>
      </div>
    </div>
  );
};

// ── Runner: executa a sequência etapa por etapa (o grafo do Unreal 🎮) ──
const LogSeqRunner = ({ macro, token, toast, data, postEntrada, onClose, onDpModo }) => {
  const lang = useT().lang;
  const [itens, setItens] = React.useState([]);
  const [config, setConfig] = React.useState(null);
  const [idx, setIdx] = React.useState(0);
  const [marcas, setMarcas] = React.useState({});
  const [hora, setHora] = React.useState('');
  const [travado, setTravado] = React.useState(null);
  const [feitos, setFeitos] = React.useState([]);
  const [confirmSair, setConfirmSair] = React.useState(false); // Mod 02 · fechar seguro
  const [selMarcas, setSelMarcas] = React.useState({});
  const [selPrincipal, setSelPrincipal] = React.useState({}); // {chaveGrupo: equip_id}
  const [ignorados, setIgnorados] = React.useState({});
  const saltosRef = React.useRef(0);
  const busyRef = React.useRef(false);
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });
  const carregar = () => Promise.all([
    fetch('/api/logbook/equipdp', { headers: H() }).then(r=>r.ok?r.json():[]),
    fetch('/api/logbook/equipdp/config', { headers: H() }).then(r=>r.ok?r.json():{config:null}),
  ]).then(([e,c]) => { setItens(Array.isArray(e)?e:[]); setConfig((c&&c.config)||null); });
  React.useEffect(() => { carregar(); }, []);

  const passos = macro.passos || [];
  const passo = passos[idx];
  const byId = {}; itens.forEach(i => { byId[i.id] = i; });
  const lg = (passo && passo.tipo === 'logica' && passo.logica) || null;

  const checksAvaliados = lg ? (lg.checks||[]).filter(c => !ignorados[c.equip_id]).map(c => { const eq = byId[c.equip_id]; const rot = _dpRotulos(eq||{}); const benigno = !!eq && (lg.acoes||[]).some(a => a.equip_id === c.equip_id && eq.status === a.status); return { eq, status_alvo: c.status, alvo: rot[c.status]||c.status, atual: eq ? (rot[eq.status]||eq.status) : '?', ok: !!eq && eq.status === c.status, benigno }; }) : [];
  const energAtual = (config && config.unifilar) ? _dpUnifEnergia(config.unifilar, itens) : null;
  const barrasAvaliadas = lg ? (lg.checks_barra||[]).map(b => { const ligada = energAtual ? !!energAtual[b.lado] : false; return { ...b, ligada, ok: b.exigir === 'on' ? ligada : !ligada }; }) : [];
  const energiaAvaliada = (() => {
    if (!lg || !lg.checar_energia) return null;
    if (!config || !config.unifilar) return { ok: true, pend: [] };
    const energ = _dpUnifEnergia(config.unifilar, itens);
    const pend = (lg.acoes||[]).filter(a => a.status === 'ok').map(a => byId[a.equip_id]).filter(eq => eq && (eq.categoria==='thruster'||eq.categoria==='gerador') && !_dpUnifAlimentado(config.unifilar, energ, eq.id));
    return { ok: pend.length === 0, pend };
  })();
  const perguntasOk = !lg || (lg.perguntas||[]).every((_, i) => marcas[i]);
  const autoOk = checksAvaliados.every(c => c.ok) && barrasAvaliadas.every(b => b.ok) && (!tieAvaliado || tieAvaliado.ok) && (!energiaAvaliada || energiaAvaliada.ok);
  const falhas = checksAvaliados.filter(c => !c.ok);
  const todasBenignas = falhas.length > 0 && falhas.every(c => c.benigno) && barrasAvaliadas.every(b => b.ok) && (!tieAvaliado || tieAvaliado.ok || (lg.acao_tie && ((lg.acao_tie==='fechar') === !!(config && config.unifilar && config.unifilar.tie)))) && (!energiaAvaliada || energiaAvaliada.ok);
  const selecao = lg && lg.selecao;
  const poolSel = selecao ? selecao.pool.map(id => byId[id]).filter(Boolean) : [];
  const _chaveG = (eq) => eq.categoria === 'sensor' ? 'g:' + (eq.grupo || '?') : eq.categoria;
  const gruposSel = (() => {
    const ordem = []; const map = {};
    poolSel.forEach(eq => { const k = _chaveG(eq); if (!map[k]) { map[k] = { chave: k, cat: eq.categoria, nome: eq.categoria === 'prs' ? 'PRS' : eq.categoria === 'sensor' ? (eq.grupo || 'Sensores') : (_DP_SECOES[eq.categoria] || eq.categoria), itens: [] }; ordem.push(k); } map[k].itens.push(eq); });
    return ordem.map(k => map[k]);
  })();
  const tieAvaliado = (lg && lg.check_tie && config && config.unifilar) ? { exigir: lg.check_tie.exigir, fechado: !!config.unifilar.tie, ok: lg.check_tie.exigir === 'fechado' ? !!config.unifilar.tie : !config.unifilar.tie } : null;
  const ajustarFalhas = async () => {
    if (busyRef.current) return; busyRef.current = true;
    try {
      const rs = await Promise.all(falhas.filter(c => c.eq).map(c => fetch(`/api/logbook/equipdp/${c.eq.id}/status`, { method:'POST', headers:H(), body: JSON.stringify({ status: c.status_alvo, texto: _dpRotulos(c.eq)[c.status_alvo] || c.status_alvo }) })));
      if (rs.some(r => !r.ok)) throw new Error();
      await carregar(); setTravado(null);
    } catch (e) { toast.push({ icon:'x', title: (lang==='pt'?'Não consegui ajustar':'Could not adjust') }); }
    busyRef.current = false;
  };
  const ignorarBenignas = () => { const ig = { ...ignorados }; falhas.forEach(c => { if (c.benigno && c.eq) ig[c.eq.id] = true; }); setIgnorados(ig); setTravado(null); };

  const irPara = (alvo) => {
    setMarcas({}); setHora(''); setTravado(null); setSelMarcas({}); setSelPrincipal({}); setIgnorados({});
    if (alvo === null || alvo === undefined || alvo >= passos.length) {
      toast.push({ icon: 'check', title: `⚡ ${macro.nome}`, body: (lang==='pt'?'sequência concluída':'sequence complete') });
      onClose(); return;
    }
    setIdx(alvo);
  };
  const concluir = async () => {
    if (busyRef.current || !passo) return;
    if (lg && !perguntasOk) { setTravado(lang==='pt'?'marque as confirmações antes de seguir':'tick the confirmations first'); return; }
    if (lg && selecao && !Object.keys(selMarcas).some(k => selMarcas[k])) { setTravado(lang==='pt'?'marque pelo menos um equipamento ativado':'tick at least one activated equipment'); return; }
    if (lg && !autoOk) {
      if (lg.on_fail !== null && lg.on_fail !== undefined) {
        if (++saltosRef.current > 30) { setTravado(lang==='pt'?'loop demais — revise os desvios da sequência':'too many jumps — review the branches'); return; }
        irPara(lg.on_fail); return;
      }
      const falha = checksAvaliados.find(c => !c.ok);
      const barraFalha = barrasAvaliadas.find(b => !b.ok);
      setTravado(falha
        ? (lang==='pt'?`${falha.eq ? falha.eq.nome : '?'} precisa estar "${falha.alvo}" — está "${falha.atual}"`:`${falha.eq ? falha.eq.nome : '?'} must be "${falha.alvo}" — it is "${falha.atual}"`)
        : barraFalha
          ? (lang==='pt'?`barramento ${barraFalha.lado === 'A' ? 'BB' : 'BE'} precisa estar ${barraFalha.exigir === 'on' ? 'energizado' : 'morto'}`:`busbar ${barraFalha.lado === 'A' ? 'port' : 'stbd'} must be ${barraFalha.exigir === 'on' ? 'energized' : 'dead'}`)
          : (tieAvaliado && !tieAvaliado.ok)
            ? (lang==='pt'?`bus tie B1-S1 precisa estar ${tieAvaliado.exigir} — está ${tieAvaliado.fechado?'fechado':'aberto'}`:`bus tie B1-S1 must be ${tieAvaliado.exigir==='fechado'?'closed':'open'} — it is ${tieAvaliado.fechado?'closed':'open'}`)
          : (lang==='pt'?`${(energiaAvaliada.pend[0]||{}).nome||'equipamento'} sem alimentação — confira a planta de força`:`${(energiaAvaliada.pend[0]||{}).nome||'equipment'} has no power — check the power plant`));
      return;
    }
    busyRef.current = true;
    try {
      const aplicaveis = [];
      if (lg && (lg.acoes||[]).length) lg.acoes.forEach(a => { const eq = byId[a.equip_id]; if (eq && eq.status !== a.status && !ignorados[a.equip_id]) aplicaveis.push({ ...a, eq }); });
      if (lg && selecao) selecao.pool.forEach(id => { const eq = byId[id]; if (eq && selMarcas[id] && eq.status !== selecao.status) aplicaveis.push({ equip_id: id, status: selecao.status, eq }); });
      if (aplicaveis.length) {
        const rs = await Promise.all(aplicaveis.map(a => fetch(`/api/logbook/equipdp/${a.equip_id}/status`, { method:'POST', headers:H(), body: JSON.stringify({ status: a.status, texto: _dpRotulos(a.eq)[a.status] || a.status }) })));
        if (rs.some(r => !r.ok)) throw new Error();
      }
      if (lg && selecao && selecao.pedir_principal) {
        const escolhidos = Object.values(selPrincipal).filter(id => id && byId[id] && !byId[id].principal);
        for (const id of escolhidos) await fetch(`/api/logbook/equipdp/${id}/principal`, { method:'POST', headers:H() }).catch(()=>{});
      }
      if (lg && (lg.acao_tie || lg.dp_modo || lg.dp_status)) {
        // Mod 02 · ato operacional via rota própria (registrar) — a entrada da etapa sai pelo postEntrada abaixo
        const bodyEstado = {};
        if (lg.acao_tie) bodyEstado.tie = lg.acao_tie === 'fechar';
        if (lg.dp_modo) bodyEstado.dp_modo = lg.dp_modo;
        if (lg.dp_status) bodyEstado.dp_status = lg.dp_status;
        const rC = await fetch('/api/logbook/equipdp/estado', { method:'POST', headers:H(), body: JSON.stringify(bodyEstado) });
        if (!rC.ok) throw new Error();
        const dC = await rC.json().catch(() => null);
        if (dC && dC.config) setConfig(dC.config);
        if (lg.dp_modo && onDpModo) onDpModo(lg.dp_modo);
      }
      let textoLog = passo.texto;
      if (lg && selecao) {
        const nomes = selecao.pool.filter(id => selMarcas[id] && byId[id]).map(id => byId[id].nome);
        if (nomes.length) textoLog += ` — ${nomes.join(', ')}`;
        const pr = Object.values(selPrincipal).filter(id => id && byId[id]).map(id => byId[id]);
        if (selecao.pedir_principal && pr.length) {
          const org = pr.find(e => e.categoria === 'prs');
          const prefs = pr.filter(e => e.categoria !== 'prs');
          if (org) textoLog += (lang==='pt'?` · origem: `:` · origin: `) + org.nome;
          if (prefs.length) textoLog += (lang==='pt'?` · preferenciais: `:` · preferred: `) + prefs.map(e=>e.nome).join(', ');
        }
      }
      if (lg && lg.acao_tie) textoLog += ` · bus tie ${lg.acao_tie === 'fechar' ? (lang==='pt'?'fechado':'closed') : (lang==='pt'?'aberto':'open')}`;
      if (lg && lg.dp_modo) textoLog += ` · ${lang==='pt'?(_DP_NIVEIS[lg.dp_modo]||{}).pt:(_DP_NIVEIS[lg.dp_modo]||{}).en}`;
      if (lg && lg.dp_status) textoLog += ` · ${lang==='pt'?(_DP_IMCA[lg.dp_status]||{}).pt:(_DP_IMCA[lg.dp_status]||{}).en}`;
      const extra = { texto: textoLog, cor: passo.cor || (lg ? 'verde' : null) };
      if (hora) { const d = new Date(`${data}T${hora}`); if (!isNaN(d)) extra.ts = d.toISOString(); }
      await postEntrada(extra);
      setFeitos(f => [...f, idx]);
      await carregar();
      irPara(lg && lg.on_ok !== null && lg.on_ok !== undefined ? lg.on_ok : idx + 1);
    } catch (e) { toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui concluir a etapa':'Could not complete the step') }); }
    busyRef.current = false;
  };

  if (!passo) return null;
  // Mod 02 · sequência guiada não morre com toque fora: fundo não fecha; fechar com progresso pede confirmação
  const pedirFechar = () => { if (feitos.length > 0) setConfirmSair(true); else onClose(); };
  return (
    <div style={{position:'fixed',inset:0,zIndex:255,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:460,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20}}>⚡ <em style={{fontStyle:'italic',color:'var(--accent)'}}>{macro.nome}</em></div>
          <button className="btn btn-sm" onClick={pedirFechar}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        {confirmSair && <LogConfirmModal
          titulo={lang==='pt'?'Abandonar a sequência?':'Abandon the sequence?'}
          corpo={lang==='pt'?'As etapas concluídas já estão registradas no log — ao reabrir, a sequência recomeça da primeira etapa.':'Completed steps are already in the log — reopening starts the sequence over.'}
          textoOk={lang==='pt'?'Abandonar':'Abandon'} perigo
          onOk={()=>{ setConfirmSair(false); onClose(); }}
          onClose={()=>setConfirmSair(false)}/>}
        <div style={{display:'flex',gap:4,margin:'8px 0 14px'}}>
          {passos.map((_, i) => <span key={i} style={{flex:1,height:4,borderRadius:2,background: feitos.includes(i) ? 'var(--leaf)' : i===idx ? 'var(--accent)' : 'var(--cream-3)'}}/>)}
        </div>
        <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`4px solid ${lg?'var(--teal)':'var(--line)'}`,borderRadius:14,padding:'14px 16px'}}>
          <div style={{fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)',marginBottom:6}}>{lang==='pt'?'etapa':'step'} {idx+1}/{passos.length}{lg ? ' · ⚡ '+(lang==='pt'?'com lógica':'with logic') : ''}</div>
          <div style={{fontFamily:'var(--f)',fontSize:14.5,lineHeight:1.45,marginBottom:12}}>{passo.texto}</div>
          {lg && (lg.perguntas||[]).length > 0 && (
            <div style={{display:'grid',gap:5,marginBottom:12}}>
              {lg.perguntas.map((p, i) => (
                <label key={i} style={{display:'flex',alignItems:'center',gap:8,padding:'8px 10px',borderRadius:9,cursor:'pointer',border:'1px solid '+(marcas[i]?'var(--leaf)':'var(--line-soft)'),background:marcas[i]?'rgba(92,126,79,0.08)':'transparent',fontFamily:'var(--f)',fontSize:13}}>
                  <input type="checkbox" checked={!!marcas[i]} onChange={e=>setMarcas(m=>({...m,[i]:e.target.checked}))}/> {p.label}
                </label>
              ))}
            </div>
          )}
          {selecao && gruposSel.length > 0 && (
            <div style={{marginBottom:12,display:'grid',gap:10}}>
              {gruposSel.map(gr => {
                const podeEstrela = selecao.pedir_principal && (gr.cat === 'prs' || gr.cat === 'sensor');
                const marcadosG = gr.itens.filter(eq => selMarcas[eq.id]);
                return (
                  <div key={gr.chave}>
                    <div style={{fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)',marginBottom:5,display:'flex',justifyContent:'space-between',alignItems:'center'}}>
                      <span>{gr.nome} — {lang==='pt'?'marque o que foi ativado':'tick what was activated'}</span>
                      {podeEstrela && <span style={{textTransform:'none',letterSpacing:0,fontSize:9.5,color:'var(--teal)'}}>★ {gr.cat==='prs'?(lang==='pt'?'origem':'origin'):(lang==='pt'?'preferencial':'preferred')}</span>}
                    </div>
                    <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:4}}>
                      {gr.itens.map(eq => (
                        <div key={eq.id} style={{display:'flex',alignItems:'center',gap:5,padding:'6px 7px',borderRadius:9,border:'1px solid '+(selMarcas[eq.id]?'var(--leaf)':'var(--line-soft)'),background:selMarcas[eq.id]?'rgba(92,126,79,0.08)':'var(--surface)'}}>
                          <label style={{display:'flex',alignItems:'center',gap:6,flex:1,minWidth:0,cursor:'pointer',fontFamily:'var(--f)',fontSize:12.5}}>
                            <input type="checkbox" checked={!!selMarcas[eq.id]} onChange={e=>{ setSelMarcas(p=>({...p,[eq.id]:e.target.checked})); if (!e.target.checked && selPrincipal[gr.chave]===eq.id) setSelPrincipal(p=>({...p,[gr.chave]:null})); }}/>
                            <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{eq.nome}</span>
                            {eq.status === selecao.status && <span title={lang==='pt'?'já está nesse estado':'already in that state'} style={{fontSize:9,opacity:0.55}}>●</span>}
                          </label>
                          {podeEstrela && selMarcas[eq.id] && (
                            <button onClick={()=>setSelPrincipal(p=>({...p,[gr.chave]: p[gr.chave]===eq.id?null:eq.id}))} title={gr.cat==='prs'?(lang==='pt'?'definir como origem':'set as origin'):(lang==='pt'?'definir como preferencial do grupo':'set as group preferred')}
                              style={{border:'none',background:'transparent',cursor:'pointer',fontSize:13,color:(selPrincipal||{})[gr.chave]===eq.id?'var(--sun)':'var(--muted-3)',padding:'0 2px',flexShrink:0}}>★</button>
                          )}
                        </div>
                      ))}
                    </div>
                  </div>
                );
              })}
            </div>
          )}
          {checksAvaliados.length > 0 && (
            <div style={{display:'grid',gap:4,marginBottom:12}}>
              {checksAvaliados.map((c, i) => (
                <div key={i} style={{display:'flex',alignItems:'center',gap:8,fontFamily:'var(--fm)',fontSize:11.5,color:c.ok?'var(--leaf)':'var(--rose)'}}>
                  {c.ok?'✓':'✗'} {c.eq ? c.eq.nome : '?'} — {lang==='pt'?'precisa':'needs'} "{c.alvo}"{c.ok?'':` · ${lang==='pt'?'está':'is'} "${c.atual}"`}{!c.ok && c.benigno && <span style={{color:'var(--sun)'}} title={lang==='pt'?'já está no estado que a etapa vai aplicar':'already at the state this step would set'}>· {lang==='pt'?'já no alvo':'already at target'}</span>}
                </div>
              ))}
            </div>
          )}
          {tieAvaliado && (
            <div style={{display:'flex',alignItems:'center',gap:8,fontFamily:'var(--fm)',fontSize:11.5,marginBottom:12,color:tieAvaliado.ok?'var(--leaf)':'var(--rose)'}}>
              {tieAvaliado.ok?'✓':'✗'} bus tie B1-S1 — {lang==='pt'?`precisa ${tieAvaliado.exigir}`:`must be ${tieAvaliado.exigir==='fechado'?'closed':'open'}`}{tieAvaliado.ok?'':` · ${lang==='pt'?'está':'is'} ${tieAvaliado.fechado?(lang==='pt'?'fechado':'closed'):(lang==='pt'?'aberto':'open')}`}
            </div>
          )}
          {barrasAvaliadas.length > 0 && (
            <div style={{display:'grid',gap:4,marginBottom:12}}>
              {barrasAvaliadas.map((b, i) => (
                <div key={i} style={{display:'flex',alignItems:'center',gap:8,fontFamily:'var(--fm)',fontSize:11.5,color:b.ok?'var(--leaf)':'var(--rose)'}}>
                  {b.ok?'✓':'✗'} {lang==='pt'?`barramento ${b.lado==='A'?'BB':'BE'}`:`${b.lado==='A'?'port':'stbd'} busbar`} — {b.exigir==='on'?(lang==='pt'?'energizado':'energized'):(lang==='pt'?'morto':'dead')}
                </div>
              ))}
            </div>
          )}
          {energiaAvaliada && (
            <div style={{display:'flex',alignItems:'center',gap:8,fontFamily:'var(--fm)',fontSize:11.5,marginBottom:12,color:energiaAvaliada.ok?'var(--leaf)':'var(--rose)'}}>
              {energiaAvaliada.ok?'✓':'✗'} ⚡ {lang==='pt'?'planta de força alimentando os alvos':'power plant feeding the targets'}{energiaAvaliada.ok?'':` · ${energiaAvaliada.pend.map(e=>e.nome).join(', ')}`}
            </div>
          )}
          <div style={{display:'flex',alignItems:'center',gap:10,justifyContent:'space-between'}}>
            <LogHoraField value={hora} onChange={setHora} placeholder="--:--"/>
            <button className="btn btn-primary" disabled={busyRef.current} onClick={concluir}>{lang==='pt'?'Concluir etapa →':'Complete step →'}</button>
          </div>
          {travado && (
            <div style={{marginTop:10,padding:'10px 12px',borderRadius:9,background:'rgba(177,78,92,0.1)',border:'1px solid var(--rose)'}}>
              <div style={{color:'var(--rose)',fontFamily:'var(--fm)',fontSize:11.5,marginBottom:falhas.length?8:0}}>🔐 {travado}</div>
              {falhas.length > 0 && (
                <div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
                  <button className="btn btn-sm" onClick={ajustarFalhas}>⚙ {lang==='pt'?'Ajustar estados e reavaliar':'Adjust states & re-check'}</button>
                  {todasBenignas && <button className="btn btn-sm" onClick={ignorarBenignas} title={lang==='pt'?'os que falharam já estão no estado que a etapa vai aplicar — seguro ignorar':'the failing ones are already at the target state — safe to skip'}>↷ {lang==='pt'?'Ignorar (já estão no alvo) e seguir':'Skip (already at target) & continue'}</button>}
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

const LogMacroConfirmModal = ({macro, onConfirm, onClose}) => {
  const lang = useT().lang;
  const [horas, setHoras] = React.useState(macro.passos.map(()=>''));
  const setHora = (i, v) => setHoras(hs => hs.map((h,j)=> j===i ? v : h));
  return (
  <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}} onClick={onClose}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:440,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
        <div style={{fontFamily:'var(--fs)',fontSize:21}}>⚡ <em style={{fontStyle:'italic',color:'var(--accent)'}}>{macro.nome}</em></div>
        <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
      </div>
      <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:12}}>Registra {macro.passos.length} entrada(s). Defina a hora de cada passo — deixe em branco pra usar a hora atual.</div>
      <div style={{display:'grid',gap:6,marginBottom:18}}>
        {macro.passos.map((p, i) => (
          <div key={p.id||i} style={{display:'flex',alignItems:'center',gap:8,fontFamily:'var(--f)',fontSize:13,padding:'8px 10px',background:'var(--surface)',border:'1px solid var(--line-soft)',borderLeft:`4px solid ${_logCorHex(p.cor)||'var(--line-soft)'}`,borderRadius:8}}>
            <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',flexShrink:0}}>{i+1}</span>
            <span style={{flex:1,minWidth:0}}>{p.texto}</span>
            <LogHoraField value={horas[i]} onChange={(v)=>setHora(i, v)} placeholder="--:--" style={{flexShrink:0}}/>
          </div>
        ))}
      </div>
      <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
        <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
        <button className="btn btn-primary" onClick={()=>onConfirm(horas)}>Disparar</button>
      </div>
    </div>
  </div>
  );
};

const LogMacrosModal = ({token, toast, onClose, onChanged, abaGer, setAbaGer}) => {
  const lang = useT().lang;
  const [macros, setMacros] = React.useState(null);
  const [selId, setSelId] = React.useState(null);
  const [novoNome, setNovoNome] = React.useState('');
  const [passos, setPassos] = React.useState([]);
  const [novoPasso, setNovoPasso] = React.useState('');
  const [novaCor, setNovaCor] = React.useState(null);
  const H = () => ({'Authorization':`Bearer ${token()}`});
  const jset = () => ({...H(),'Content-Type':'application/json'});

  const load = () => fetch('/api/logbook/macros', {headers:H()}).then(r=>r.ok?r.json():[]).then(d => {
    setMacros(d);
    setSelId(prev => { const still = d.find(m => m.id === prev); return still ? prev : (d[0] ? d[0].id : null); });
  }).catch(()=>setMacros([]));
  React.useEffect(() => { load(); }, []);
  const sel = macros ? macros.find(m => m.id === selId) : null;
  React.useEffect(() => { setPassos(sel ? sel.passos.map(p => ({texto:p.texto, cor:p.cor, tipo:p.tipo||'texto', logica:p.logica?{perguntas:p.logica.perguntas||[],checks:p.logica.checks||[],checks_barra:p.logica.checks_barra||[],check_tie:p.logica.check_tie||null,acao_tie:p.logica.acao_tie||null,dp_modo:p.logica.dp_modo||null,dp_status:p.logica.dp_status||null,checar_energia:!!p.logica.checar_energia,acoes:p.logica.acoes||[],selecao:p.logica.selecao||null,on_ok:p.logica.on_ok??null,on_fail:p.logica.on_fail??null}:null})) : []); setPassoAberto(null); }, [selId, macros]);
  const [passoAberto, setPassoAberto] = React.useState(null);
  const [picker, setPicker] = React.useState(null); // {i, campo}
  const [equip, setEquip] = React.useState([]);
  React.useEffect(() => { fetch('/api/logbook/equipdp', {headers:H()}).then(r=>r.ok?r.json():[]).then(d=>setEquip(Array.isArray(d)?d:[])).catch(()=>{}); }, []);
  const setLg = (i, mut) => setPassos(a => a.map((p, x) => { if (x !== i) return p; const lg = p.logica ? { ...p.logica, perguntas:p.logica.perguntas.map(q=>({...q})), checks:p.logica.checks.map(c=>({...c})), checks_barra:(p.logica.checks_barra||[]).map(c=>({...c})), check_tie:p.logica.check_tie?{...p.logica.check_tie}:null, acoes:p.logica.acoes.map(c=>({...c})), selecao:p.logica.selecao?{...p.logica.selecao,pool:p.logica.selecao.pool.slice()}:null } : { perguntas:[], checks:[], checks_barra:[], check_tie:null, acao_tie:null, dp_modo:null, dp_status:null, checar_energia:false, acoes:[], selecao:null, on_ok:null, on_fail:null }; return { ...p, logica: mut(lg) }; }));
  const toggleTipo = (i) => setPassos(a => a.map((p, x) => x === i ? (p.tipo === 'logica' ? { ...p, tipo:'texto', logica:null } : { ...p, tipo:'logica', logica: p.logica || { perguntas:[], checks:[], checks_barra:[], check_tie:null, acao_tie:null, dp_modo:null, dp_status:null, checar_energia:false, acoes:[], selecao:null, on_ok:null, on_fail:null } }) : p));

  const changed = () => { load(); onChanged && onChanged(); };
  const addMacro = async () => { const n = novoNome.trim(); if (!n) return; const r = await fetch('/api/logbook/macros', {method:'POST', headers:jset(), body:JSON.stringify({nome:n})}); if (r.ok) { setNovoNome(''); const m = await r.json(); await load(); setSelId(m.id); } };
  const [pedirM, setPedirM] = React.useState(null); // Mod 02 · {tipo:'ren'|'del', macro}
  const renMacro = (m) => setPedirM({tipo:'ren', macro:m});
  const delMacro = (m) => setPedirM({tipo:'del', macro:m});
  const execRenMacro = async (m, n) => { if (n && n !== m.nome) { await fetch(`/api/logbook/macros/${m.id}`, {method:'PATCH', headers:jset(), body:JSON.stringify({nome:n})}); changed(); } };
  const execDelMacro = async (m) => { await fetch(`/api/logbook/macros/${m.id}`, {method:'DELETE', headers:H()}); changed(); };

  const addPasso = () => { const t = novoPasso.trim(); if (!t) return; setPassos(a => [...a, {texto:t, cor:novaCor}]); setNovoPasso(''); setNovaCor(null); };
  const rmPasso = (i) => setPassos(a => a.filter((_, x) => x !== i));
  const movePasso = (i, d) => setPassos(a => { const n = a.slice(); const j = i + d; if (j < 0 || j >= n.length) return a; [n[i], n[j]] = [n[j], n[i]]; return n; });
  const setPassoCor = (i, cor) => setPassos(a => a.map((p, x) => x === i ? {...p, cor} : p));
  const salvarPassos = async () => { if (!sel) return; await fetch(`/api/logbook/macros/${sel.id}/passos`, {method:'PUT', headers:jset(), body:JSON.stringify({passos})}); toast.push({icon:'check', title:'Sequência salva'}); changed(); };

  return (
    <div style={{position:'fixed',inset:0,zIndex:245,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:640,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4,gap:10}}>
          <div style={{display:'flex',gap:4,background:'var(--cream-2)',borderRadius:10,padding:3}}>
            <button onClick={()=>setAbaGer&&setAbaGer('biblioteca')} style={{fontFamily:'var(--fm)',fontSize:12,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:'transparent',color:'var(--muted)'}}>⚙ Biblioteca</button>
            <button onClick={()=>setAbaGer&&setAbaGer('sequencias')} style={{fontFamily:'var(--fm)',fontSize:12,padding:'5px 13px',borderRadius:8,border:'none',cursor:'pointer',background:'var(--surface)',color:'var(--ink)',fontWeight:600}}>⚡ Sequências</button>
          </div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:16}}>Um toque dispara vários registros em ordem — ex: "Chegada na locação".</div>

        <div style={{display:'flex',gap:16,flexWrap:'wrap'}}>
          {/* lista de macros */}
          <div style={{flex:'1 1 200px'}}>
            <div className="t-eyebrow" style={{marginBottom:8}}>{lang==='pt'?'Sequências':'Sequences'}</div>
            <div style={{display:'grid',gap:5,marginBottom:10}}>
              {macros === null ? <span style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Carregando…':'Loading…'}</span> :
               macros.length === 0 ? <span style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>{lang==='pt'?'Nenhuma ainda.':'None yet.'}</span> :
               macros.map(m => {
                const on = m.id === selId;
                return (
                  <div key={m.id} onClick={()=>setSelId(m.id)} style={{display:'flex',alignItems:'center',gap:6,padding:'8px 10px',borderRadius:8,cursor:'pointer',border:'1px solid '+(on?'var(--teal)':'var(--line-soft)'),background:on?'rgba(31,87,85,0.08)':'transparent'}}>
                    <span onDoubleClick={()=>renMacro(m)} title="duplo-clique renomeia" style={{flex:1,fontFamily:'var(--fm)',fontSize:12.5,color:on?'var(--teal)':'var(--ink)',fontWeight:on?600:400}}>{m.nome}</span>
                    <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>{m.passos.length}</span>
                    <button onClick={(e)=>{ e.stopPropagation(); delMacro(m); }} title="apagar" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:11}}>✕</button>
                  </div>
                );
              })}
            </div>
            <div style={{display:'flex',gap:6}}>
              <input className="input" value={novoNome} placeholder="Nova sequência…" onChange={e=>setNovoNome(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') addMacro(); }} style={{flex:1}}/>
              <button className="btn btn-primary btn-sm" onClick={addMacro}>+</button>
            </div>
          </div>

          {/* passos da macro selecionada */}
          <div style={{flex:'1 1 300px',borderLeft:'1px solid var(--cream-2)',paddingLeft:16}}>
            <div className="t-eyebrow" style={{marginBottom:8}}>Passos {sel ? `· ${sel.nome}` : ''}</div>
            {!sel ? <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>Selecione ou crie uma sequência.</div> : (
              <>
                <div style={{display:'grid',gap:5,marginBottom:10}}>
                  {passos.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--muted)'}}>Sem passos ainda.</div> :
                   passos.map((p, i) => {
                    const lg = p.logica;
                    const parQuery = (campo, rotV) => (lg[campo]||[]).map((a, j) => {
                      const eq = equip.find(x=>x.id===a.equip_id); const rr = _dpRotulos(eq||{});
                      return (
                        <span key={j} style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid var(--line-soft)',background:'var(--cream-2)',fontFamily:'var(--fm)',fontSize:10.5}}>
                          {eq ? eq.nome : '?'}
                          <select className="select" value={a.status} onChange={e=>setLg(i, m=>{ m[campo][j].status = e.target.value; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px',borderLeft:`3px solid ${(_DP_STATUS[a.status]||{}).cor}`}}>
                            {[['ok',rr.ok],['warn',rr.warn],['off',rr.off]].map(([v,l])=><option key={v} value={v}>{l}</option>)}
                          </select>
                          <button onClick={()=>setLg(i, m=>{ m[campo] = m[campo].filter((_,k)=>k!==j); return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                        </span>
                      );
                    });
                    const opDesvio = (val, onCh, padrao) => (
                      <select className="select" value={val===null||val===undefined?'':String(val)} onChange={e=>onCh(e.target.value===''?null:parseInt(e.target.value))} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 6px',maxWidth:150}}>
                        <option value="">{padrao}</option>
                        {passos.map((pp, k) => k !== i ? <option key={k} value={k}>→ {k+1}. {pp.texto.slice(0,22)}{pp.texto.length>22?'…':''}</option> : null)}
                      </select>
                    );
                    return (
                    <div key={i} style={{background:'var(--surface)',border:'1px solid '+(p.tipo==='logica'?'var(--teal)':'var(--line-soft)'),borderLeft:`4px solid ${p.tipo==='logica'?'var(--teal)':(_logCorHex(p.cor)||'var(--line-soft)')}`,borderRadius:8,padding:'6px 8px'}}>
                      <div style={{display:'flex',alignItems:'center',gap:6}}>
                        <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>{i+1}</span>
                        <input className="input" value={p.texto} onChange={e=>setPassos(a=>a.map((x,k)=>k===i?{...x,texto:e.target.value}:x))} style={{flex:1,minWidth:0,padding:'4px 7px',fontSize:12.5,border:'1px solid transparent',background:'transparent'}}/>
                        <button onClick={()=>{ toggleTipo(i); if (p.tipo!=='logica') setPassoAberto(i); }} title={p.tipo==='logica'?'virar etapa simples':'virar etapa com lógica ⚡'} style={{border:'none',background:p.tipo==='logica'?'var(--teal)':'var(--cream-2)',color:p.tipo==='logica'?'#fff':'var(--muted)',borderRadius:7,width:24,height:22,cursor:'pointer',fontSize:11,flexShrink:0}}>⚡</button>
                        {p.tipo==='logica' && <button onClick={()=>setPassoAberto(passoAberto===i?null:i)} title="editar a lógica" style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--teal)',fontSize:11}}>{passoAberto===i?'▴':'▾'}</button>}
                        <LogCorDots sel={p.cor||null} onPick={(c)=>setPassoCor(i, c)}/>
                        <button onClick={()=>movePasso(i,-1)} title="subir" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:11}}>▲</button>
                        <button onClick={()=>movePasso(i,1)} title="descer" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:11}}>▼</button>
                        <button onClick={()=>rmPasso(i)} title="remover" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:11}}>✕</button>
                      </div>
                      {p.tipo==='logica' && passoAberto===i && lg && (
                        <div style={{marginTop:8,paddingTop:8,borderTop:'1px dashed var(--line-soft)',display:'grid',gap:8}}>
                          <div>
                            <div style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)',marginBottom:4}}>perguntas (checkboxes pro operador)</div>
                            {(lg.perguntas||[]).map((q, j) => (
                              <div key={j} style={{display:'flex',gap:5,alignItems:'center',marginBottom:4}}>
                                <input className="input" value={q.label} onChange={e=>setLg(i, m=>{ m.perguntas[j].label = e.target.value; return m; })} placeholder="ex: HIPAP em cima?" style={{flex:1,padding:'4px 8px',fontSize:11.5}}/>
                                <button onClick={()=>setLg(i, m=>{ m.perguntas = m.perguntas.filter((_,k)=>k!==j); return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:11}}>✕</button>
                              </div>
                            ))}
                            <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ m.perguntas = [...m.perguntas, {label:''}]; return m; })}>+ pergunta</button>
                          </div>
                          <div>
                            <div style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)',marginBottom:4}}>só se (estado exigido)</div>
                            <div style={{display:'flex',gap:5,flexWrap:'wrap',alignItems:'center'}}>
                              {parQuery('checks')}
                              {(lg.checks_barra||[]).map((b, j) => (
                                <span key={'b'+j} style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid var(--sun)',background:'rgba(198,138,31,0.08)',fontFamily:'var(--fm)',fontSize:10.5}}>
                                  ⚡ {b.lado==='A'?'BB':'BE'}
                                  <select className="select" value={b.exigir} onChange={e=>setLg(i, m=>{ m.checks_barra[j].exigir = e.target.value; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px'}}>
                                    <option value="on">energizado</option><option value="off">morto</option>
                                  </select>
                                  <button onClick={()=>setLg(i, m=>{ m.checks_barra = m.checks_barra.filter((_,k)=>k!==j); return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                                </span>
                              ))}
                              <button className="btn btn-sm" onClick={()=>setPicker({i, campo:'checks'})}>+ equipamentos</button>
                              <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ const tem = new Set(m.checks_barra.map(b=>b.lado)); const lado = !tem.has('A') ? 'A' : (!tem.has('B') ? 'B' : null); if (lado) m.checks_barra = [...m.checks_barra, {lado, exigir:'on'}]; return m; })}>+ barramento</button>
                              {lg.check_tie ? (
                                <span style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid var(--teal)',background:'rgba(31,87,85,0.06)',fontFamily:'var(--fm)',fontSize:10.5}}>
                                  ⛓ bus tie
                                  <select className="select" value={lg.check_tie.exigir} onChange={e=>setLg(i, m=>{ m.check_tie = { exigir: e.target.value }; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px'}}>
                                    <option value="fechado">fechado</option><option value="aberto">aberto</option>
                                  </select>
                                  <button onClick={()=>setLg(i, m=>{ m.check_tie = null; return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                                </span>
                              ) : (
                                <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ m.check_tie = { exigir:'fechado' }; return m; })}>+ bus tie</button>
                              )}
                            </div>
                          </div>
                          <div>
                            <div style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)',marginBottom:4}}>faz (muda estados ao concluir)</div>
                            <div style={{display:'flex',gap:5,flexWrap:'wrap',alignItems:'center'}}>
                              {parQuery('acoes')}
                              {lg.acao_tie && (
                                <span style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid var(--teal)',background:'rgba(31,87,85,0.06)',fontFamily:'var(--fm)',fontSize:10.5}}>
                                  ⛓ bus tie
                                  <select className="select" value={lg.acao_tie} onChange={e=>setLg(i, m=>{ m.acao_tie = e.target.value; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px'}}>
                                    <option value="fechar">fechar</option><option value="abrir">abrir</option>
                                  </select>
                                  <button onClick={()=>setLg(i, m=>{ m.acao_tie = null; return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                                </span>
                              )}
                              {lg.dp_modo && (
                                <span style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid '+(_DP_NIVEIS[lg.dp_modo]||{}).cor,background:'var(--cream-2)',fontFamily:'var(--fm)',fontSize:10.5}}>
                                  🧭
                                  <select className="select" value={lg.dp_modo} onChange={e=>setLg(i, m=>{ m.dp_modo = e.target.value; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px'}}>
                                    {Object.keys(_DP_NIVEIS).map(k=><option key={k} value={k}>{_DP_NIVEIS[k].pt}</option>)}
                                  </select>
                                  <button onClick={()=>setLg(i, m=>{ m.dp_modo = null; return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                                </span>
                              )}
                              <button className="btn btn-sm" onClick={()=>setPicker({i, campo:'acoes'})}>+ equipamentos</button>
                              {lg.dp_status && (
                                <span style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 8px',borderRadius:8,border:'1px solid '+(_DP_IMCA[lg.dp_status]||{}).cor,background:'var(--cream-2)',fontFamily:'var(--fm)',fontSize:10.5}}>
                                  🚦
                                  <select className="select" value={lg.dp_status} onChange={e=>setLg(i, m=>{ m.dp_status = e.target.value; return m; })} style={{fontFamily:'var(--fm)',fontSize:10,padding:'2px 3px'}}>
                                    {Object.keys(_DP_IMCA).map(k=><option key={k} value={k}>{_DP_IMCA[k].pt}</option>)}
                                  </select>
                                  <button onClick={()=>setLg(i, m=>{ m.dp_status = null; return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:10,padding:0}}>✕</button>
                                </span>
                              )}
                              {!lg.acao_tie && <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ m.acao_tie = 'fechar'; return m; })}>+ bus tie</button>}
                              {!lg.dp_modo && <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ m.dp_modo = 'autopos'; return m; })}>+ modo DP</button>}
                              {!lg.dp_status && <button className="btn btn-sm" onClick={()=>setLg(i, m=>{ m.dp_status = 'verde'; return m; })}>+ status IMCA</button>}
                            </div>
                          </div>
                          <div>
                            <div style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)',marginBottom:4}}>seleção pelo operador (marca o que ativou — sem exigir estado prévio)</div>
                            {lg.selecao ? (
                              <div style={{display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'}}>
                                <span style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 9px',borderRadius:8,border:'1px solid var(--teal)',background:'rgba(31,87,85,0.06)'}}>{lg.selecao.pool.length} {lg.selecao.pool.length===1?'equipamento':'equipamentos'}</span>
                                <button className="btn btn-sm" onClick={()=>setPicker({i, campo:'selecao'})}>editar pool</button>
                                <select className="select" value={lg.selecao.status} onChange={e=>setLg(i, m=>{ m.selecao = {...m.selecao, status:e.target.value}; return m; })} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 6px',borderLeft:`3px solid ${(_DP_STATUS[lg.selecao.status]||{}).cor}`}}>
                                  <option value="ok">marcado → Ligado</option><option value="warn">marcado → Atenção</option><option value="off">marcado → Desligado</option>
                                </select>
                                <label style={{display:'inline-flex',gap:5,alignItems:'center',fontFamily:'var(--fm)',fontSize:10.5,cursor:'pointer'}}>
                                  <input type="checkbox" checked={!!lg.selecao.pedir_principal} onChange={e=>setLg(i, m=>{ m.selecao = {...m.selecao, pedir_principal:e.target.checked}; return m; })}/> pedir origem/preferencial ★
                                </label>
                                <button onClick={()=>setLg(i, m=>{ m.selecao = null; return m; })} style={{border:'none',background:'transparent',cursor:'pointer',color:'var(--muted)',fontSize:11}}>✕</button>
                              </div>
                            ) : (
                              <button className="btn btn-sm" onClick={()=>setPicker({i, campo:'selecao'})}>+ pool de seleção</button>
                            )}
                          </div>
                          <label style={{display:'inline-flex',gap:6,alignItems:'center',fontFamily:'var(--fm)',fontSize:11,cursor:'pointer'}}>
                            <input type="checkbox" checked={!!lg.checar_energia} onChange={e=>setLg(i, m=>{ m.checar_energia = e.target.checked; return m; })}/> ⚡ exigir alimentação da planta de força
                          </label>
                          <div style={{display:'flex',gap:10,flexWrap:'wrap',alignItems:'center'}}>
                            <span style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)'}}>passou:</span>
                            {opDesvio(lg.on_ok, (v)=>setLg(i, m=>{ m.on_ok = v; return m; }), 'próxima etapa')}
                            <span style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)'}}>falhou:</span>
                            {opDesvio(lg.on_fail, (v)=>setLg(i, m=>{ m.on_fail = v; return m; }), '🔐 bloquear e aguardar')}
                          </div>
                        </div>
                      )}
                    </div>
                    );
                  })}
                </div>
                <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'10px 12px',marginBottom:10}}>
                  <input className="input" value={novoPasso} placeholder="Novo passo…" onChange={e=>setNovoPasso(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') addPasso(); }} style={{width:'100%',boxSizing:'border-box',marginBottom:8}}/>
                  <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
                    <div style={{display:'flex',alignItems:'center',gap:8}}><span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>cor:</span><LogCorDots sel={novaCor} onPick={setNovaCor}/></div>
                    <button className="btn btn-sm" onClick={addPasso}>+ passo</button>
                  </div>
                </div>
                <button className="btn btn-primary" onClick={salvarPassos} style={{width:'100%'}}>Salvar sequência</button>
              </>
            )}
            {picker && <LogEquipPickerModal itens={equip} titulo={picker.campo==='checks'?'Exigir estado de…':picker.campo==='selecao'?'Pool de seleção do operador':'Mudar estado de…'} onClose={()=>setPicker(null)} onOk={(ids)=>{ setLg(picker.i, m=>{ if (picker.campo === 'selecao') { const base = m.selecao || { pool: [], status:'ok', pedir_principal:false }; const ja = new Set(base.pool); m.selecao = { ...base, pool: [...base.pool, ...ids.filter(id=>!ja.has(id))] }; } else { const ja = new Set(m[picker.campo].map(a=>a.equip_id)); m[picker.campo] = [...m[picker.campo], ...ids.filter(id=>!ja.has(id)).map(id=>({equip_id:id, status:'ok'}))]; } return m; }); setPicker(null); }}/>}
            {pedirM && pedirM.tipo === 'ren' && <LogPromptModal titulo="Renomear sequência" valor={pedirM.macro.nome} onOk={async (n)=>{ const m = pedirM.macro; setPedirM(null); await execRenMacro(m, n); }} onClose={()=>setPedirM(null)}/>}
            {pedirM && pedirM.tipo === 'del' && <LogConfirmModal titulo={`Apagar a sequência "${pedirM.macro.nome}"?`} textoOk={lang==='pt'?'Apagar':'Delete'} perigo onOk={async ()=>{ const m = pedirM.macro; setPedirM(null); await execDelMacro(m); }} onClose={()=>setPedirM(null)}/>}
          </div>
        </div>
      </div>
    </div>
  );
};

// Login de desk: quem está de serviço no DP (conta compartilhada escolhe o perfil + senha)
const LogDeskModal = ({ tripulacao, turnoAtual, token, toast, onClose, onSaved }) => {
  const lang = useT().lang;
  const [sel, setSel] = React.useState(null);
  const [senha, setSenha] = React.useState('');
  const [obs, setObs] = React.useState(''); // Mod 02 · "passar o serviço"
  const [turnosDia, setTurnosDia] = React.useState([]);
  const [saving, setSaving] = React.useState(false);
  const [erro, setErro] = React.useState('');
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });
  React.useEffect(() => {
    fetch(`/api/logbook/turnos?data=${_logHojeStr()}`, { headers: H() }).then(r=>r.ok?r.json():[]).then(d=>setTurnosDia(Array.isArray(d)?d:[])).catch(()=>{});
  }, []);
  // a última entrega com observações escritas — quem assume lê antes de digitar a senha
  const ultimaObs = (turnosDia.filter(t => t.observacoes && t.operador_id !== ((turnoAtual||{}).operador_id)).slice(-1)[0]) || (turnosDia.filter(t => t.observacoes).slice(-1)[0]) || null;
  const assumir = async () => {
    if (!sel || !senha) { setErro((lang==='pt'?'Digite a senha':'Enter the password')); return; }
    setSaving(true); setErro('');
    try {
      const r = await fetch('/api/logbook/turnos/assumir', { method: 'POST', headers: H(), body: JSON.stringify({ user_id: sel.user_id, senha, observacoes: obs.trim() || undefined }) });
      if (r.ok) { toast.push({ icon: 'check', title: `${sel.nome} de serviço`, body: (lang==='pt'?'Desk do DP assumido':'DP desk taken') }); onSaved(); return; }
      const d = await r.json().catch(() => ({}));
      setErro(d.error || 'Não deu certo');
    } catch (e) { setErro('Falha de conexão'); }
    setSaving(false);
  };
  const liberar = async () => {
    setSaving(true);
    try {
      const r = await fetch('/api/logbook/turnos/sair', { method: 'POST', headers: H() });
      if (r.ok) { toast.push({ icon: 'check', title: (lang==='pt'?'Desk liberado':'Desk released'), body: 'Ninguém de serviço — o próximo faz login pra registrar' }); onSaved(); return; }
    } catch (e) {}
    toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui liberar':'Could not release') });
    setSaving(false);
  };
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'7vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:460,background:'var(--cream)',borderRadius:24,padding:'24px 26px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Desk do <em style={{fontStyle:'italic',color:'var(--accent)'}}>DP</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:14,lineHeight:1.5}}>Quem assume responde pelas entradas do log. A troca pede a <b>{lang==='pt'?'senha de quem assume':'password of who takes over'}</b>.</div>
        {window.BridgeHandoverResumo ? <window.BridgeHandoverResumo token={token}/> : null}
        {turnoAtual && (
          <div style={{display:'flex',alignItems:'center',gap:10,background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:12,padding:'9px 12px',marginBottom:14}}>
            <span style={{width:8,height:8,borderRadius:'50%',background:'var(--leaf)',flexShrink:0}}/>
            <span style={{flex:1,fontFamily:'var(--f)',fontSize:13}}>{lang==='pt'?'De serviço':'On watch'}: <b>{turnoAtual.operador_nome}</b></span>
            <button className="btn btn-sm" onClick={liberar} disabled={saving} title="ninguém registra até o próximo login">🔓 liberar o desk</button>
          </div>
        )}
        <div style={{fontFamily:'var(--fm)',fontSize:10,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)',marginBottom:8}}>{turnoAtual ? 'trocar para' : 'quem assume o desk'}</div>
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(96px, 1fr))',gap:8,maxHeight:'34vh',overflowY:'auto',paddingRight:2,marginBottom:14}}>
          {tripulacao.filter(u => !turnoAtual || u.user_id !== turnoAtual.operador_id).map(u => {
            const on = sel && sel.user_id === u.user_id;
            return (
              <button key={u.user_id} onClick={()=>{ setSel(u); setErro(''); }} style={{display:'flex',flexDirection:'column',alignItems:'center',gap:6,padding:'12px 6px 10px',borderRadius:14,cursor:'pointer',border:'2px solid '+(on?'var(--accent)':'var(--line-soft)'),background:on?'var(--accent-soft)':'var(--surface)'}}>
                <LogAvatar foto={u.foto_url} initials={u.initials} nome={u.nome} size={44}/>
                <span style={{fontFamily:'var(--f)',fontSize:11.5,fontWeight:on?600:400,color:'var(--ink)',lineHeight:1.2,textAlign:'center',maxWidth:'100%',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{u.nome}</span>
              </button>
            );
          })}
          {tripulacao.length === 0 && <div style={{gridColumn:'1/-1',fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'ninguém com acesso ao logbook a bordo':'nobody aboard with logbook access'}</div>}
        </div>
        {sel && (
          <div>
            <div className="label">Senha de {sel.nome}</div>
            <div style={{display:'flex',gap:8}}>
              <input className="input" type="password" value={senha} autoFocus onChange={e=>{ setSenha(e.target.value); setErro(''); }} onKeyDown={e=>{ if (e.key==='Enter') assumir(); }} placeholder="••••••" style={{flex:1,minWidth:0}}/>
              <button className="btn btn-primary" onClick={assumir} disabled={saving}>{saving?'…':'Assumir'}</button>
            </div>
            {erro && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--rose)',marginTop:6}}>✕ {erro}</div>}
            <div className="label" style={{marginTop:12}}>{lang==='pt'?'Passar o serviço (opcional)':'Handover notes (optional)'}</div>
            <textarea className="input" value={obs} onChange={e=>setObs(e.target.value)} placeholder={lang==='pt'?'pendências, avisos pro próximo turno… fica gravado no turno e sai no relatório':'pending items, heads-up for the next watch… saved on the watch record'} style={{width:'100%',boxSizing:'border-box',minHeight:56,resize:'vertical',fontFamily:'var(--f)',fontSize:12.5}}/>
          </div>
        )}
      </div>
    </div>
  );
};

const LogFiltroModal = ({fOrdem, setFOrdem, fAutor, setFAutor, fSoAnexo, setFSoAnexo, fSemCanc, setFSemCanc, autores, onClose, onLimpar}) => {
  const lang = useT().lang;
  const sw = (on) => ({width:38,height:22,borderRadius:999,border:'none',cursor:'pointer',background:on?'var(--accent)':'var(--cream-3)',position:'relative',flexShrink:0,padding:0});
  const knob = (on) => ({position:'absolute',top:2,left:on?18:2,width:18,height:18,borderRadius:'50%',background:'#fff'});
  const row = {display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,padding:'12px 0',borderBottom:'1px solid var(--cream-2)'};
  const lbl = {fontFamily:'var(--fm)',fontSize:13,color:'var(--ink)'};
  const seg = (on) => ({fontFamily:'var(--fm)',fontSize:11,padding:'5px 11px',borderRadius:6,border:'none',cursor:'pointer',background:on?'var(--surface)':'transparent',color:on?'var(--ink)':'var(--muted)',fontWeight:on?600:400});
  return (
    <div style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'8vh 16px',overflowY:'auto'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:380,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20}}>Filtros do <em style={{fontStyle:'italic',color:'var(--accent)'}}>log</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={row}>
          <span style={lbl}>Ordem</span>
          <div style={{display:'flex',gap:4,background:'var(--cream-2)',borderRadius:9,padding:3}}>
            <button onClick={()=>setFOrdem('asc')} style={seg(fOrdem==='asc')}>↑ Antigas</button>
            <button onClick={()=>setFOrdem('desc')} style={seg(fOrdem==='desc')}>↓ Recentes</button>
          </div>
        </div>
        <div style={row}>
          <span style={lbl}>{lang==='pt'?'Autor':'Author'}</span>
          <select className="select" value={fAutor} onChange={e=>setFAutor(e.target.value)} style={{width:'auto',fontSize:12,maxWidth:190}}>
            <option value="">{lang==='pt'?'Todos':'All'}</option>
            {autores.map(a => <option key={a} value={a}>{a}</option>)}
          </select>
        </div>
        <div style={row}>
          <span style={lbl}>📎 Só com anexo</span>
          <button onClick={()=>setFSoAnexo(v=>!v)} style={sw(fSoAnexo)}><span style={knob(fSoAnexo)}/></button>
        </div>
        <div style={{...row, borderBottom:'none'}}>
          <span style={lbl}>Mostrar removidas (auditoria)</span>
          <button onClick={()=>setFSemCanc(v=>!v)} style={sw(fSemCanc)}><span style={knob(fSemCanc)}/></button>
        </div>
        <div style={{display:'flex',justifyContent:'space-between',marginTop:16}}>
          <button className="btn" onClick={onLimpar}>{lang==='pt'?'Limpar':'Clear'}</button>
          <button className="btn btn-primary" onClick={onClose}>{lang==='pt'?'Aplicar':'Apply'}</button>
        </div>
      </div>
    </div>
  );
};

// Relógio UTC + local ao vivo (chip discreto do header)
const _logGmtSalvo = () => { const v = localStorage.getItem('genesis_logbook_gmt'); return (v === null || v === 'auto') ? 'auto' : parseFloat(v); };
const LogTzModal = ({ atual, onEscolher, onClose }) => {
  const ops = ['auto'];
  for (let g = -12; g <= 14; g++) ops.push(g);
  const rotulo = (g) => g === 'auto' ? 'Automático (relógio do aparelho)' : `GMT${g >= 0 ? '+' + g : g}`;
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.45)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:280,background:'var(--cream)',borderRadius:18,padding:'16px 18px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{fontFamily:'var(--fs)',fontSize:18,marginBottom:10}}>Fuso do relógio local</div>
        <div style={{display:'grid',gap:3,maxHeight:'50vh',overflowY:'auto',paddingRight:2}}>
          {ops.map(g => {
            const ativo = String(atual) === String(g);
            return (
              <button key={g} onClick={()=>{ onEscolher(g); onClose(); }} style={{textAlign:'left',fontFamily:'var(--fm)',fontSize:12,padding:'8px 11px',borderRadius:9,cursor:'pointer',border:'1px solid '+(ativo?'var(--accent)':'var(--line-soft)'),background:ativo?'var(--accent-soft)':'var(--surface)',color:'var(--ink)'}}>
                {rotulo(g)}{ativo && ' ✓'}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
};
const LogRelogioVivo = () => {
  const [, tick] = React.useState(0);
  const [gmt, setGmt] = React.useState(_logGmtSalvo());
  const [tzOpen, setTzOpen] = React.useState(false);
  React.useEffect(() => { const t = setInterval(() => tick(n => n + 1), 1000); return () => clearInterval(t); }, []);
  const n = new Date();
  const p2 = (x) => String(x).padStart(2, '0');
  const utc = `${p2(n.getUTCHours())}:${p2(n.getUTCMinutes())}:${p2(n.getUTCSeconds())}`;
  let loc, rot;
  if (gmt === 'auto') { loc = `${p2(n.getHours())}:${p2(n.getMinutes())}:${p2(n.getSeconds())}`; rot = 'local'; }
  else { const d = new Date(n.getTime() + gmt * 3600000); loc = `${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}:${p2(d.getUTCSeconds())}`; rot = `GMT${gmt >= 0 ? '+' + gmt : gmt}`; }
  return (
    <>
      <button onClick={()=>setTzOpen(true)} title="toque pra escolher o fuso do relógio local" style={{display:'flex',flexDirection:'column',justifyContent:'center',gap:1,fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:9,padding:'2px 10px',height:34,boxSizing:'border-box',cursor:'pointer',lineHeight:1.35,textAlign:'left'}}>
        <span><b style={{color:'var(--ink)',fontWeight:600}}>{utc}</b> UTC</span>
        <span><b style={{color:'var(--ink)',fontWeight:600}}>{loc}</b> {rot}</span>
      </button>
      {tzOpen && <LogTzModal atual={gmt} onEscolher={(g)=>{ setGmt(g); try { localStorage.setItem('genesis_logbook_gmt', String(g)); } catch(_){} }} onClose={()=>setTzOpen(false)}/>}
    </>
  );
};

// Widget de gráfico meteo independente (individual ou conjunto) — usa o cache compartilhado
const LogDadosModal = ({ data, token, toast, onClose }) => (
  <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.5)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'6vh 16px',overflowY:'auto'}}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:640,background:'var(--cream)',borderRadius:24,padding:'20px 22px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
        <div style={{fontFamily:'var(--fs)',fontSize:20}}>Dados <em style={{fontStyle:'italic',color:'var(--accent)'}}>{useT().lang==='pt'?'hora-a-hora':'hourly'}</em></div>
        <button className="btn btn-sm" onClick={onClose}>{useT().lang==='pt'?'fechar':'close'}</button>
      </div>
      <LogHorasBar data={data} token={token} toast={toast} soHoras/>
    </div>
  </div>
);

const LogGraficoWidget = ({ data, token, toast, tipo, onAtraso }) => {
  const lang = useT().lang;
  const [horarios, setHorarios] = React.useState(null);
  const [graphCfg, setGraphCfg] = React.useState(_logGraphCfg());
  const [cfgOpen, setCfgOpen] = React.useState(false);
  const [dadosOpen, setDadosOpen] = React.useState(false);
  const [, tickMin] = React.useState(0);
  React.useEffect(() => { const t = setInterval(() => tickMin(n => n + 1), 60000); return () => clearInterval(t); }, []); // reavalia o atraso quando a hora vira
  const carregar = () => { _logFetchHorarios(data, token).then(h => setHorarios(h)); };
  React.useEffect(() => { setHorarios(null); carregar(); }, [data]);
  React.useEffect(() => {
    const fn = (e) => { if (!e.detail || e.detail.data === data) carregar(); };
    window.addEventListener('log-horarios-changed', fn);
    return () => window.removeEventListener('log-horarios-changed', fn);
  }, [data]);
  const cheios = (horarios || []).filter(r => r.preenchido);
  const horaAgoraM = new Date().getHours();
  const regHoraM = (h) => (horarios || []).find(r => r.hora === h);
  const atrasadoGeral = data === _logHojeStr() && horarios !== null && (() => { for (let h = 0; h <= horaAgoraM; h++) { const r = regHoraM(h); if (!(r && (r.preenchido || r.motivo))) return true; } return false; })();
  React.useEffect(() => { onAtraso && onAtraso(atrasadoGeral); }, [atrasadoGeral]);
  // Mod 02 · horário atrasado → avisa o backend, que confere se a hora segue vazia e notifica a chefia 1x por (data, hora)
  const horaPendente = (() => { if (data !== _logHojeStr() || horarios === null) return null; for (let h = 0; h <= horaAgoraM; h++) { const r = regHoraM(h); if (!(r && (r.preenchido || r.motivo))) return h; } return null; })();
  const lembreteRef = React.useRef('');
  React.useEffect(() => {
    if (horaPendente === null) return;
    const chave = data + ':' + horaPendente;
    if (lembreteRef.current === chave) return; // já avisei dessa hora neste aparelho
    lembreteRef.current = chave;
    fetch('/api/logbook/horarios/lembrete', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization': `Bearer ${token()}` }, body: JSON.stringify({ data, hora: horaPendente }) }).catch(()=>{});
  }, [horaPendente, data]);
  const SERIES = {
    vento:    { pts: cheios.filter(r => r.vento_int != null).map(r => ({h:r.hora, v:r.vento_int, dir:r.vento_dir})), cor:'var(--sky)',  titulo:'Vento',    un:"'" },
    corrente: { pts: cheios.filter(r => r.corrente_int != null).map(r => ({h:r.hora, v:r.corrente_int, dir:r.corrente_dir})), cor:'var(--teal)', titulo:'Corrente', un:"'" },
    swell:    { pts: cheios.filter(r => r.swell_int != null).map(r => ({h:r.hora, v:r.swell_int, dir:r.swell_dir})), cor:'var(--leaf)', titulo:'Swell',    un:'m' },
  };
  const quais = tipo === 'todos' ? ['vento','corrente','swell'] : [tipo];
  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:16,padding:'14px 16px',height:'100%',boxSizing:'border-box',display:'flex',flexDirection:'column',minHeight:0}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10,flexShrink:0}}>
        <div className="t-eyebrow">{tipo === 'todos' ? 'meteo & ambiente' : SERIES[tipo].titulo.toLowerCase()}</div>
        <span style={{display:'flex',alignItems:'center',gap:7,flexShrink:0}}>
          {tipo === 'todos' && (() => {
            const horaAgora = new Date().getHours();
            const regHora = (h) => (horarios || []).find(r => r.hora === h) || null;
            const atrasado = atrasadoGeral;
            return (
              <button onClick={()=>setDadosOpen(true)} title={atrasado ? 'tem horário do dia sem preenchimento — toque pra completar' : 'preencher os dados hora-a-hora'} style={{..._logBtnPri, height:28, padding:'0 12px', fontSize:13, background: atrasado ? 'var(--rose)' : 'var(--accent)'}}>
                {atrasado && <svg width="14" height="14" viewBox="0 0 24 24"><path d="M12 3L2 20h20L12 3z" fill="var(--sun)"/><path d="M12 10v4.5" stroke="#1F1A14" strokeWidth="2.1" strokeLinecap="round"/><circle cx="12" cy="17.4" r="1.1" fill="#1F1A14"/></svg>}
                {atrasado ? 'preencher horário' : 'dados'}
              </button>
            );
          })()}
          <button onClick={()=>setCfgOpen(true)} title="ajustar escala dos gráficos" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:26,height:26,cursor:'pointer',fontSize:13,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>
        </span>
      </div>
      <div style={{flex:1,minHeight:0,overflowY:'auto',display:'grid',gap:10,alignContent:'start'}}>
        {horarios === null
          ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'14px 0'}}>{lang==='pt'?'carregando…':'loading…'}</div>
          : quais.map(k => <LogGraficoDir key={k} pontos={SERIES[k].pts} cor={SERIES[k].cor} titulo={SERIES[k].titulo} unidade={SERIES[k].un} base={graphCfg[k].topo} passo={graphCfg[k].passo}/>)}
      </div>
      {cfgOpen && <LogGraficoConfigModal cfg={graphCfg} onClose={()=>setCfgOpen(false)} onSave={(c)=>{ setGraphCfg(c); _logSaveGraphCfg(c); setCfgOpen(false); }}/>}
      {dadosOpen && <LogDadosModal data={data} token={token} toast={toast} onClose={()=>setDadosOpen(false)}/>}
    </div>
  );
};

// ─── ASOG · matriz operacional (Green / Advisory / Yellow / Red) ─────────────
const _ASOG_NIVEIS = {
  green:    { cor: 'var(--leaf)', label: 'GREEN',    corLog: 'verde',    peso: 0 },
  advisory: { cor: 'var(--sky)',  label: 'ADVISORY', corLog: 'azul',     peso: 1 },
  yellow:   { cor: 'var(--sun)',  label: 'YELLOW',   corLog: 'amarelo',  peso: 2 },
  red:      { cor: 'var(--rose)', label: 'RED',      corLog: 'vermelho', peso: 3 },
};
const _ASOG_ORDEM = ['green', 'advisory', 'yellow', 'red'];
const _asogGeral = (lista) => (lista || []).reduce((acc, p) => (_ASOG_NIVEIS[p.status] || _ASOG_NIVEIS.green).peso > _ASOG_NIVEIS[acc].peso ? p.status : acc, 'green');
const _asogCriterio = (p, nivel) => p['c_' + nivel] || '';

// chip do status (usado no widget e no header do logbook)
const LogAsogChip = ({ status, size }) => {
  const n = _ASOG_NIVEIS[status] || _ASOG_NIVEIS.green;
  return (
    <span style={{display:'inline-flex',alignItems:'center',gap:6,fontFamily:'var(--fm)',fontSize:size==='sm'?10:11,fontWeight:700,letterSpacing:1,color:'#fff',background:n.cor,borderRadius:999,padding:size==='sm'?'2px 10px':'3px 13px'}}>
      ASOG {n.label}
    </span>
  );
};

// Popover de um parâmetro: os 4 critérios; tocar num nível muda a condição
const LogAsogPopover = ({ param, canManage, onStatus, onClose }) => (
  <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.45)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:400,background:'var(--cream)',borderRadius:18,padding:'16px 18px',boxShadow:'var(--shadow-lg)'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
        <div style={{fontFamily:'var(--fs)',fontSize:18}}>{param.nome}</div>
        <LogAsogChip status={param.status} size="sm"/>
      </div>
      <div style={{display:'grid',gap:5}}>
        {_ASOG_ORDEM.map(nv => {
          const n = _ASOG_NIVEIS[nv];
          const ativo = param.status === nv;
          return (
            <button key={nv} onClick={()=>canManage && onStatus(nv)} disabled={!canManage} style={{display:'flex',alignItems:'flex-start',gap:10,padding:'10px 12px',borderRadius:11,border:'1.5px solid '+(ativo?n.cor:'var(--line-soft)'),background:ativo?'var(--surface)':'transparent',cursor:canManage?'pointer':'default',textAlign:'left',color:'var(--ink)',opacity:!canManage&&!ativo?0.7:1}}>
              <span style={{width:11,height:11,borderRadius:'50%',background:n.cor,flexShrink:0,marginTop:2}}/>
              <span style={{flex:1,minWidth:0}}>
                <span style={{display:'block',fontFamily:'var(--fm)',fontSize:10,fontWeight:700,letterSpacing:1,color:n.cor}}>{n.label}{ativo && ' ✓'}</span>
                {_asogCriterio(param, nv) && <span style={{display:'block',fontFamily:'var(--f)',fontSize:12.5,lineHeight:1.4,marginTop:2}}>{_asogCriterio(param, nv)}</span>}
              </span>
            </button>
          );
        })}
      </div>
      {param.atualizado_por && <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',marginTop:10,textAlign:'center'}}>última mudança por {param.atualizado_por}</div>}
      <button className="btn btn-sm" onClick={onClose} style={{marginTop:10,width:'100%'}}>{useT().lang==='pt'?'fechar':'close'}</button>
    </div>
  </div>
);

// Editor da matriz (manage): nome + critérios; drag ⠿ pra reordenar
const LogAsogEditorModal = ({ params, token, toast, onClose, onSaved }) => {
  const lang = useT().lang;
  const [lista, setLista] = React.useState(params.map(p => ({ id: p.id, nome: p.nome, c_green: p.c_green || '', c_advisory: p.c_advisory || '', c_yellow: p.c_yellow || '', c_red: p.c_red || '' })));
  const [salvando, setSalvando] = React.useState(false);
  const [dragItem, setDragItem] = React.useState(null);
  const listaRef = React.useRef(lista); listaRef.current = lista;
  const iniciarDrag = (e, idx) => {
    e.preventDefault();
    const row = e.currentTarget.closest('[data-asogrow]');
    const rowH = row ? row.getBoundingClientRect().height + 8 : 120;
    const y0 = e.clientY;
    const arr0 = listaRef.current.slice();
    const item = arr0[idx];
    setDragItem(item);
    const mover = (ev) => {
      const delta = Math.round((ev.clientY - y0) / rowH);
      const alvo = Math.min(arr0.length - 1, Math.max(0, idx + delta));
      const sem = arr0.filter((_, k) => k !== idx);
      sem.splice(alvo, 0, item);
      setLista(sem);
    };
    const soltar = () => { window.removeEventListener('pointermove', mover); window.removeEventListener('pointerup', soltar); setDragItem(null); };
    window.addEventListener('pointermove', mover);
    window.addEventListener('pointerup', soltar);
  };
  const upd = (i, campo, val) => setLista(prev => prev.map((x, k) => k === i ? { ...x, [campo]: val } : x));
  const salvar = async () => {
    setSalvando(true);
    try {
      const r = await fetch('/api/logbook/asog', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` }, body: JSON.stringify({ itens: lista }) });
      if (!r.ok) throw new Error();
      const novos = await r.json();
      toast.push({ icon: 'check', title: (lang==='pt'?'Matriz ASOG atualizada':'ASOG matrix updated') });
      onSaved(novos);
    } catch (e) { toast.push({ icon: 'x', title: (lang==='pt'?'Erro ao salvar a matriz':'Error saving the matrix') }); setSalvando(false); }
  };
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:240,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:640,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>Matriz <em style={{fontStyle:'italic',color:'var(--accent)'}}>ASOG</em></div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:12,lineHeight:1.5}}>Arraste pelo <b>⠿</b> pra reordenar. As condições atuais são preservadas ao salvar.</div>
        <div style={{display:'grid',gap:8,maxHeight:'56vh',overflowY:'auto',paddingRight:4}}>
          {lista.map((p, i) => (
            <div key={p.id || 'n'+i} data-asogrow style={{background:'var(--surface)',border:dragItem===p?'1px dashed var(--accent)':'1px solid var(--line-soft)',borderRadius:12,padding:'10px 12px'}}>
              <div style={{display:'flex',alignItems:'center',gap:6,marginBottom:8}}>
                <span onPointerDown={(e)=>iniciarDrag(e, i)} title="arraste pra reordenar" style={{touchAction:'none',cursor:'grab',color:'var(--muted)',fontSize:15,userSelect:'none',padding:'2px 3px',flexShrink:0}}>⠿</span>
                <input className="input" value={p.nome} onChange={e=>upd(i,'nome',e.target.value)} placeholder="Parâmetro (ex: Vento)" style={{flex:1,minWidth:0,padding:'7px 10px',fontSize:13,fontWeight:600}}/>
                <button onClick={()=>setLista(prev=>prev.filter((_,k)=>k!==i))} title="remover" style={{width:24,height:24,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:12,flexShrink:0}}>×</button>
              </div>
              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))',gap:6}}>
                {_ASOG_ORDEM.map(nv => {
                  const n = _ASOG_NIVEIS[nv];
                  return (
                    <div key={nv} style={{display:'flex',alignItems:'center',gap:6}}>
                      <span title={n.label} style={{width:9,height:9,borderRadius:'50%',background:n.cor,flexShrink:0}}/>
                      <input className="input" value={p['c_'+nv]} onChange={e=>upd(i,'c_'+nv,e.target.value)} placeholder={n.label} style={{flex:1,minWidth:0,padding:'6px 9px',fontSize:12}}/>
                    </div>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:12}}>
          <button className="btn btn-sm" onClick={()=>setLista(prev=>[...prev,{id:null,nome:'',c_green:'',c_advisory:'',c_yellow:'',c_red:''}])}>+ adicionar parâmetro</button>
          <div style={{display:'flex',gap:8}}>
            <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
            <button className="btn btn-primary" onClick={salvar} disabled={salvando}>{salvando?'salvando…':'Salvar'}</button>
          </div>
        </div>
      </div>
    </div>
  );
};

const LogAsogWidget = ({ token, toast, canManage, podeStatus, onLog, bloqueado, onBloqueado }) => {
  const lang = useT().lang;
  const [params, setParams] = React.useState(null);
  const [paramSel, setParamSel] = React.useState(null);
  const [edOpen, setEdOpen] = React.useState(false);
  const mudandoRef = React.useRef(false);
  // Mod 02 · mudar a CONDIÇÃO é ato operacional (podeStatus = registrar); editar a MATRIZ segue gerir (canManage)
  const podeMudar = podeStatus || canManage;
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });
  const load = () => fetch('/api/logbook/asog', { headers: H() }).then(r => r.ok ? r.json() : []).then(d => setParams(Array.isArray(d) ? d : [])).catch(() => { if (params === null) setParams([]); });
  React.useEffect(() => { load(); const t = setInterval(load, 30000); return () => clearInterval(t); }, []);

  const geral = _asogGeral(params);
  React.useEffect(() => {
    if (params === null) return;
    try { window.dispatchEvent(new CustomEvent('genesis-asog', { detail: { geral } })); } catch (_) {}
  }, [geral, params === null]);

  const mudar = async (param, nv) => {
    if (bloqueado) { onBloqueado && onBloqueado(); return; }
    if (!canManage || mudandoRef.current || param.status === nv) { setParamSel(null); return; }
    mudandoRef.current = true;
    setParamSel(null);
    const anterior = param.status;
    setParams(prev => prev.map(p => p.id === param.id ? { ...p, status: nv } : p)); // otimista
    try {
      const r = await fetch(`/api/logbook/asog/${param.id}/status`, { method: 'POST', headers: H(), body: JSON.stringify({ status: nv }) });
      if (!r.ok) throw new Error();
      if (onLog) onLog(`ASOG · ${param.nome} → ${_ASOG_NIVEIS[nv].label}`, _ASOG_NIVEIS[nv].corLog);
    } catch (e) {
      setParams(prev => prev.map(p => p.id === param.id ? { ...p, status: anterior } : p));
      toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui mudar a condição':'Could not change the condition') });
    }
    mudandoRef.current = false;
  };

  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:16,padding:'14px 16px',height:'100%',boxSizing:'border-box',display:'flex',flexDirection:'column',minHeight:0}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:8,marginBottom:12,flexShrink:0}}>
        <div style={{display:'flex',alignItems:'center',gap:10,minWidth:0}}>
          <div className="t-eyebrow">asog</div>
          {params !== null && params.length > 0 && <LogAsogChip status={geral} size="sm"/>}
        </div>
        {canManage && <button onClick={()=>setEdOpen(true)} title="editar a matriz ASOG" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:26,height:26,cursor:'pointer',fontSize:13,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>}
      </div>
      <div style={{flex:1,minHeight:0,overflowY:'auto'}}>
        {params === null ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'10px 0'}}>{lang==='pt'?'carregando…':'loading…'}</div> :
         params.length === 0 ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>matriz vazia — {canManage ? 'use o ⚙ pra montar' : 'peça a um admin pra montar'}</div> : (
          <div style={{display:'grid',gap:3}}>
            {params.map(p => (
              <div key={p.id} style={{display:'flex',alignItems:'center',gap:10,padding:'4px 4px',borderRadius:9,background:p.status==='red'?'rgba(177,78,92,0.08)':p.status==='yellow'?'rgba(198,138,31,0.07)':'transparent'}}>
                <span onClick={()=>setParamSel(p)} title="ver critérios" style={{flex:1,minWidth:0,fontFamily:'var(--f)',fontSize:12.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',cursor:'pointer'}}>{p.nome}</span>
                <span style={{display:'flex',gap:4,flexShrink:0}}>
                  {_ASOG_ORDEM.map(nv => {
                    const n = _ASOG_NIVEIS[nv];
                    const ativo = p.status === nv;
                    return (
                      <span key={nv} onClick={()=>canManage ? mudar(p, nv) : setParamSel(p)} title={`${n.label}${_asogCriterio(p, nv) ? `: ${_asogCriterio(p, nv)}` : ''}${canManage ? ' — toque pra definir' : ''}`}
                        style={{width:20,height:16,borderRadius:5,cursor:'pointer',background:ativo?n.cor:'transparent',border:'1.5px solid '+(ativo?n.cor:'var(--line-soft)'),boxShadow:ativo&&(nv==='red')?'0 0 0 3px rgba(177,78,92,0.2)':'none',transition:'all .15s'}}/>
                    );
                  })}
                </span>
              </div>
            ))}
          </div>
        )}
      </div>
      {paramSel && <LogAsogPopover param={params.find(p=>p.id===paramSel.id) || paramSel} canManage={podeMudar} onStatus={(nv)=>mudar(paramSel, nv)} onClose={()=>setParamSel(null)}/>}
      {edOpen && <LogAsogEditorModal params={params || []} token={token} toast={toast} onClose={()=>setEdOpen(false)} onSaved={(novos)=>{ setParams(novos); setEdOpen(false); }}/>}
    </div>
  );
};

// ─── Console DP · propulsores, geradores, PRS & sensores (estado compartilhado) ───
const _DP_STATUS = {
  ok:    { cor: 'var(--leaf)',    corLog: 'verde',    label: 'OK' },
  warn:  { cor: 'var(--sun)',     corLog: 'amarelo',  label: 'ATENÇÃO' },
  alarm: { cor: 'var(--rose)',    corLog: 'vermelho', label: 'ALARME' },
  off:   { cor: 'var(--muted-3)', corLog: 'cinza',    label: 'FORA' },
};
const _DP_SECOES = { thruster: 'Propulsores', gerador: 'Geradores', rudder: 'Lemes', prs: 'PRS', sensor: 'Sensores', ops: 'Operações' };
const _DP_SECOES_ORDEM_DEFAULT = ['thruster', 'gerador', 'rudder', 'prs', 'sensor', 'ops'];
const _DP_TAM = { p: { simb: 34, ico: 18, fonte: 9.5 }, m: { simb: 46, ico: 26, fonte: 10.5 }, g: { simb: 60, ico: 34, fonte: 11.5 } };
// normaliza a lista de seções da config: filtra inválidas, completa as que faltam, preserva tam/orient
const _dpNormSecoes = (config) => {
  const base = (config && Array.isArray(config.secoes) && config.secoes.length) ? config.secoes.filter(sc => sc && _DP_SECOES[sc.cat]) : [];
  const vistas = new Set(base.map(sc => sc.cat));
  const todas = [...base, ..._DP_SECOES_ORDEM_DEFAULT.filter(cat => !vistas.has(cat)).map(cat => ({ cat, indicadores: [] }))];
  return todas.map(sc => ({ cat: sc.cat, indicadores: sc.indicadores || [], tam: _DP_TAM[sc.tam] ? sc.tam : 'm', orient: sc.orient === 'v' ? 'v' : 'h' }));
};
const _dpHelice = (cor, size) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill={cor} aria-hidden="true">
    <circle cx="12" cy="12" r="2.3"/>
    <ellipse cx="12" cy="5.6" rx="2.6" ry="4.1"/>
    <ellipse cx="6.5" cy="15.3" rx="2.6" ry="4.1" transform="rotate(120 6.5 15.3)"/>
    <ellipse cx="17.5" cy="15.3" rx="2.6" ry="4.1" transform="rotate(240 17.5 15.3)"/>
  </svg>
);
// mini-ícones dos indicadores de seção (biblioteca)
const _DP_ICONES = {
  updown:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M8 17V7M8 7L5 10M8 7l3 3"/><path d="M16 7v10m0 0l-3-3m3 3l3-3"/></svg>,
  bus:     (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round"><rect x="3" y="9" width="6" height="6" rx="1.5"/><rect x="15" y="9" width="6" height="6" rx="1.5"/><path d="M9 12h6"/></svg>,
  helice:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill={cor}><circle cx="12" cy="12" r="2"/><ellipse cx="12" cy="6.2" rx="2.3" ry="3.6"/><ellipse cx="7" cy="15" rx="2.3" ry="3.6" transform="rotate(120 7 15)"/><ellipse cx="17" cy="15" rx="2.3" ry="3.6" transform="rotate(240 17 15)"/></svg>,
  check:   (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12.5l5 5L20 6.5"/></svg>,
  power:   (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill={cor}><path d="M13 2L4.5 13.5h5L10 22l8.5-11.5h-5L13 2z"/></svg>,
  alerta:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3L2 20h20L12 3z"/><path d="M12 10v4"/><circle cx="12" cy="17" r="0.6" fill={cor}/></svg>,
  ancora:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="5" r="2.2"/><path d="M12 7.5V21M12 21c-4 0-7-3-7.5-6.5M12 21c4 0 7-3 7.5-6.5M8 12h8"/></svg>,
  gps:     (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>,
  radar:   (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4.5"/><path d="M12 12l6-6"/><circle cx="12" cy="12" r="1" fill={cor}/></svg>,
  onda:    (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round"><path d="M2 9c2.5 0 2.5 2 5 2s2.5-2 5-2 2.5 2 5 2 2.5-2 5-2"/><path d="M2 15c2.5 0 2.5 2 5 2s2.5-2 5-2 2.5 2 5 2 2.5-2 5-2"/></svg>,
  vento:   (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round"><path d="M3 8h11a3 3 0 1 0-3-3"/><path d="M3 13h15a3 3 0 1 1-3 3"/><path d="M3 18h7"/></svg>,
  giro:    (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 12l4.5-4.5M12 3.5v2M12 18.5v2M3.5 12h2M18.5 12h2"/></svg>,
  olho:    (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.6"/></svg>,
  escudo:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2l8 3.5v5.5c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V5.5L12 2z"/></svg>,
  cadeado: (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 1 1 8 0v3"/></svg>,
  chave:   (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 6.5a4 4 0 0 0-5.6 5L3 17.4V21h3.6l5.9-5.9a4 4 0 0 0 5-5.6L14 13l-3-3 3.5-3.5z"/></svg>,
  sino:    (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 9a6 6 0 1 0-12 0c0 6-2.5 7-2.5 7h17S18 15 18 9z"/><path d="M10 20a2.2 2.2 0 0 0 4 0"/></svg>,
  bandeira:(cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 21V4"/><path d="M5 5h13l-2.5 3.5L18 12H5"/></svg>,
  gota:    (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3s6.5 7 6.5 11.5a6.5 6.5 0 1 1-13 0C5.5 10 12 3 12 3z"/></svg>,
  bussola: (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><path d="M15.5 8.5l-2 5-5 2 2-5 5-2z" fill={cor} fillOpacity="0.25"/></svg>,
  engren:  (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M2 12h3M19 12h3M4.9 19.1L7 17M17 7l2.1-2.1"/></svg>,
  rov:     (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="8" width="14" height="9" rx="2.5"/><circle cx="9" cy="12.5" r="2"/><path d="M18 10.5h3M18 14.5h3M8 8V5.5h5V8"/></svg>,
  guindaste:(cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 21V6l14 4"/><path d="M3 21h6"/><path d="M13 8.3V13"/><path d="M13 13l-1.6 2.6h3.2L13 13z"/></svg>,
};
const _DP_ICONE_NOMES = { updown:'Seta ↑↓', bus:'Barramento', helice:'Hélice', check:'Check', power:'Raio', alerta:'Alerta', ancora:'Âncora', gps:'GPS', radar:'Radar', onda:'Onda', vento:'Vento', giro:'Gyro', olho:'Olho', escudo:'Escudo', cadeado:'Cadeado', chave:'Ferramenta', sino:'Sino', bandeira:'Bandeira', gota:'Gota', bussola:'Bússola', engren:'Engrenagem' };

// normaliza um indicador (migra o formato antigo de 2 estados pra cenários)
const _dpNormInd = (ind) => {
  const base = { icone: _DP_ICONES[ind.icone] ? ind.icone : 'check', label: ind.label || '', ativo: null, cenarios: [] };
  if (Array.isArray(ind.cenarios)) {
    base.cenarios = ind.cenarios.slice(0, 4).map(c => ({
      nome: c.nome || '', statusSel: _DP_STATUS[c.statusSel] ? c.statusSel : 'ok',
      statusResto: (c.statusResto === 'keep' || _DP_STATUS[c.statusResto]) ? c.statusResto : 'keep',
      equipIds: Array.isArray(c.equipIds) ? c.equipIds.filter(x => Number.isInteger(x)) : [],
    }));
    base.ativo = (Number.isInteger(ind.ativo) && ind.ativo >= 0 && ind.ativo < base.cenarios.length) ? ind.ativo : null;
  } else if (Array.isArray(ind.estados)) { // formato antigo (2 estados)
    base.cenarios = ind.estados.slice(0, 2).map((e, i) => ({ nome: e.texto || `Estado ${i === 0 ? 'A' : 'B'}`, statusSel: _DP_STATUS[e.cor] ? e.cor : 'ok', statusResto: 'keep', equipIds: [] }));
    base.ativo = ind.estado === 1 ? 1 : 0;
  }
  return base;
};

// bolinha de status (PRS / sensores) — argola dourada = principal (origem/preferencial)
const _dpBolinha = (item, size, onClick, tip) => {
  const st = _DP_STATUS[item.status] || _DP_STATUS.ok;
  return (
    <span key={item.id} onClick={onClick} title={tip} style={{width:size+7,height:size+7,borderRadius:'50%',display:'grid',placeItems:'center',cursor:onClick?'pointer':'default',border:item.principal?'1.5px solid var(--sun)':'1.5px solid transparent',flexShrink:0}}>
      <span style={{width:size,height:size,borderRadius:'50%',background:st.cor,display:'block',boxShadow:item.status==='alarm'?'0 0 0 3px rgba(177,78,92,0.22)':'none',opacity:item.status==='off'?0.75:1,transition:'all .15s'}}/>
    </span>
  );
};

// ─── Unifilar DP · diagrama de barramentos estilo Kongsberg ──────────────────
// Estado dos breakers/bus tie mora no dp_config (JSONB); equipamentos vêm do dp_equip.
const _dpNormUnif = (config, itens) => {
  const u = (config && config.unifilar) || {};
  const br = {};
  for (const it of (itens || []).filter(i => i.categoria === 'gerador' || i.categoria === 'thruster')) {
    const s = (u.br && u.br[it.id]) || {};
    br[it.id] = { barras: ['A', 'B', 'AB'].includes(s.barras) ? s.barras : 'A', A: !!s.A, B: !!s.B };
  }
  return { ativo: true, tie: !!u.tie, nomeA: u.nomeA || 'SwbdA(1)', nomeB: u.nomeB || 'SwbdB(2)', br };
};
// gerador contribui se não estiver 'off' e o breaker pro barramento estiver fechado
const _dpUnifEnergia = (unif, itens) => {
  const fontes = { A: false, B: false };
  for (const g of (itens || []).filter(i => i.categoria === 'gerador')) {
    if (g.status === 'off') continue;
    const l = unif.br[g.id] || {};
    if ((l.barras === 'A' || l.barras === 'AB') && l.A) fontes.A = true;
    if ((l.barras === 'B' || l.barras === 'AB') && l.B) fontes.B = true;
  }
  return { A: fontes.A || (unif.tie && fontes.B), B: fontes.B || (unif.tie && fontes.A) };
};
// thruster de barramento fixo liga direto (sem breaker); só o dual (azimutal) escolhe via T3-S1/S2
const _dpUnifAlimentado = (unif, energ, id) => {
  const l = unif.br[id] || {};
  if (l.barras === 'A') return !!energ.A;
  if (l.barras === 'B') return !!energ.B;
  return !!((l.A && energ.A) || (l.B && energ.B));
};

const LogDPUnifilar = ({ itens, unif, canManage, onEquip, onBreaker, onTie }) => {
  const energ = _dpUnifEnergia(unif, itens);
  const lado = (id) => (unif.br[id] || {}).barras || 'A';
  const ordenar = (arr) => [...arr.filter(i => lado(i.id) === 'A'), ...arr.filter(i => lado(i.id) === 'AB'), ...arr.filter(i => lado(i.id) === 'B')];
  const gers = ordenar((itens || []).filter(i => i.categoria === 'gerador'));
  const thrs = ordenar((itens || []).filter(i => i.categoria === 'thruster'));
  if (!gers.length && !thrs.length) return null;

  const SLOT = 92;
  const n = Math.max(gers.length, thrs.length, 4);
  const W = SLOT * n;
  const cx = W / 2;
  const busY = 118, busH = 11;
  const aX1 = 14, aX2 = cx - 36, bX1 = cx + 36, bX2 = W - 14;
  const H = 252;
  const xDe = (arr, i) => W * (i + 0.5) / arr.length;

  const MORTO = 'var(--muted-3)';
  const VIVO = 'var(--leaf)';
  const corDe = (i) => (_DP_STATUS[i.status] || _DP_STATUS.ok).cor;
  const fio = 'var(--line-strong)';

  // breaker: quadradinho clicável (fechado = preenchido)
  const Brk = (x, y, fechado, energizado, label, ladoLabel, onClick, tip) => (
    <g key={label + x} onClick={canManage ? onClick : undefined} style={{ cursor: canManage ? 'pointer' : 'default' }}>
      <rect x={x - 11} y={y - 11} width={22} height={22} fill="transparent"/>
      <rect x={x - 5} y={y - 7} width={10} height={14} rx={1.5} fill={fechado ? (energizado ? VIVO : 'var(--muted)') : 'var(--surface)'} stroke={fechado && energizado ? VIVO : 'var(--muted)'} strokeWidth="1.4"/>
      {!fechado && <line x1={x - 5} y1={y + 7} x2={x + 6} y2={y - 8} stroke="var(--muted)" strokeWidth="1.4"/>}
      <text x={ladoLabel === 'e' ? x - 9 : x + 9} y={y + 3} textAnchor={ladoLabel === 'e' ? 'end' : 'start'} fontFamily="var(--fm)" fontSize="6.6" fill="var(--muted)">{label}</text>
      <title>{tip}</title>
    </g>
  );

  const pernaGer = (g, gx, alvo) => { // alvo: 'A' | 'B' — ponto de pouso no barramento
    const px = alvo === 'A' ? Math.min(aX2 - 16, Math.max(aX1 + 16, gx - (lado(g.id) === 'AB' ? 26 : 0))) : Math.max(bX1 + 16, Math.min(bX2 - 16, gx + (lado(g.id) === 'AB' ? 26 : 0)));
    const l = unif.br[g.id] || {};
    const fech = alvo === 'A' ? l.A : l.B;
    const eng = fech && g.status !== 'off' ? true : (energ[alvo] && fech);
    const rot = lado(g.id) === 'AB' ? `${g.nome}-S${alvo === 'A' ? 1 : 2}` : `${g.nome}-S1`;
    return (
      <g key={g.id + alvo}>
        <path d={`M ${gx} 72 V 82 ${px !== gx ? `H ${px}` : ''} V ${busY}`} fill="none" stroke={fio} strokeWidth="1.3"/>
        {Brk(px, 98, fech, g.status !== 'off' && fech, rot, alvo === 'A' ? 'e' : 'd', () => onBreaker(g, alvo, rot, !fech), `${rot} · ${fech ? 'fechado' : 'aberto'}${canManage ? ' — toque pra ' + (fech ? 'abrir' : 'fechar') : ''}`)}
      </g>
    );
  };

  const pernaThr = (t, tx, alvo) => {
    const dual = lado(t.id) === 'AB';
    const px = alvo === 'A' ? Math.min(aX2 - 16, Math.max(aX1 + 16, tx - (dual ? 26 : 0))) : Math.max(bX1 + 16, Math.min(bX2 - 16, tx + (dual ? 26 : 0)));
    const l = unif.br[t.id] || {};
    const fech = alvo === 'A' ? l.A : l.B;
    const rot = `${t.nome}-S${alvo === 'A' ? 1 : 2}`;
    return (
      <g key={t.id + alvo}>
        <path d={`M ${px} ${busY + busH} V 152 ${px !== tx ? `H ${tx}` : ''} V 168`} fill="none" stroke={fio} strokeWidth="1.3"/>
        {dual && Brk(px, 138, fech, fech && energ[alvo], rot, alvo === 'A' ? 'e' : 'd', () => onBreaker(t, alvo, rot, !fech), `${rot} · ${fech ? 'fechado' : 'aberto'}${canManage ? ' — toque pra ' + (fech ? 'abrir' : 'fechar') : ''}`)}
      </g>
    );
  };

  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block', userSelect: 'none' }}>
      {/* geradores */}
      {gers.map((g, i) => {
        const gx = xDe(gers, i);
        const cor = g.status === 'off' ? MORTO : corDe(g);
        return (
          <g key={g.id}>
            <text x={gx} y={11} textAnchor="middle" fontFamily="var(--fm)" fontSize="8.6" fill="var(--ink)">{g.nome}</text>
            <g onClick={() => onEquip(g)} style={{ cursor: canManage ? 'pointer' : 'default' }}>
              <rect x={gx - 26} y={18} width={52} height={54} rx={4} fill={g.status === 'off' ? 'var(--cream-2)' : 'transparent'} stroke={cor} strokeWidth="2"/>
              <path d="M2 -14 L-7.5 2 h5 L-2 14 L8 -1.5 h-5 Z" transform={`translate(${gx},45)`} fill={cor}/>
              <title>{g.nome} · {(_DP_STATUS[g.status] || {}).label}{canManage ? ' — toque pra mudar' : ''}</title>
            </g>
            {(lado(g.id) === 'A' || lado(g.id) === 'AB') && pernaGer(g, gx, 'A')}
            {(lado(g.id) === 'B' || lado(g.id) === 'AB') && pernaGer(g, gx, 'B')}
          </g>
        );
      })}

      {/* barramentos + bus tie */}
      <rect x={aX1} y={busY} width={aX2 - aX1} height={busH} rx={2} fill={energ.A ? VIVO : MORTO}/>
      <text x={(aX1 + aX2) / 2} y={busY + 8.4} textAnchor="middle" fontFamily="var(--fm)" fontSize="7.4" fill={energ.A ? '#08300F' : 'var(--muted)'}>{unif.nomeA}</text>
      <rect x={bX1} y={busY} width={bX2 - bX1} height={busH} rx={2} fill={energ.B ? VIVO : MORTO}/>
      <text x={(bX1 + bX2) / 2} y={busY + 8.4} textAnchor="middle" fontFamily="var(--fm)" fontSize="7.4" fill={energ.B ? '#08300F' : 'var(--muted)'}>{unif.nomeB}</text>
      <line x1={aX2} y1={busY + busH / 2} x2={cx - 6} y2={busY + busH / 2} stroke={energ.A && unif.tie ? VIVO : fio} strokeWidth="1.6"/>
      <line x1={cx + 6} y1={busY + busH / 2} x2={bX1} y2={busY + busH / 2} stroke={energ.B && unif.tie ? VIVO : fio} strokeWidth="1.6"/>
      <text x={cx} y={busY - 12} textAnchor="middle" fontFamily="var(--fm)" fontSize="6.6" fill="var(--muted)">B1-S1</text>
      {Brk(cx, busY + busH / 2, unif.tie, unif.tie && (energ.A || energ.B), '', 'd', () => onTie(!unif.tie), `B1-S1 (bus tie) · ${unif.tie ? 'fechado' : 'aberto'}${canManage ? ' — toque pra ' + (unif.tie ? 'abrir' : 'fechar') : ''}`)}

      {/* thrusters */}
      {thrs.map((t, i) => {
        const tx = xDe(thrs, i);
        const alim = _dpUnifAlimentado(unif, energ, t.id);
        const cor = (!alim || t.status === 'off') ? MORTO : corDe(t);
        return (
          <g key={t.id}>
            {(lado(t.id) === 'A' || lado(t.id) === 'AB') && pernaThr(t, tx, 'A')}
            {(lado(t.id) === 'B' || lado(t.id) === 'AB') && pernaThr(t, tx, 'B')}
            <g onClick={() => onEquip(t)} style={{ cursor: canManage ? 'pointer' : 'default' }}>
              <circle cx={tx} cy={196} r={27} fill={(!alim || t.status === 'off') ? 'var(--cream-2)' : 'transparent'} stroke={cor} strokeWidth="2"/>
              <g transform={`translate(${tx - 15},${181}) scale(1.25)`}>{null}</g>
              <g transform={`translate(${tx},${196})`}>
                <circle r="2.6" fill={cor}/>
                <ellipse cx="0" cy="-7.4" rx="3" ry="4.8" fill={cor}/>
                <ellipse cx="-6.4" cy="3.7" rx="3" ry="4.8" transform="rotate(120 -6.4 3.7)" fill={cor}/>
                <ellipse cx="6.4" cy="3.7" rx="3" ry="4.8" transform="rotate(240 6.4 3.7)" fill={cor}/>
              </g>
              <title>{t.nome} · {(_DP_STATUS[t.status] || {}).label}{!alim ? ' · SEM ALIMENTAÇÃO' : ''}{canManage ? ' — toque pra mudar' : ''}</title>
            </g>
            <text x={tx} y={238} textAnchor="middle" fontFamily="var(--fm)" fontSize="8.6" fill={(!alim || t.status === 'off') ? 'var(--muted)' : 'var(--ink)'}>{t.nome}</text>
          </g>
        );
      })}
    </svg>
  );
};

// Popover: escolher a frase que o toque lança (aplica o status dela + registra no log)
const _dpRotulos = (item) => {
  const base = { ok: 'Ligado', warn: 'Atenção', off: 'Desligado' };
  return { ...base, ...(item && item.rotulos ? item.rotulos : {}) };
};
const LogDPFrasePopover = ({ item, frases, onFrase, onPrincipal, onClose }) => {
  const lang = useT().lang;
  const podePrincipal = item.categoria === 'prs' || item.categoria === 'sensor';
  const rot = _dpRotulos(item);
  const frasesDoItem = (frases || []).filter(f => !f.categoria || f.categoria === item.categoria);
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.45)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:340,background:'var(--cream)',borderRadius:18,padding:'16px 18px',boxShadow:'var(--shadow-lg)'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
          <div style={{fontFamily:'var(--fs)',fontSize:18}}>{item.nome}</div>
          <span style={{fontFamily:'var(--fm)',fontSize:9.5,fontWeight:600,color:(_DP_STATUS[item.status]||{}).cor,letterSpacing:0.5}}>{(_DP_STATUS[item.status]||{}).label}</span>
        </div>
        <div style={{display:'flex',gap:6,marginBottom:10}}>
          {[['ok','var(--leaf)'],['warn','var(--sun)'],['off','var(--muted-3)']].map(([st, cor]) => (
            <button key={st} onClick={()=>onFrase({ status: st, texto: rot[st] })}
              style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',gap:5,padding:'10px 6px',borderRadius:12,cursor:'pointer',
                border: item.status===st ? `2px solid ${cor}` : '1px solid var(--line-soft)',
                background: item.status===st ? 'var(--cream-2)' : 'var(--surface)'}}>
              <span style={{width:14,height:14,borderRadius:'50%',background:cor}}/>
              <span style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--ink)',fontWeight:item.status===st?700:500,textAlign:'center',lineHeight:1.2}}>{rot[st]}</span>
            </button>
          ))}
        </div>
        <div style={{display:'grid',gap:4,maxHeight:'40vh',overflowY:'auto'}}>
          {frasesDoItem.map(f => {
            const st = _DP_STATUS[f.status] || _DP_STATUS.ok;
            return (
              <button key={f.id} onClick={()=>onFrase(f)} style={{display:'flex',alignItems:'center',gap:10,padding:'9px 11px',borderRadius:10,border:'1px solid var(--line-soft)',background:'var(--surface)',cursor:'pointer',textAlign:'left',color:'var(--ink)'}}>
                <span style={{width:10,height:10,borderRadius:'50%',background:st.cor,flexShrink:0}}/>
                <span style={{flex:1,fontFamily:'var(--f)',fontSize:13.5}}>{f.texto}</span>
              </button>
            );
          })}
          {frasesDoItem.length===0 && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'8px 0'}}>{lang==='pt'?'nenhuma frase pra esta categoria — configure no ⚙':'no phrases for this category — set up in ⚙'}</div>}
        </div>
        {podePrincipal && !item.principal && (
          <button onClick={onPrincipal} style={{marginTop:10,width:'100%',display:'flex',alignItems:'center',justifyContent:'center',gap:8,padding:'9px 0',borderRadius:10,border:'1.5px solid var(--sun)',background:'transparent',color:'var(--sun)',cursor:'pointer',fontFamily:'var(--fm)',fontSize:12}}>
            <span style={{width:13,height:13,borderRadius:'50%',border:'1.5px solid var(--sun)',display:'inline-block'}}/>
            {item.categoria==='prs' ? 'definir como origem' : 'definir como preferencial'}
          </button>
        )}
        {podePrincipal && item.principal && <div style={{marginTop:10,fontFamily:'var(--fm)',fontSize:10.5,color:'var(--sun)',textAlign:'center'}}>◉ {item.categoria==='prs' ? 'origem atual do DP' : 'preferencial do grupo'}</div>}
        <button className="btn btn-sm" onClick={onClose} style={{marginTop:12,width:'100%'}}>{lang==='pt'?'fechar':'close'}</button>
      </div>
    </div>
  );
};

// Popover do indicador: escolher o cenário (aplica status em vários equipamentos de uma vez)
const LogDPCenarioPopover = ({ ind, itensDaSecao, onCenario, onClose }) => (
  <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:250,background:'rgba(31,42,46,0.45)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:360,background:'var(--cream)',borderRadius:18,padding:'16px 18px',boxShadow:'var(--shadow-lg)'}}>
      <div style={{display:'flex',alignItems:'center',gap:9,marginBottom:12}}>
        <span style={{display:'grid',placeItems:'center',width:26,height:26,borderRadius:7,background:'var(--cream-2)'}}>{(_DP_ICONES[ind.icone]||_DP_ICONES.check)('var(--ink)', 15)}</span>
        <div style={{fontFamily:'var(--fs)',fontSize:18}}>{ind.label || 'Cenários'}</div>
      </div>
      <div style={{display:'grid',gap:5,maxHeight:'50vh',overflowY:'auto'}}>
        {ind.cenarios.map((c, idx) => {
          const st = _DP_STATUS[c.statusSel] || _DP_STATUS.ok;
          const nomes = (itensDaSecao||[]).filter(e => c.equipIds.includes(e.id)).map(e => e.nome);
          const ativo = ind.ativo === idx;
          return (
            <button key={idx} onClick={()=>onCenario(idx)} style={{display:'flex',alignItems:'center',gap:10,padding:'10px 12px',borderRadius:11,border:'1.5px solid '+(ativo?st.cor:'var(--line-soft)'),background:'var(--surface)',cursor:'pointer',textAlign:'left',color:'var(--ink)'}}>
              <span style={{width:11,height:11,borderRadius:'50%',background:st.cor,flexShrink:0}}/>
              <span style={{flex:1,minWidth:0}}>
                <span style={{display:'block',fontFamily:'var(--f)',fontSize:13.5,fontWeight:ativo?600:400}}>{c.nome || `Cenário ${idx+1}`}{ativo && ' ✓'}</span>
                {nomes.length > 0 && <span style={{display:'block',fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',marginTop:2,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{nomes.join(' · ')}</span>}
              </span>
            </button>
          );
        })}
        {ind.cenarios.length === 0 && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{useT().lang==='pt'?'sem cenários — configure no ⚙':'no scenarios — set up in ⚙'}</div>}
      </div>
      <button className="btn btn-sm" onClick={onClose} style={{marginTop:12,width:'100%'}}>{useT().lang==='pt'?'fechar':'close'}</button>
    </div>
  </div>
);

// Editor do console (3 abas): equipamentos, frases, seções & ícones — reordenar por arrasto
const LogDPEditorModal = ({ itens, frases, config, token, toast, onClose, onSaved, modo }) => {
  const lang = useT().lang;
  const ehForca = modo === 'forca';
  const ESCOPO = ehForca ? ['thruster', 'gerador'] : ['prs', 'sensor', 'rudder', 'ops'];
  const [aba, setAba] = React.useState(ehForca ? 'frases' : 'equip');
  const [destravado, setDestravado] = React.useState(false);
  const [iconeAberto, setIconeAberto] = React.useState(null);
  const [lista, setLista] = React.useState(itens.filter(i => ESCOPO.includes(i.categoria)).map(i => ({ id: i.id, nome: i.nome, categoria: i.categoria, grupo: i.grupo || '', icone: i.icone || '', rotulos: i.rotulos || null })));
  const [frs, setFrs] = React.useState(frases.map(f => ({ id: f.id, texto: f.texto, status: f.status, categoria: f.categoria || '' })));
  const [rotAberto, setRotAberto] = React.useState(null); // índice do item com rótulos expandidos
  const todosEquip = itens; // manobras podem tocar qualquer equipamento (thrusters incluídos)
  const [secoes, setSecoes] = React.useState(() => _dpNormSecoes(config).map(sc => ({ ...sc, indicadores: sc.indicadores.slice(0, 3).map(_dpNormInd) })));
  const [unif, setUnif] = React.useState(() => _dpNormUnif(config, itens));
  const [salvando, setSalvando] = React.useState(false);
  const [dragItem, setDragItem] = React.useState(null);
  const listaRef = React.useRef(lista); listaRef.current = lista;
  const frsRef = React.useRef(frs); frsRef.current = frs;
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });

  // reordenar por arrasto (handle ⠿) — funciona no dedo e no mouse
  const iniciarDragLinha = (e, idx, getArr, setArr) => {
    e.preventDefault();
    const row = e.currentTarget.closest('[data-dprow]');
    const rowH = row ? row.getBoundingClientRect().height + 6 : 44;
    const y0 = e.clientY;
    const arr0 = getArr().slice();
    const item = arr0[idx];
    setDragItem(item);
    const mover = (ev) => {
      const delta = Math.round((ev.clientY - y0) / rowH);
      const alvo = Math.min(arr0.length - 1, Math.max(0, idx + delta));
      const sem = arr0.filter((_, k) => k !== idx);
      sem.splice(alvo, 0, item);
      setArr(sem);
    };
    const soltar = () => { window.removeEventListener('pointermove', mover); window.removeEventListener('pointerup', soltar); setDragItem(null); };
    window.addEventListener('pointermove', mover);
    window.addEventListener('pointerup', soltar);
  };
  const handleDrag = (idx, getArr, setArr) => (
    <span onPointerDown={(e)=>iniciarDragLinha(e, idx, getArr, setArr)} title="arraste pra reordenar" style={{touchAction:'none',cursor:'grab',color:'var(--muted)',fontSize:15,userSelect:'none',padding:'2px 3px',flexShrink:0}}>⠿</span>
  );

  const moverSec = (i, d) => { const j = i + d; if (j < 0 || j >= secoes.length) return; const n = secoes.map(x => ({ ...x })); [n[i], n[j]] = [n[j], n[i]]; setSecoes(n); };
  const CATS = [['thruster','Propulsor'],['gerador','Gerador'],['rudder','Leme'],['prs','PRS'],['sensor','Sensor'],['ops','Operação']].filter(([v]) => ESCOPO.includes(v));
  const STATUS_OPS = [['ok','verde'],['warn','âmbar'],['alarm','vermelho'],['off','cinza']];

  const salvar = async () => {
    setSalvando(true);
    try {
      const r1 = await fetch('/api/logbook/equipdp', { method: 'PUT', headers: H(), body: JSON.stringify({ itens: lista, categorias: ESCOPO }) });
      const r2 = await fetch('/api/logbook/equipdp/frases', { method: 'PUT', headers: H(), body: JSON.stringify({ itens: frs }) });
      const r3 = await fetch('/api/logbook/equipdp/config', { method: 'PUT', headers: H(), body: JSON.stringify({ config: { secoes, unifilar: unif } }) });
      if (!r1.ok || !r2.ok || !r3.ok) throw new Error();
      toast.push({ icon: 'check', title: (lang==='pt'?'Console atualizado':'Console updated') });
      onSaved();
    } catch (e) { toast.push({ icon: 'x', title: (lang==='pt'?'Erro ao salvar':'Error saving') }); setSalvando(false); }
  };

  const pill = (on) => ({ fontFamily:'var(--fm)', fontSize:11, padding:'5px 12px', borderRadius:999, cursor:'pointer', border:'1px solid '+(on?'var(--accent)':'var(--line-soft)'), background:on?'var(--accent)':'transparent', color:on?'#fff':'var(--ink)' });
  const setInd = (si, ii, mut) => setSecoes(prev => prev.map((sc, k) => k !== si ? sc : { ...sc, indicadores: sc.indicadores.map((ind, m) => m !== ii ? ind : mut({ ...ind, cenarios: ind.cenarios.map(c => ({ ...c, equipIds: c.equipIds.slice() })) })) }));
  const rowSt = (it) => ({ display:'flex', alignItems:'center', gap:6, borderRadius:10, padding:'2px 4px', background: dragItem === it ? 'var(--accent-soft)' : 'transparent', border: dragItem === it ? '1px dashed var(--accent)' : '1px dashed transparent' });

  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:240,background:'rgba(31,42,46,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'flex-start',justifyContent:'center',padding:'5vh 16px',overflowY:'auto'}}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:620,background:'var(--cream)',borderRadius:24,padding:'22px 24px'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
          <div style={{fontFamily:'var(--fs)',fontSize:21}}>{ehForca ? <>Planta de <em style={{fontStyle:'italic',color:'var(--accent)'}}>força</em></> : <>Posicionamento & <em style={{fontStyle:'italic',color:'var(--accent)'}}>sensores</em></>}</div>
          <button className="btn btn-sm" onClick={onClose}>{lang==='pt'?'fechar':'close'}</button>
        </div>
        <div style={{display:'flex',gap:6,marginBottom:14,flexWrap:'wrap'}}>
          {!ehForca && <button style={pill(aba==='equip')} onClick={()=>setAba('equip')}>{lang==='pt'?'Equipamentos':'Equipment'}</button>}
          <button style={pill(aba==='frases')} onClick={()=>setAba('frases')}>{lang==='pt'?'Frases':'Phrases'}</button>
          <button style={pill(aba==='secoes')} onClick={()=>setAba('secoes')}>{ehForca ? 'Indicadores' : 'Seções & ícones'}</button>
          {ehForca && <button style={pill(aba==='unifilar')} onClick={()=>setAba('unifilar')}>Ligações</button>}
          {ehForca && <button style={pill(aba==='equip')} onClick={()=>setAba('equip')}>Planta 🔒</button>}
        </div>

        {aba === 'equip' && ehForca && !destravado && (
          <div style={{textAlign:'center',padding:'26px 16px'}}>
            <div style={{fontSize:26,marginBottom:8}}>🔒</div>
            <div style={{fontFamily:'var(--f)',fontSize:13.5,marginBottom:6}}>A planta de força (geradores e propulsores) não muda no dia a dia.</div>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:16,lineHeight:1.5}}>Criar, renomear ou excluir aqui altera o navio inteiro. Destrave só se precisar montar ou corrigir a planta.</div>
            <button className="btn" onClick={()=>setDestravado(true)}>🔓 destravar edição da planta</button>
          </div>
        )}
        {aba === 'equip' && (!ehForca || destravado) && (
          <div>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:10,lineHeight:1.5}}>{ehForca ? <>⚠ Editando a <b>{lang==='pt'?'planta de força':'power plant'}</b>. Só toca em geradores e propulsores — o resto do console fica intacto.</> : <>Arraste pelo <b>⠿</b> pra reordenar. Sensores com o mesmo <b>{lang==='pt'?'grupo':'group'}</b> viram bolinhas na mesma linha; itens de <b>Operação</b> têm ícone. Só mexe nos equipamentos DESTE painel.</>}</div>
            <div style={{display:'grid',gap:6,maxHeight:'46vh',overflowY:'auto',paddingRight:4}}>
              {lista.map((it, i) => (
                <div key={it.id || 'n'+i} data-dprow style={rowSt(it)}>
                  {handleDrag(i, ()=>listaRef.current, setLista)}
                  <input className="input" value={it.nome} onChange={e=>setLista(p=>p.map((x,k)=>k===i?{...x,nome:e.target.value}:x))} placeholder="Nome (ex: T1, DGPS 1, Wind 1)" style={{flex:1,minWidth:0,padding:'7px 10px',fontSize:13}}/>
                  <button onClick={()=>setRotAberto(rotAberto===i?null:i)} title={lang==='pt'?'rótulos dos estados (ex: HIPAP — Embaixo/Em cima)':'state labels (e.g. HIPAP — Down/Up)'} style={{width:26,height:26,borderRadius:8,border:'1px solid '+(it.rotulos||rotAberto===i?'var(--teal)':'var(--line-soft)'),background:rotAberto===i?'var(--teal)':(it.rotulos?'rgba(31,87,85,0.1)':'transparent'),color:rotAberto===i?'#fff':'var(--ink)',cursor:'pointer',fontSize:12,flexShrink:0}}>🏷</button>
                  <select className="select" value={it.categoria} onChange={e=>setLista(p=>p.map((x,k)=>k===i?{...x,categoria:e.target.value}:x))} style={{fontFamily:'var(--fm)',fontSize:11,padding:'7px 8px'}}>
                    {CATS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                  </select>
                  {it.categoria === 'sensor'
                    ? <input className="input" value={it.grupo} onChange={e=>setLista(p=>p.map((x,k)=>k===i?{...x,grupo:e.target.value}:x))} placeholder="grupo" style={{width:88,padding:'7px 8px',fontSize:12}}/>
                    : it.categoria === 'ops'
                    ? <button onClick={()=>setIconeAberto(iconeAberto===i?null:i)} title="ícone do símbolo" style={{width:88,height:32,borderRadius:8,border:'1px solid var(--line-soft)',background:'var(--surface)',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',gap:5,fontFamily:'var(--fm)',fontSize:9,color:'var(--muted)',flexShrink:0}}>{(_DP_ICONES[it.icone]||_DP_ICONES.engren)('var(--ink)',14)} ícone</button>
                    : <span style={{width:88,flexShrink:0}}/>}
                  <button onClick={()=>setLista(p=>p.filter((_,k)=>k!==i))} title="remover" style={{width:24,height:24,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:12,flexShrink:0}}>×</button>
                </div>
              )).reduce((acc, el, i) => {
                acc.push(el);
                if (rotAberto === i && lista[i]) acc.push(
                  <div key={'rot'+i} style={{display:'flex',gap:6,alignItems:'center',flexWrap:'wrap',padding:'4px 6px 10px 30px'}}>
                    <span style={{fontFamily:'var(--fm)',fontSize:9.5,letterSpacing:0.8,textTransform:'uppercase',color:'var(--muted)'}}>{lang==='pt'?'rótulos:':'labels:'}</span>
                    {[['ok','var(--leaf)','Ligado'],['warn','var(--sun)','Atenção'],['off','var(--muted-3)','Desligado']].map(([st, cor, ph]) => (
                      <span key={st} style={{display:'inline-flex',alignItems:'center',gap:5}}>
                        <span style={{width:9,height:9,borderRadius:'50%',background:cor,flexShrink:0}}/>
                        <input className="input" value={(lista[i].rotulos||{})[st]||''} placeholder={ph}
                          onChange={e=>setLista(p=>p.map((x,k)=>{ if (k!==i) return x; const r = { ...(x.rotulos||{}) }; if (e.target.value.trim()) r[st] = e.target.value; else delete r[st]; return { ...x, rotulos: Object.keys(r).length ? r : null }; }))}
                          style={{width:96,padding:'5px 8px',fontSize:11.5}}/>
                      </span>
                    ))}
                    <span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted-2)'}}>{lang==='pt'?'ex. HIPAP: Embaixo · Manutenção · Em cima':'e.g. HIPAP: Down · Maintenance · Up'}</span>
                  </div>
                );
                if (iconeAberto === i && lista[i] && lista[i].categoria === 'ops') acc.push(
                  <div key={'ico'+i} style={{display:'flex',gap:4,flexWrap:'wrap',padding:'4px 6px 8px 30px'}}>
                    {Object.keys(_DP_ICONES).map(k => (
                      <button key={k} onClick={()=>{ setLista(p=>p.map((x,m)=>m===i?{...x,icone:k}:x)); setIconeAberto(null); }} title={_DP_ICONE_NOMES[k]} style={{width:28,height:28,borderRadius:8,border:'1.5px solid '+(lista[i].icone===k?'var(--accent)':'var(--line-soft)'),background:lista[i].icone===k?'var(--accent-soft)':'var(--cream)',display:'grid',placeItems:'center',cursor:'pointer',padding:0}}>
                        {_DP_ICONES[k]('var(--ink)', 13)}
                      </button>
                    ))}
                  </div>
                );
                return acc;
              }, [])}
            </div>
            <button className="btn btn-sm" onClick={()=>setLista(p=>[...p,{id:null,nome:'',categoria:ehForca?'thruster':'sensor',grupo:'',icone:''}])} style={{marginTop:10}}>+ adicionar equipamento</button>
          </div>
        )}

        {aba === 'frases' && (
          <div>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:10,lineHeight:1.5}}>Arraste pelo <b>⠿</b> pra reordenar. Cada frase tem uma cor: ao escolher num equipamento, pinta o botão e registra o texto no log.</div>
            <div style={{display:'grid',gap:6,maxHeight:'46vh',overflowY:'auto',paddingRight:4}}>
              {frs.map((f, i) => (
                <div key={f.id || 'n'+i} data-dprow style={rowSt(f)}>
                  {handleDrag(i, ()=>frsRef.current, setFrs)}
                  <input className="input" value={f.texto} onChange={e=>setFrs(p=>p.map((x,k)=>k===i?{...x,texto:e.target.value}:x))} placeholder="Texto da frase" style={{flex:1,minWidth:0,padding:'7px 10px',fontSize:13}}/>
                  <select className="select" value={f.status} onChange={e=>setFrs(p=>p.map((x,k)=>k===i?{...x,status:e.target.value}:x))} style={{fontFamily:'var(--fm)',fontSize:11,padding:'7px 8px',borderLeft:`4px solid ${(_DP_STATUS[f.status]||{}).cor}`}}>
                    {STATUS_OPS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                  </select>
                  <select className="select" value={f.categoria||''} onChange={e=>setFrs(p=>p.map((x,k)=>k===i?{...x,categoria:e.target.value}:x))} title={lang==='pt'?'em quais equipamentos a frase aparece':'which equipment shows this phrase'} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'7px 6px',maxWidth:104}}>
                    <option value="">{lang==='pt'?'todos':'all'}</option>
                    {['thruster','gerador','rudder','prs','sensor','ops'].map(c=><option key={c} value={c}>{_DP_SECOES[c]||c}</option>)}
                  </select>
                  <button onClick={()=>setFrs(p=>p.filter((_,k)=>k!==i))} title="remover" style={{width:24,height:24,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:12,flexShrink:0}}>×</button>
                </div>
              ))}
            </div>
            <button className="btn btn-sm" onClick={()=>setFrs(p=>[...p,{id:null,texto:'',status:'ok',categoria:''}])} style={{marginTop:10}}>+ adicionar frase</button>
          </div>
        )}


        {aba === 'secoes' && (
          <div>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:10,lineHeight:1.5}}>Ordene as seções e configure até 3 mini-ícones por título. Cada ícone tem <b>{lang==='pt'?'cenários':'scenarios'}</b>: tocar nele no painel abre a escolha, e o cenário pinta os equipamentos marcados de uma vez (ex: "Trânsito" liga T6/T7 e desliga o resto).</div>
            <div style={{display:'grid',gap:10,maxHeight:'46vh',overflowY:'auto',paddingRight:4}}>
              {secoes.map((sc, si) => !ESCOPO.includes(sc.cat) ? null : (
                <div key={sc.cat} style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:12,padding:'10px 12px'}}>
                  <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:sc.indicadores.length?8:0}}>
                    <div style={{display:'flex',flexDirection:'column'}}>
                      <button onClick={()=>moverSec(si,-1)} disabled={si===0} style={{border:'none',background:'transparent',cursor:si===0?'default':'pointer',color:si===0?'var(--muted-3)':'var(--muted)',fontSize:10,lineHeight:1,padding:1}}>▲</button>
                      <button onClick={()=>moverSec(si,1)} disabled={si===secoes.length-1} style={{border:'none',background:'transparent',cursor:si===secoes.length-1?'default':'pointer',color:si===secoes.length-1?'var(--muted-3)':'var(--muted)',fontSize:10,lineHeight:1,padding:1}}>▼</button>
                    </div>
                    <div style={{flex:1,fontFamily:'var(--fm)',fontSize:11,letterSpacing:1,textTransform:'uppercase'}}>{_DP_SECOES[sc.cat]}</div>
                    {sc.indicadores.length < 3 && <button className="btn btn-sm" onClick={()=>setSecoes(prev=>prev.map((x,k)=>k===si?{...x,indicadores:[...x.indicadores,{icone:'check',label:'',ativo:null,cenarios:[{nome:'',statusSel:'ok',statusResto:'keep',equipIds:[]}]}]}:x))}>+ ícone</button>}
                  </div>

                  {sc.indicadores.map((ind, ii) => {
                    const equipsSec = lista.filter(x => x.categoria === sc.cat && x.id);
                    return (
                      <div key={ii} style={{borderTop:'1px dashed var(--line-soft)',paddingTop:8,marginTop:8}}>
                        <div style={{display:'flex',gap:6,alignItems:'center',marginBottom:7}}>
                          <input className="input" value={ind.label} onChange={e=>setInd(si,ii,x=>({...x,label:e.target.value}))} placeholder="Nome do indicador (ex: Modo de propulsão)" style={{flex:1,minWidth:0,padding:'6px 9px',fontSize:12.5}}/>
                          <button onClick={()=>setSecoes(prev=>prev.map((x,k)=>k===si?{...x,indicadores:x.indicadores.filter((_,m)=>m!==ii)}:x))} title="remover indicador" style={{width:22,height:22,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:11,flexShrink:0}}>×</button>
                        </div>
                        <div style={{display:'flex',gap:4,flexWrap:'wrap',marginBottom:8}}>
                          {Object.keys(_DP_ICONES).map(k => (
                            <button key={k} onClick={()=>setInd(si,ii,x=>({...x,icone:k}))} title={_DP_ICONE_NOMES[k]} style={{width:29,height:29,borderRadius:8,border:'1.5px solid '+(ind.icone===k?'var(--accent)':'var(--line-soft)'),background:ind.icone===k?'var(--accent-soft)':'var(--cream)',display:'grid',placeItems:'center',cursor:'pointer',padding:0}}>
                              {_DP_ICONES[k]('var(--ink)', 14)}
                            </button>
                          ))}
                        </div>
                        {ind.cenarios.map((cen, ci) => (
                          <div key={ci} style={{background:'var(--cream)',border:'1px solid var(--line-soft)',borderRadius:10,padding:'8px 10px',marginBottom:6}}>
                            <div style={{display:'flex',gap:6,alignItems:'center',marginBottom:6}}>
                              <span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',flexShrink:0}}>cenário {ci+1}</span>
                              <input className="input" value={cen.nome} onChange={e=>setInd(si,ii,x=>{x.cenarios[ci].nome=e.target.value;return x;})} placeholder="Nome (vai pro log — ex: Trânsito T6/T7)" style={{flex:1,minWidth:0,padding:'5px 9px',fontSize:12}}/>
                              <button onClick={()=>setInd(si,ii,x=>({...x,cenarios:x.cenarios.filter((_,m)=>m!==ci),ativo:null}))} title="remover cenário" style={{width:20,height:20,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:10,flexShrink:0}}>×</button>
                            </div>
                            {equipsSec.length > 0 && (
                              <div style={{display:'flex',gap:4,flexWrap:'wrap',marginBottom:6}}>
                                {equipsSec.map(eq => {
                                  const on = cen.equipIds.includes(eq.id);
                                  return <button key={eq.id} onClick={()=>setInd(si,ii,x=>{const c=x.cenarios[ci];c.equipIds=on?c.equipIds.filter(z=>z!==eq.id):[...c.equipIds,eq.id];return x;})} style={{fontFamily:'var(--fm)',fontSize:10,padding:'3px 9px',borderRadius:999,cursor:'pointer',border:'1px solid '+(on?'var(--teal)':'var(--line-soft)'),background:on?'var(--teal)':'var(--surface)',color:on?'#fff':'var(--muted)'}}>{eq.nome}</button>;
                                })}
                              </div>
                            )}
                            <div style={{display:'flex',gap:6,alignItems:'center',flexWrap:'wrap'}}>
                              <span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'}}>marcados →</span>
                              <select className="select" value={cen.statusSel} onChange={e=>setInd(si,ii,x=>{x.cenarios[ci].statusSel=e.target.value;return x;})} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 6px',borderLeft:`4px solid ${(_DP_STATUS[cen.statusSel]||{}).cor}`}}>
                                {STATUS_OPS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                              </select>
                              <span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'}}>demais →</span>
                              <select className="select" value={cen.statusResto} onChange={e=>setInd(si,ii,x=>{x.cenarios[ci].statusResto=e.target.value;return x;})} style={{fontFamily:'var(--fm)',fontSize:10.5,padding:'4px 6px'}}>
                                <option value="keep">não mexer</option>
                                {STATUS_OPS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                              </select>
                            </div>
                          </div>
                        ))}
                        {ind.cenarios.length < 4 && <button className="btn btn-sm" onClick={()=>setInd(si,ii,x=>({...x,cenarios:[...x.cenarios,{nome:'',statusSel:'ok',statusResto:'keep',equipIds:[]}]}))}>+ cenário</button>}
                      </div>
                    );
                  })}
                </div>
              ))}
            </div>
          </div>
        )}

        {aba === 'unifilar' && (
          <div>
            <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:10,lineHeight:1.5}}>Diagrama de barramentos no lugar das seções de geradores e propulsores. Configure a qual barramento cada um conecta — <b>ambos</b> cria dois breakers (tipo G2 e T3). Abrir/fechar breakers é no próprio diagrama.</div>

            <div style={{display:'flex',gap:8,marginBottom:14,flexWrap:'wrap'}}>
              <label style={{flex:1,minWidth:140}}><span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'}}>{lang==='pt'?'barramento de BB':'port busbar'}</span><input className="input" value={unif.nomeA} onChange={e=>setUnif(u=>({...u,nomeA:e.target.value.slice(0,20)}))} style={{width:'100%',boxSizing:'border-box',padding:'6px 9px',fontSize:12,marginTop:3}}/></label>
              <label style={{flex:1,minWidth:140}}><span style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'}}>{lang==='pt'?'barramento de BE':'stbd busbar'}</span><input className="input" value={unif.nomeB} onChange={e=>setUnif(u=>({...u,nomeB:e.target.value.slice(0,20)}))} style={{width:'100%',boxSizing:'border-box',padding:'6px 9px',fontSize:12,marginTop:3}}/></label>
            </div>
            <div style={{display:'grid',gap:4,maxHeight:'36vh',overflowY:'auto',paddingRight:4}}>
              {lista.filter(x=>(x.categoria==='gerador'||x.categoria==='thruster')&&x.id).map(eq => {
                const l = unif.br[eq.id] || { barras:'A' };
                const setBarra = (b) => setUnif(u => ({ ...u, br: { ...u.br, [eq.id]: { ...(u.br[eq.id]||{A:false,B:false}), barras: b } } }));
                return (
                  <div key={eq.id} style={{display:'flex',alignItems:'center',gap:8}}>
                    <span style={{width:20,textAlign:'center',flexShrink:0}}>{eq.categoria==='gerador'?'⚡':'◉'}</span>
                    <span style={{flex:1,minWidth:0,fontFamily:'var(--f)',fontSize:12.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{eq.nome}</span>
                    <span style={{display:'flex',gap:3,flexShrink:0}}>
                      {[['A','BB'],['B','BE'],['AB','ambos']].map(([v,rot]) => (
                        <button key={v} onClick={()=>setBarra(v)} style={{fontFamily:'var(--fm)',fontSize:9.5,padding:'3px 10px',borderRadius:999,cursor:'pointer',border:'1px solid '+(l.barras===v?'var(--teal)':'var(--line-soft)'),background:l.barras===v?'var(--teal)':'var(--surface)',color:l.barras===v?'#fff':'var(--muted)'}}>{rot}</button>
                      ))}
                    </span>
                  </div>
                );
              })}
              {lista.filter(x=>(x.categoria==='gerador'||x.categoria==='thruster')&&!x.id).length>0 && <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)'}}>itens recém-adicionados aparecem aqui depois de salvar</div>}
            </div>
          </div>
        )}

        <div style={{display:'flex',justifyContent:'flex-end',gap:8,marginTop:16}}>
          <button className="btn" onClick={onClose}>{lang==='pt'?'Cancelar':'Cancel'}</button>
          <button className="btn btn-primary" onClick={salvar} disabled={salvando}>{salvando?'salvando…':'Salvar'}</button>
        </div>
      </div>
    </div>
  );
};

// Confirmação de manobra de breaker/bus tie (evita missclick no tablet)
const LogDPConfirmModal = ({ texto, onSim, onClose }) => (
  <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:260,background:'rgba(31,42,46,0.5)',backdropFilter:'blur(2px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
    <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:300,background:'var(--cream)',borderRadius:18,padding:'18px 20px',boxShadow:'var(--shadow-lg)',textAlign:'center'}}>
      <div style={{fontFamily:'var(--fs)',fontSize:18,marginBottom:6}}>{texto}</div>
      <div style={{fontFamily:'var(--fm)',fontSize:10.5,color:'var(--muted)',marginBottom:14}}>{useT().lang==='pt'?'a manobra entra no registro do dia':'the maneuver goes into the daily log'}</div>
      <div style={{display:'flex',gap:8}}>
        <button className="btn" onClick={onClose} style={{flex:1}}>{useT().lang==='pt'?'Cancelar':'Cancel'}</button>
        <button className="btn btn-primary" onClick={()=>{ onSim(); onClose(); }} style={{flex:1}}>{useT().lang==='pt'?'Confirmar':'Confirm'}</button>
      </div>
    </div>
  </div>
);

// ─── Posicionamento & sensores · lemes/operações + PRS | sensores ────────────
const LogPainelRef = ({ token, toast, canManage, onLog, bloqueado, onBloqueado }) => {
  const lang = useT().lang;
  const [itens, setItens] = React.useState(null);
  const [frases, setFrases] = React.useState([]);
  const [config, setConfig] = React.useState(null);
  const [edOpen, setEdOpen] = React.useState(false);
  const [itemSel, setItemSel] = React.useState(null);
  const [indSel, setIndSel] = React.useState(null);
  const mudandoRef = React.useRef(false);
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });
  const load = () => Promise.all([
    fetch('/api/logbook/equipdp', { headers: H() }).then(r => r.ok ? r.json() : []),
    fetch('/api/logbook/equipdp/frases', { headers: H() }).then(r => r.ok ? r.json() : []),
    fetch('/api/logbook/equipdp/config', { headers: H() }).then(r => r.ok ? r.json() : { config: null }),
  ]).then(([e, f, c]) => { setItens(Array.isArray(e) ? e : []); setFrases(Array.isArray(f) ? f : []); setConfig((c && c.config) || null); }).catch(() => { if (itens === null) setItens([]); });
  React.useEffect(() => { load(); const t = setInterval(load, 30000); return () => clearInterval(t); }, []);

  const secoesCfg = _dpNormSecoes(config);
  const siDe = (cat) => secoesCfg.findIndex(sc => sc.cat === cat);

  const aplicarFrase = async (item, f) => {
    if (!canManage || mudandoRef.current) return;
    mudandoRef.current = true;
    setItemSel(null);
    const anterior = { status: item.status, status_texto: item.status_texto };
    setItens(prev => prev.map(i => i.id === item.id ? { ...i, status: f.status, status_texto: f.texto } : i));
    try {
      const r = await fetch(`/api/logbook/equipdp/${item.id}/status`, { method: 'POST', headers: H(), body: JSON.stringify({ status: f.status, texto: f.texto }) });
      if (!r.ok) throw new Error();
      if (onLog) onLog(`${item.nome}: ${f.texto}`, _DP_STATUS[f.status] ? _DP_STATUS[f.status].corLog : null);
    } catch (e) {
      setItens(prev => prev.map(i => i.id === item.id ? { ...i, ...anterior } : i));
      toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui aplicar':'Could not apply') });
    }
    mudandoRef.current = false;
  };

  const definirPrincipal = async (item) => {
    if (!canManage) return;
    setItemSel(null);
    try {
      const r = await fetch(`/api/logbook/equipdp/${item.id}/principal`, { method: 'POST', headers: H() });
      if (!r.ok) throw new Error();
      if (onLog) onLog(`${item.nome} selecionado como ${item.categoria === 'prs' ? 'origem' : 'preferencial'}`, 'verde');
      load();
    } catch (e) { toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui definir':'Could not set') }); }
  };

  const aplicarCenario = async (si, ii, ci) => {
    if (!canManage) return;
    setIndSel(null);
    const secoesAtuais = secoesCfg.map(sc => ({ ...sc, indicadores: (sc.indicadores || []).map(_dpNormInd) }));
    const ind = secoesAtuais[si] && secoesAtuais[si].indicadores[ii]; if (!ind) return;
    const cen = ind.cenarios[ci]; if (!cen) return;
    ind.ativo = ci;
    const alvos = (itens || []).filter(e => e.categoria === secoesAtuais[si].cat);
    const mudancas = [];
    for (const eq of alvos) {
      const alvoStatus = cen.equipIds.includes(eq.id) ? cen.statusSel : (cen.statusResto !== 'keep' ? cen.statusResto : null);
      if (alvoStatus && alvoStatus !== eq.status) mudancas.push({ id: eq.id, status: alvoStatus });
    }
    setItens(prev => prev.map(i => { const m = mudancas.find(x => x.id === i.id); return m ? { ...i, status: m.status } : i; }));
    const cfgNovo = { ...(config || {}), secoes: secoesAtuais };
    setConfig(cfgNovo);
    try {
      const rs = await Promise.all([
        ...mudancas.map(m => fetch(`/api/logbook/equipdp/${m.id}/status`, { method: 'POST', headers: H(), body: JSON.stringify({ status: m.status }) })),
        fetch('/api/logbook/equipdp/config', { method: 'PUT', headers: H(), body: JSON.stringify({ config: cfgNovo }) }),
      ]);
      if (rs.some(r => !r.ok)) throw new Error();
      if (onLog) onLog(`${ind.label ? ind.label + ': ' : ''}${cen.nome || 'cenário ' + (ci + 1)}`, _DP_STATUS[cen.statusSel] ? _DP_STATUS[cen.statusSel].corLog : null);
    } catch (e) { load(); toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui aplicar o cenário':'Could not apply the scenario') }); }
  };

  const abrir = (item) => { if (!canManage) return; if (bloqueado) { onBloqueado && onBloqueado(); return; } setItemSel(item); };
  const grupo = (cat) => (itens || []).filter(i => i.categoria === cat);
  const tip = (i) => `${(_DP_STATUS[i.status] || {}).label || i.status}${i.status_texto ? ` · ${i.status_texto}` : ''}${i.principal ? (i.categoria === 'prs' ? ' · origem' : ' · preferencial') : ''}${i.atualizado_por ? ` · por ${i.atualizado_por}` : ''}`;

  const indicadoresDe = (cat) => {
    const si = siDe(cat);
    if (si < 0) return null;
    const sc = secoesCfg[si];
    return (sc.indicadores || []).slice(0, 3).map((indRaw, ii) => {
      const ind = _dpNormInd(indRaw);
      const cenAtivo = ind.ativo != null ? ind.cenarios[ind.ativo] : null;
      const cor = cenAtivo ? (_DP_STATUS[cenAtivo.statusSel] || _DP_STATUS.ok).cor : 'var(--muted)';
      const Icone = _DP_ICONES[ind.icone] || _DP_ICONES.check;
      return (
        <span key={cat + ii} onClick={() => { if (!canManage) return; if (bloqueado) { onBloqueado && onBloqueado(); return; } setIndSel({ si, ii }); }} title={`${ind.label || 'indicador'}${cenAtivo && cenAtivo.nome ? ` · ${cenAtivo.nome}` : ''}`} style={{display:'grid',placeItems:'center',width:22,height:22,borderRadius:6,cursor:canManage?'pointer':'default',background:'var(--cream-2)',userSelect:'none',border:cenAtivo?`1px solid ${cor}`:'1px solid transparent'}}>
          {Icone(cor, 14)}
        </span>
      );
    });
  };

  const simbolosTopo = [...grupo('rudder'), ...grupo('ops')];
  const prs = grupo('prs');
  const sensores = grupo('sensor');
  const gruposSens = [];
  for (const i of sensores) { const g = i.grupo || i.nome; let gr = gruposSens.find(x => x.nome === g); if (!gr) { gr = { nome: g, itens: [] }; gruposSens.push(gr); } gr.itens.push(i); }

  const iconeDe = (i) => {
    if (i.categoria === 'rudder') return (cor, s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={cor} strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v5"/><path d="M8.5 7h7l-1.2 12a2.4 2.4 0 0 1-4.6 0L8.5 7z"/></svg>;
    return _DP_ICONES[i.icone] || _DP_ICONES.engren;
  };

  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:16,padding:'14px 16px',height:'100%',boxSizing:'border-box',display:'flex',flexDirection:'column',minHeight:0}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12,flexShrink:0}}>
        <div style={{display:'flex',alignItems:'center',gap:8}}>
          <div className="t-eyebrow">{lang==='pt'?'posicionamento & sensores':'positioning & sensors'}</div>
          {indicadoresDe('rudder')}
          {indicadoresDe('ops')}
        </div>
        {canManage && <button onClick={()=>setEdOpen(true)} title="editar este painel (PRS, sensores, lemes, operações)" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:26,height:26,cursor:'pointer',fontSize:13,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>}
      </div>
      <div style={{flex:1,minHeight:0,overflowY:'auto'}}>
        {itens === null ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'10px 0'}}>{lang==='pt'?'carregando…':'loading…'}</div> : (
          <div style={{display:'grid',gap:14,alignContent:'start'}}>
            {simbolosTopo.length > 0 && (
              <div style={{display:'flex',gap:12,flexWrap:'wrap',paddingBottom:12,borderBottom:'1px solid var(--line-soft)'}}>
                {simbolosTopo.map(i => { const st = _DP_STATUS[i.status] || _DP_STATUS.ok; const Ico = iconeDe(i); return (
                  <div key={i.id} onClick={()=>abrir(i)} title={tip(i)} style={{display:'flex',flexDirection:'column',alignItems:'center',gap:4,cursor:canManage?'pointer':'default',userSelect:'none',maxWidth:64}}>
                    <span style={{width:48,height:48,borderRadius:11,border:`2.5px solid ${st.cor}`,background:i.status==='off'?'var(--cream-2)':'transparent',display:'grid',placeItems:'center',opacity:i.status==='off'?0.65:1,transition:'all .15s'}}>
                      {Ico(st.cor, 24)}
                    </span>
                    <span style={{fontFamily:'var(--fm)',fontSize:9,textAlign:'center',lineHeight:1.2,color:i.status==='off'?'var(--muted)':'var(--ink)',maxWidth:64,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{i.nome}</span>
                  </div>
                ); })}
              </div>
            )}
            {(prs.length > 0 || gruposSens.length > 0) && (
              <div style={{display:'grid',gridTemplateColumns:prs.length && gruposSens.length ? '1fr 1fr' : '1fr',gap:0}}>
                {prs.length > 0 && (
                  <div style={{paddingRight:gruposSens.length?12:0}}>
                    <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:8}}>
                      <span style={{fontFamily:'var(--fm)',fontSize:10,letterSpacing:1.2,textTransform:'uppercase',color:'var(--muted)'}}>PRS</span>
                      {indicadoresDe('prs')}
                    </div>
                    <div style={{display:'grid',gap:2}}>
                      {prs.map(i => (
                        <div key={i.id} style={{display:'flex',alignItems:'center',gap:4,padding:'2px 0'}}>
                          {_dpBolinha(i, 13, ()=>abrir(i), tip(i))}
                          <span onClick={()=>abrir(i)} style={{fontFamily:'var(--f)',fontSize:12.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',minWidth:0,cursor:canManage?'pointer':'default',color:i.status==='off'?'var(--muted)':'var(--ink)'}}>{i.nome}</span>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
                {gruposSens.length > 0 && (
                  <div style={{borderLeft:prs.length?'1px solid var(--line-soft)':'none',paddingLeft:prs.length?14:0}}>
                    <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:8}}>
                      <span style={{fontFamily:'var(--fm)',fontSize:10,letterSpacing:1.2,textTransform:'uppercase',color:'var(--muted)'}}>Sensores</span>
                      {indicadoresDe('sensor')}
                    </div>
                    <div style={{display:'grid',gap:9}}>
                      {gruposSens.map(gr => (
                        <div key={gr.nome}>
                          <div style={{fontFamily:'var(--f)',fontSize:12,marginBottom:3,color:'var(--ink)'}}>{gr.nome}</div>
                          <div style={{display:'flex',gap:4,alignItems:'center'}}>
                            {gr.itens.map(i => _dpBolinha(i, 15, ()=>abrir(i), `${i.nome} · ${tip(i)}`))}
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
              </div>
            )}
            {(itens || []).filter(i => ['prs','sensor','rudder','ops'].includes(i.categoria)).length === 0 && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>nada aqui ainda — {canManage ? 'cadastre PRS, sensores, lemes e operações (ROV, guindaste) no ⚙' : 'peça a um admin pra montar'}</div>}
          </div>
        )}
      </div>
      {itemSel && <LogDPFrasePopover item={(itens||[]).find(p=>p.id===itemSel.id) || itemSel} frases={frases} onFrase={(f)=>aplicarFrase(itemSel, f)} onPrincipal={()=>definirPrincipal(itemSel)} onClose={()=>setItemSel(null)}/>}
      {indSel && (() => { const sc = secoesCfg[indSel.si]; const ind = sc && sc.indicadores && sc.indicadores[indSel.ii]; if (!ind) return null; return <LogDPCenarioPopover ind={_dpNormInd(ind)} itensDaSecao={(itens||[]).filter(e=>e.categoria===sc.cat)} onCenario={(ci)=>aplicarCenario(indSel.si, indSel.ii, ci)} onClose={()=>setIndSel(null)}/>; })()}
      {edOpen && <LogDPEditorModal modo="ref" itens={itens || []} frases={frases} config={config} token={token} toast={toast} onClose={()=>setEdOpen(false)} onSaved={()=>{ setEdOpen(false); load(); }}/>}
    </div>
  );
};

const LogPainelDP = ({ token, toast, canManage, onLog, bloqueado, onBloqueado }) => {
  const lang = useT().lang;
  const [itens, setItens] = React.useState(null);
  const [frases, setFrases] = React.useState([]);
  const [config, setConfig] = React.useState(null);
  const [edOpen, setEdOpen] = React.useState(false);
  const [itemSel, setItemSel] = React.useState(null);
  const [indSel, setIndSel] = React.useState(null); // {si, ii}
  const [confirmar, setConfirmar] = React.useState(null); // {texto, onSim}
  const mudandoRef = React.useRef(false);
  const H = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` });

  const load = () => Promise.all([
    fetch('/api/logbook/equipdp', { headers: H() }).then(r => r.ok ? r.json() : []),
    fetch('/api/logbook/equipdp/frases', { headers: H() }).then(r => r.ok ? r.json() : []),
    fetch('/api/logbook/equipdp/config', { headers: H() }).then(r => r.ok ? r.json() : { config: null }),
  ]).then(([e, f, c]) => { setItens(Array.isArray(e) ? e : []); setFrases(Array.isArray(f) ? f : []); setConfig((c && c.config) || null); }).catch(() => { if (itens === null) setItens([]); });
  React.useEffect(() => { load(); const t = setInterval(load, 30000); return () => clearInterval(t); }, []);

  const aplicarFrase = async (item, f) => {
    if (!canManage || mudandoRef.current) return;
    if (item.categoria === 'thruster' && (f.status === 'ok' || f.status === 'warn')) {
      const u = _dpNormUnif(config, itens);
      if (u.ativo && !_dpUnifAlimentado(u, _dpUnifEnergia(u, itens), item.id)) {
        setItemSel(null);
        toast.push({ icon: 'x', title: `${item.nome} sem alimentação`, body: 'Feche um breaker pra um barramento energizado antes de ligar.' });
        return;
      }
    }
    mudandoRef.current = true;
    setItemSel(null);
    const anterior = item.status;
    setItens(prev => prev.map(i => i.id === item.id ? { ...i, status: f.status } : i)); // otimista
    try {
      const r = await fetch(`/api/logbook/equipdp/${item.id}/status`, { method: 'POST', headers: H(), body: JSON.stringify({ status: f.status, texto: f.texto }) });
      if (!r.ok) throw new Error();
      if (onLog) onLog(`${item.nome}: ${f.texto}`, _DP_STATUS[f.status] ? _DP_STATUS[f.status].corLog : null);
    } catch (e) {
      setItens(prev => prev.map(i => i.id === item.id ? { ...i, status: anterior } : i));
      toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui aplicar':'Could not apply') });
    }
    mudandoRef.current = false;
  };

  const definirPrincipal = async (item) => {
    if (!canManage) return;
    setItemSel(null);
    try {
      const r = await fetch(`/api/logbook/equipdp/${item.id}/principal`, { method: 'POST', headers: H() });
      if (!r.ok) throw new Error();
      if (onLog) onLog(`${item.nome} selecionado como ${item.categoria === 'prs' ? 'origem' : 'preferencial'}`, 'verde');
      load();
    } catch (e) { toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui definir':'Could not set') }); }
  };

  // aplica um cenário do indicador: pinta os equipamentos marcados + registra no log
  const aplicarCenario = async (si, ii, ci) => {
    if (!canManage) return;
    setIndSel(null);
    const secoesAtuais = secoesCfg.map(sc => ({ cat: sc.cat, indicadores: (sc.indicadores || []).map(_dpNormInd) }));
    const ind = secoesAtuais[si] && secoesAtuais[si].indicadores[ii]; if (!ind) return;
    const cen = ind.cenarios[ci]; if (!cen) return;
    ind.ativo = ci;
    const alvos = (itens || []).filter(e => e.categoria === secoesAtuais[si].cat);
    const mudancas = [];
    for (const eq of alvos) {
      const alvoStatus = cen.equipIds.includes(eq.id) ? cen.statusSel : (cen.statusResto !== 'keep' ? cen.statusResto : null);
      if (alvoStatus && alvoStatus !== eq.status) mudancas.push({ id: eq.id, status: alvoStatus });
    }
    // otimista: pinta local + salva config com o cenário ativo
    setItens(prev => prev.map(i => { const m = mudancas.find(x => x.id === i.id); return m ? { ...i, status: m.status } : i; }));
    setConfig({ secoes: secoesAtuais });
    try {
      const rs = await Promise.all([
        ...mudancas.map(m => fetch(`/api/logbook/equipdp/${m.id}/status`, { method: 'POST', headers: H(), body: JSON.stringify({ status: m.status }) })),
        fetch('/api/logbook/equipdp/config', { method: 'PUT', headers: H(), body: JSON.stringify({ config: { secoes: secoesAtuais } }) }),
      ]);
      if (rs.some(r => !r.ok)) throw new Error();
      if (onLog) onLog(`${ind.label ? ind.label + ': ' : ''}${cen.nome || 'cenário ' + (ci + 1)}`, _DP_STATUS[cen.statusSel] ? _DP_STATUS[cen.statusSel].corLog : null);
    } catch (e) { load(); toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui aplicar o cenário':'Could not apply the scenario') }); }
  };

  const abrir = (item) => { if (!canManage) return; if (bloqueado) { onBloqueado && onBloqueado(); return; } setItemSel(item); };
  const grupo = (cat) => (itens || []).filter(i => i.categoria === cat);
  const tip = (i) => `${(_DP_STATUS[i.status] || {}).label || i.status}${i.status_texto ? ` · ${i.status_texto}` : ''}${i.principal ? (i.categoria === 'prs' ? ' · origem' : ' · preferencial') : ''}${i.atualizado_por ? ` · por ${i.atualizado_por}` : ''}`;

  // seções na ordem da config
  const secoesCfg = _dpNormSecoes(config);
  const unif = _dpNormUnif(config, itens);
  const salvarUnif = async (novoUnif, textoLog, corLog) => {
    const cfgNovo = { secoes: secoesCfg, unifilar: novoUnif };
    setConfig(cfgNovo); // otimista
    try {
      const r = await fetch('/api/logbook/equipdp/config', { method: 'PUT', headers: H(), body: JSON.stringify({ config: cfgNovo }) });
      if (!r.ok) throw new Error();
      if (onLog && textoLog) onLog(textoLog, corLog);
    } catch (e) { load(); toast.push({ icon: 'x', title: (lang==='pt'?'Não consegui salvar':'Could not save') }); }
  };
  const toggleBreaker = (eq, alvo, rotulo, fechar) => {
    if (!canManage) return;
    if (bloqueado) { onBloqueado && onBloqueado(); return; }
    setConfirmar({ texto: `${fechar ? 'Fechar' : 'Abrir'} ${rotulo}?`, onSim: () => {
      const atual = unif.br[eq.id] || { barras: 'A', A: false, B: false };
      const novoUnif = { ...unif, br: { ...unif.br, [eq.id]: { ...atual, [alvo]: fechar } } };
      salvarUnif(novoUnif, `${rotulo} ${fechar ? 'fechado' : 'aberto'}`, fechar ? 'verde' : 'cinza');
    } });
  };
  const toggleTie = (fechar) => {
    if (!canManage) return;
    if (bloqueado) { onBloqueado && onBloqueado(); return; }
    setConfirmar({ texto: `${fechar ? 'Fechar' : 'Abrir'} B1-S1 (bus tie)?`, onSim: () => salvarUnif({ ...unif, tie: fechar }, `B1-S1 (bus tie) ${fechar ? 'fechado' : 'aberto'}`, fechar ? 'verde' : 'cinza') });
  };

  const tituloSecao = (sc, si) => (
    <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
      <span style={{fontFamily:'var(--fm)',fontSize:10,letterSpacing:1.2,textTransform:'uppercase',color:'var(--muted)'}}>{_DP_SECOES[sc.cat]}</span>
      {(sc.indicadores || []).slice(0, 3).map((indRaw, ii) => {
        const ind = _dpNormInd(indRaw);
        const cenAtivo = ind.ativo != null ? ind.cenarios[ind.ativo] : null;
        const cor = cenAtivo ? (_DP_STATUS[cenAtivo.statusSel] || _DP_STATUS.ok).cor : 'var(--muted)';
        const Icone = _DP_ICONES[ind.icone] || _DP_ICONES.check;
        return (
          <span key={ii} onClick={() => { if (!canManage) return; if (bloqueado) { onBloqueado && onBloqueado(); return; } setIndSel({ si, ii }); }} title={`${ind.label || 'indicador'}${cenAtivo && cenAtivo.nome ? ` · ${cenAtivo.nome}` : ''}${canManage ? ' — toque pra escolher o cenário' : ''}`} style={{display:'grid',placeItems:'center',width:22,height:22,borderRadius:6,cursor:canManage?'pointer':'default',background:'var(--cream-2)',userSelect:'none',border:cenAtivo?`1px solid ${cor}`:'1px solid transparent'}}>
            {Icone(cor, 14)}
          </span>
        );
      })}
    </div>
  );

  const corpoSecao = (sc) => {
    const lista = grupo(sc.cat);
    if (!lista.length) return null;
    const T = _DP_TAM[sc.tam] || _DP_TAM.m;
    const dirSt = { display: 'flex', gap: sc.orient === 'v' ? 8 : 12, flexWrap: sc.orient === 'v' ? 'nowrap' : 'wrap', flexDirection: sc.orient === 'v' ? 'column' : 'row', alignItems: sc.orient === 'v' ? 'flex-start' : 'flex-start' };
    const simbolo = (i, st, forma) => {
      const comum = { border: `2.5px solid ${st.cor}`, background: i.status === 'off' ? 'var(--cream-2)' : 'transparent', display: 'grid', placeItems: 'center', opacity: i.status === 'off' ? 0.65 : 1, transition: 'all .15s', width: T.simb, height: T.simb, flexShrink: 0 };
      if (forma === 'circ') return <span style={{...comum, borderRadius: '50%'}}>{_dpHelice(st.cor, T.ico)}</span>;
      if (forma === 'raio') return <span style={{...comum, borderRadius: Math.round(T.simb * 0.24)}}><svg width={T.ico * 0.78} height={T.ico * 0.78} viewBox="0 0 24 24" fill={st.cor}><path d="M13 2L4.5 13.5h5L10 22l8.5-11.5h-5L13 2z"/></svg></span>;
      return <span style={{...comum, borderRadius: Math.round(T.simb * 0.24)}}><svg width={T.ico * 0.82} height={T.ico * 0.82} viewBox="0 0 24 24" fill="none" stroke={st.cor} strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v5"/><path d="M8.5 7h7l-1.2 12a2.4 2.4 0 0 1-4.6 0L8.5 7z"/></svg></span>;
    };
    const formaPorCat = { thruster: 'circ', gerador: 'raio', rudder: 'leme' };
    if (formaPorCat[sc.cat]) {
      const orientItem = sc.orient === 'v' ? { flexDirection: 'row', gap: 8 } : { flexDirection: 'column', gap: 5 };
      return (
        <div style={dirSt}>
          {lista.map(i => { const st = _DP_STATUS[i.status] || _DP_STATUS.ok; return (
            <div key={i.id} onClick={()=>abrir(i)} title={tip(i)} style={{display:'flex',alignItems:'center',cursor:canManage?'pointer':'default',userSelect:'none',...orientItem}}>
              {simbolo(i, st, formaPorCat[sc.cat])}
              <span style={{fontFamily:'var(--fm)',fontSize:T.fonte,color:i.status==='off'?'var(--muted)':'var(--ink)'}}>{i.nome}</span>
            </div>
          ); })}
        </div>
      );
    }
    if (sc.cat === 'prs') return (
      <div style={{display:'grid',gap:3}}>
        {lista.map(i => (
          <div key={i.id} style={{display:'flex',alignItems:'center',gap:5,padding:'3px 4px'}}>
            <span style={{fontFamily:'var(--f)',fontSize:12.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',minWidth:0,color:i.status==='off'?'var(--muted)':'var(--ink)'}}>{i.nome}</span>
            {_dpBolinha(i, 13, ()=>abrir(i), tip(i))}
          </div>
        ))}
      </div>
    );
    // sensores: agrupados — nome do grupo + bolinhas coladas
    const grupos = [];
    for (const i of lista) { const g = i.grupo || i.nome; let gr = grupos.find(x => x.nome === g); if (!gr) { gr = { nome: g, itens: [] }; grupos.push(gr); } gr.itens.push(i); }
    return (
      <div style={{display:'grid',gap:3}}>
        {grupos.map(gr => (
          <div key={gr.nome} style={{display:'flex',alignItems:'center',gap:5,padding:'3px 4px'}}>
            <span style={{fontFamily:'var(--f)',fontSize:12.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',minWidth:0}}>{gr.nome}</span>
            <span style={{display:'flex',gap:2,alignItems:'center',flexShrink:0}}>
              {gr.itens.map(i => _dpBolinha(i, 13, ()=>abrir(i), `${i.nome} · ${tip(i)}`))}
            </span>
          </div>
        ))}
      </div>
    );
  };

  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:16,padding:'14px 16px',height:'100%',boxSizing:'border-box',display:'flex',flexDirection:'column',minHeight:0}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12,flexShrink:0}}>
        <div className="t-eyebrow">{lang==='pt'?'console dp':'dp console'}</div>
        {canManage && <button onClick={()=>setEdOpen(true)} title="editar a planta (frases, ligações, indicadores)" style={{background:'var(--accent)',color:'#fff',border:'none',borderRadius:8,width:26,height:26,cursor:'pointer',fontSize:13,display:'grid',placeItems:'center',flexShrink:0}}>⚙</button>}
      </div>
      <div style={{flex:1,minHeight:0,overflowY:'auto'}}>
        {itens === null ? <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'10px 0'}}>{lang==='pt'?'carregando…':'loading…'}</div> : (
          <div style={{display:'flex',gap:22,flexWrap:'wrap',alignItems:'flex-start'}}>
            <div style={{flex:'1 1 100%',minWidth:0}}>
              <div style={{display:'flex',gap:18,flexWrap:'wrap',marginBottom:4}}>
                {secoesCfg.map((sc, si) => (sc.cat === 'thruster' || sc.cat === 'gerador') && (sc.indicadores || []).length ? <div key={sc.cat}>{tituloSecao(sc, si)}</div> : null)}
              </div>
              <LogDPUnifilar itens={itens} unif={unif} canManage={canManage} onEquip={abrir} onBreaker={toggleBreaker} onTie={toggleTie}/>
            </div>
            {(itens || []).length === 0 && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>nenhum equipamento — {canManage ? 'use o ⚙ pra montar a lista' : 'peça a um admin pra montar'}</div>}
          </div>
        )}
      </div>
      {confirmar && <LogDPConfirmModal texto={confirmar.texto} onSim={confirmar.onSim} onClose={()=>setConfirmar(null)}/>}
      {itemSel && <LogDPFrasePopover item={itemSel} frases={frases} onFrase={(f)=>aplicarFrase(itemSel, f)} onPrincipal={()=>definirPrincipal(itemSel)} onClose={()=>setItemSel(null)}/>}
      {indSel && (() => { const sc = secoesCfg[indSel.si]; const ind = sc && sc.indicadores && sc.indicadores[indSel.ii]; if (!ind) return null; return <LogDPCenarioPopover ind={_dpNormInd(ind)} itensDaSecao={(itens||[]).filter(e=>e.categoria===sc.cat)} onCenario={(ci)=>aplicarCenario(indSel.si, indSel.ii, ci)} onClose={()=>setIndSel(null)}/>; })()}
      {edOpen && <LogDPEditorModal modo="forca" itens={itens || []} frases={frases} config={config} token={token} toast={toast} onClose={()=>setEdOpen(false)} onSaved={()=>{ setEdOpen(false); load(); }}/>}
    </div>
  );
};

const Logbook = ({state, setState, setPage}) => {
  const lang = useT().lang;
  const toast = useToast();
  const isMobile = useMobile();
  const token = () => localStorage.getItem('genesis_token');
  const me = state.currentUser;
  const chaves = state.currentUser?.permissoes || [];
  // gerir = configurar console/frases/sequências · dpr = gerar o relatório do cliente
  const canManage = me.role === 'admin' || me.is_staff === true || chaves.includes('logbook.gerir');
  const podeDpr = me.role === 'admin' || me.is_staff === true || chaves.includes('dpr.gerar');
  // Mod 02 · registrar = operar o console (entradas, semáforo DP, ASOG) sem poder configurar
  const podeRegistrar = canManage || chaves.includes('logbook.registrar');

  const [data, setData] = React.useState(_logHojeStr());
  const [entradas, setEntradas] = React.useState(null);
  const [tripulacao, setTripulacao] = React.useState([]);
  const [texto, setTexto] = React.useState('');
  const [foto, setFoto] = React.useState(null);
  const [enviando, setEnviando] = React.useState(false);
  const [editar, setEditar] = React.useState(null);
  const [histEntrada, setHistEntrada] = React.useState(null);
  const [turnoAtual, setTurnoAtual] = React.useState(null);
  const [handover, setHandover] = React.useState(false);
  const [buscaOpen, setBuscaOpen] = React.useState(false); // Mod 02 · busca no log inteiro
  const [horaManual, setHoraManual] = React.useState('');
  const loadTurno = () => fetch('/api/logbook/turnos/atual', {headers:H()}).then(r=>r.ok?r.json():null).then(setTurnoAtual).catch(()=>{});
  const [abas, setAbas] = React.useState([]);
  const [abaAtiva, setAbaAtiva] = React.useState(null);
  const [gerenciar, setGerenciar] = React.useState(false);
  const [gerenciarAba, setGerenciarAba] = React.useState('biblioteca');
  const [macros, setMacros] = React.useState([]);
  const [macroConfirm, setMacroConfirm] = React.useState(null);
  const [seqRun, setSeqRun] = React.useState(null);
  const [dpCfg, setDpCfg] = React.useState(null);
  const [alertasOpen, setAlertasOpen] = React.useState(false);
  const [temAlertas, setTemAlertas] = React.useState(false);
  React.useEffect(() => {
    const carga = () => Promise.all([
      fetch('/api/logbook/equipdp/config', { headers: { 'Authorization': `Bearer ${token()}` } }).then(r=>r.ok?r.json():{config:null}).then(c=>setDpCfg((c&&c.config)||null)),
      fetch('/api/logbook/alertas', { headers: { 'Authorization': `Bearer ${token()}` } }).then(r=>r.ok?r.json():null).then(d=>setTemAlertas(!!(d && d.atual && d.atual.texto && d.atual.texto.trim()))),
    ]).catch(()=>{});
    carga(); const t = setInterval(carga, 60000); return () => clearInterval(t);
  }, []);
  const mudarDpChave = async (chave, nivel, dict, rotulo) => {
    const cfgNovo = { ...(dpCfg || {}), [chave]: nivel };
    setDpCfg(cfgNovo);
    try {
      const r = await fetch('/api/logbook/equipdp/config', { method:'PUT', headers: { 'Content-Type':'application/json', 'Authorization': `Bearer ${token()}` }, body: JSON.stringify({ config: cfgNovo }) });
      if (!r.ok) throw new Error();
      const n = dict[nivel] || {};
      postEntrada({ texto: `${rotulo}: ${lang==='pt'?n.pt:n.en}`, cor: n.corLog });
    } catch (e) { toast.push({ icon:'x', title: (lang==='pt'?'Não consegui mudar':'Could not change') }); }
  };
  const [equipCores, setEquipCores] = React.useState({});
  const [fOrdem, setFOrdem] = React.useState('desc');
  const [fAutor, setFAutor] = React.useState('');
  const [fSoAnexo, setFSoAnexo] = React.useState(false);
  const [fSemCanc, setFSemCanc] = React.useState(false);
  const [filtroAberto, setFiltroAberto] = React.useState(false);
  const [template, setTemplate] = React.useState(null);
  const [noturno, setNoturno] = React.useState(() => localStorage.getItem('genesis_logbook_noturno') === '1');

  const [layoutCfg, setLayoutCfg] = React.useState(_logLayoutCfg());
  const [editMode, setEditMode] = React.useState(false);
  const [colapsados, setColapsados] = React.useState(() => { try { return new Set(JSON.parse(localStorage.getItem('genesis_log_colaps') || '[]')); } catch(e) { return new Set(); } });
  const toggleColapso = (id) => setColapsados(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); try { localStorage.setItem('genesis_log_colaps', JSON.stringify([...n])); } catch(e){} return n; });
  const [meteoAtrasado, setMeteoAtrasado] = React.useState(false);
  const [dragId, setDragId] = React.useState(null);
  const [guias, setGuias] = React.useState(null);
  const layoutRef = React.useRef(layoutCfg); layoutRef.current = layoutCfg;
  const gridRef = React.useRef(null);
  const dragInfo = React.useRef(null);
  const layoutDirty = React.useRef(false);   // editou nesta sessão → servidor não sobrescreve
  const avisouPutRef = React.useRef(false);
  // persiste local (cache) + servidor (loadout por usuário — tablet compartilhado)
  const persistirLayout = (c) => {
    layoutDirty.current = true;
    c.ts = Date.now();
    _logSaveLayoutCfg(c);
    fetch('/api/logbook/layout', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` }, body: JSON.stringify({ layout: c }) })
      .then(r => {
        if (!r.ok && !avisouPutRef.current) {
          avisouPutRef.current = true;
          console.warn('PUT /api/logbook/layout falhou:', r.status);
          toast.push({ icon: 'x', title: (lang==='pt'?'Layout não salvou no servidor':'Layout did not save on the server'), body: 'Ficou só neste aparelho. Backend desatualizado? Reinicia o container.' });
        }
      })
      .catch(() => {});
  };
  // ao montar: o loadout do servidor é a fonte da verdade (se existir)
  React.useEffect(() => {
    fetch('/api/logbook/layout', { headers: { 'Authorization': `Bearer ${token()}` } })
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (layoutDirty.current) return; // o que você editou agora vence o servidor
        if (d && d.layout) {
          const c = _logSanitizarLayout(d.layout);
          const tsLocal = (layoutRef.current && layoutRef.current.ts) || 0;
          if (c && (c.ts || 0) >= tsLocal && JSON.stringify(c.widgets) !== JSON.stringify(layoutRef.current.widgets)) { setLayoutCfg(c); _logSaveLayoutCfg(c); }
        }
      }).catch(() => {});
  }, []);
  React.useEffect(() => {
    const root = document.documentElement;
    if (noturno) { Object.entries(_LOG_TEMA_NOTURNO).forEach(([k,v]) => root.style.setProperty(k, v)); localStorage.setItem('genesis_logbook_noturno','1'); }
    else { Object.keys(_LOG_TEMA_NOTURNO).forEach(k => root.style.removeProperty(k)); localStorage.setItem('genesis_logbook_noturno','0'); }
    return () => { Object.keys(_LOG_TEMA_NOTURNO).forEach(k => root.style.removeProperty(k)); };
  }, [noturno]);

  const H = () => ({'Authorization':`Bearer ${token()}`});
  const loadEntradas = () => fetch(`/api/logbook/entradas?data=${data}`, {headers:H()}).then(r=>r.ok?r.json():[]).then(setEntradas).catch(()=>setEntradas([]));
  const loadAbas = () => fetch('/api/logbook/abas', {headers:H()}).then(r=>r.ok?r.json():[]).then(d => { setAbas(d); setAbaAtiva(prev => (d.find(a=>a.id===prev) ? prev : (d[0] ? d[0].id : null))); }).catch(()=>setAbas([]));
  const loadMacros = () => fetch('/api/logbook/macros', {headers:H()}).then(r=>r.ok?r.json():[]).then(setMacros).catch(()=>setMacros([]));
  const loadCores = () => fetch('/api/logbook/equip-cores', {headers:H()}).then(r=>r.ok?r.json():{}).then(setEquipCores).catch(()=>{});
  // Mod 02 · o log se atualiza sozinho: polling leve quando está em "hoje" + refetch ao voltar pra aba.
  // Cobre entradas de outros aparelhos e a troca de turno feita fora deste tablet (autorSel correto).
  React.useEffect(() => { loadEntradas(); }, [data]);
  React.useEffect(() => {
    const vivo = () => { if (data === _logHojeStr()) { loadEntradas(); loadCores(); loadTurno(); } };
    const t = setInterval(vivo, 30000);
    const onVis = () => { if (!document.hidden) vivo(); };
    document.addEventListener('visibilitychange', onVis);
    return () => { clearInterval(t); document.removeEventListener('visibilitychange', onVis); };
  }, [data]);
  React.useEffect(() => { loadAbas(); loadCores(); loadMacros(); loadTurno(); }, []);
  React.useEffect(() => { loadAbas(); }, []);
  React.useEffect(() => {
    fetch('/api/boarding/manifesto', {headers:H()}).then(r=>r.ok?r.json():{abordo:[]})
      .then(d => setTripulacao((d.abordo||[]).map(u => ({ ...u, user_id: u.user_id != null ? u.user_id : u.id })).filter(u=>u.nome && (u.is_staff===true || u.role==='admin')).sort((a,b)=>a.nome.localeCompare(b.nome)))).catch(()=>{});
  }, []);

  // conta compartilhada (admin, tablet/PC do passadiço): autor = quem fez login no desk; ninguém logado → bloqueia
  // conta pessoal (staff no próprio aparelho): autor = a própria pessoa, sem troca
  const ehContaAdmin = me.role === 'admin';
  const autorSel = !ehContaAdmin
    ? {user_id: me.id, nome: me.nome, foto_url: me.foto_url, initials: me.initials}
    : (turnoAtual
        ? (tripulacao.find(u => u.user_id === turnoAtual.operador_id) || {user_id: turnoAtual.operador_id, nome: turnoAtual.operador_nome, foto_url: null, initials: null})
        : null);
  const semOperador = ehContaAdmin && !turnoAtual;

  const upFoto = async (file) => {
    if (!file) return;
    const fd = new FormData(); fd.append('photo', file);
    const r = await fetch('/api/upload/photo', {method:'POST', headers:H(), body:fd});
    const d = await r.json();
    if (r.ok && d.url) { setFoto(d.url); toast.push({icon:'check', title:'Foto anexada'}); }
    else toast.push({icon:'x', title:(lang==='pt'?'Erro no upload':'Upload error')});
  };

  const postEntrada = async (extra) => {
    const e = { ...extra };
    // registrando num dia que não é hoje e sem hora explícita → ancora no dia selecionado (senão o backend usa NOW = hoje)
    if (e.ts == null && data !== _logHojeStr()) {
      const now = new Date();
      const hhmm = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
      const d = new Date(`${data}T${hhmm}`);
      if (!isNaN(d)) e.ts = d.toISOString();
    }
    if (!autorSel) { setHandover(true); toast.push({icon:'x', title:'Ninguém de serviço', body:'Faça login no desk pra registrar'}); return false; }
    const body = { autor_id: autorSel.user_id, autor_nome: autorSel.nome, ...e };
    try {
      const r = await fetch('/api/logbook/entradas', {method:'POST', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify(body)});
      if (r.ok) { loadEntradas(); loadCores(); return true; }
      toast.push({icon:'x', title:(lang==='pt'?'Erro ao registrar':'Error logging')}); return false;
    } catch (err) {
      // Mod 02 · sem conexão com o NAS: guarda na fila local (com a hora original) e reenvia sozinho
      if (body.ts == null) body.ts = new Date().toISOString();
      const q = _logOutboxLoad(); q.push(body); _logOutboxSave(q); setFila(q.length);
      toast.push({icon:'clock', title:(lang==='pt'?'Sem conexão — guardei na fila':'Offline — queued'), body:(lang==='pt'?'Reenvia sozinho quando a rede voltar':'Retries when the network is back')});
      return true;
    }
  };
  // fila offline: estado + reenvio automático (20s + quando a rede volta)
  const [fila, setFila] = React.useState(() => _logOutboxLoad().length);
  const filaBusyRef = React.useRef(false);
  React.useEffect(() => {
    const flush = async () => {
      if (filaBusyRef.current) return;
      const q = _logOutboxLoad();
      if (!q.length) { if (fila !== 0) setFila(0); return; }
      filaBusyRef.current = true;
      let enviados = 0;
      const resto = [];
      for (const body of q) {
        try {
          const r = await fetch('/api/logbook/entradas', {method:'POST', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify(body)});
          if (r.ok) { enviados++; continue; }
          if (r.status >= 500) resto.push(body); // servidor instável: tenta de novo no próximo ciclo
          // 4xx: registro inválido — descarta pra não travar a fila
        } catch (e) { resto.push(body); }
      }
      filaBusyRef.current = false;
      _logOutboxSave(resto); setFila(resto.length);
      if (enviados) { loadEntradas(); loadCores(); toast.push({icon:'check', title:(lang==='pt'?`${enviados} registro(s) da fila enviados`:`${enviados} queued entries sent`) }); }
    };
    const t = setInterval(flush, 20000);
    window.addEventListener('online', flush);
    return () => { clearInterval(t); window.removeEventListener('online', flush); };
  }, [data, fila]);
  const registrar = async () => {
    if (!texto.trim() && !foto) { toast.push({icon:'x', title:'Escreva algo ou anexe uma foto'}); return; }
    setEnviando(true);
    try {
      const extra = { texto: texto.trim() || '(foto)', foto_url: foto };
      if (horaManual) { const d = new Date(`${data}T${horaManual}`); if (!isNaN(d)) extra.ts = d.toISOString(); }
      const ok = await postEntrada(extra);
      if (ok) { setTexto(''); setFoto(null); setHoraManual(''); }
    }
    finally { setEnviando(false); }
  };
  const registrarFraseFinal = async (f, texto) => {
    if (f.id) { fetch(`/api/logbook/frases/${f.id}/uso`, {method:'POST', headers:H()}).then(()=>loadAbas()).catch(()=>{}); }
    if (f.alvos && f.alvos.length) {
      const st = f.cor === 'amarelo' ? 'warn' : f.cor === 'vermelho' ? 'alarm' : f.cor === 'cinza' ? 'off' : 'ok';
      let n = 0;
      for (const eid of f.alvos) {
        try { const r = await fetch(`/api/logbook/equipdp/${eid}/status`, { method:'POST', headers: {...H(), 'Content-Type':'application/json'}, body: JSON.stringify({ status: st, texto }) }); if (r.ok) n++; } catch(e) {}
      }
      await postEntrada({ texto, cor: f.cor });
      if (n) toast.push({icon:'check', title:`${n} equipamento(s) atualizados no console`, body:texto});
    } else {
      const ok = await postEntrada({ texto, cor: f.cor }); if (ok) toast.push({icon:'check', title:'Registrado', body:texto});
    }
  };
  const dispararMacro = async (macro, horas) => {
    let n = 0;
    for (let i = 0; i < macro.passos.length; i++) {
      const p = macro.passos[i];
      const extra = { texto: p.texto, cor: p.cor, equip_id: p.equip_id };
      const hh = horas && horas[i];
      if (hh) { const d = new Date(`${data}T${hh}`); if (!isNaN(d)) extra.ts = d.toISOString(); }
      const ok = await postEntrada(extra); if (ok) n++;
    }
    if (n) toast.push({icon:'check', title:`⚡ ${macro.nome}`, body:`${n} registro(s)`});
  };
  const registrarFrase = async (f) => {
    if (_logSlots(f.texto).length > 0) { setTemplate(f); return; }
    await registrarFraseFinal(f, f.texto);
  };
  const registrarEquip = async (equip, f) => { const ok = await postEntrada({ texto: f.texto, cor: f.cor, equip_id: equip.id }); if (ok) toast.push({icon:'check', title:'Registrado', body:`${equip.nome}: ${f.texto}`}); };
  // Mod 02 · cancelamento com motivo obrigatório em modal próprio (rastro IMCA)
  const [cancelando, setCancelando] = React.useState(null);
  const cancelarEntrada = (e) => setCancelando(e);
  const confirmarCancelamento = async (motivo) => {
    const e = cancelando; setCancelando(null);
    if (!e) return;
    await fetch(`/api/logbook/entradas/${e.id}`, {method:'DELETE', headers:{...H(),'Content-Type':'application/json'}, body:JSON.stringify({motivo, por_nome:(autorSel||{}).nome||'—'})});
    toast.push({icon:'check', title:(lang==='pt'?'Entrada cancelada':'Entry cancelled')}); loadEntradas(); loadCores();
  };
  const navDia = (delta) => { const d = new Date(data + 'T12:00'); d.setDate(d.getDate() + delta); setData(d.toISOString().slice(0,10)); };
  const dateRef = React.useRef(null);
  const abrirCalendario = () => { const el = dateRef.current; if (el) { try { el.showPicker(); } catch(_) { el.focus(); el.click(); } } };

  const [dprOpen, setDprOpen] = React.useState(false);
  const gerarDpr = async (cfg, resumo, preparadoPor) => {
    let turnos = [];
    try { const r = await fetch(`/api/logbook/turnos?data=${data}`, {headers:H()}); if (r.ok) turnos = await r.json(); } catch(e){}
    let horarios = [];
    try { const r = await fetch(`/api/logbook/horarios?data=${data}`, {headers:H()}); if (r.ok) horarios = await r.json(); } catch(e){}
    let npts = [];
    try { const r = await fetch(`/api/npt/eventos?desde=${data}T00:00:00&ate=${data}T23:59:59`, {headers:H()}); if (r.ok) npts = await r.json(); } catch(e){}
    let pob = null, movDia = null;
    try { const r = await fetch('/api/boarding/manifesto', {headers:H()}); if (r.ok) { const m = await r.json(); pob = (m.total_bordo != null) ? m.total_bordo : (Array.isArray(m.abordo) ? m.abordo.length : (Array.isArray(m) ? m.length : null)); } } catch(e){}
    try { const r = await fetch('/api/boarding/log', {headers:H()}); if (r.ok) { const lg = await r.json(); const arr = Array.isArray(lg) ? lg : (lg.log || lg.eventos || []); movDia = arr.filter(x => String(x.created_at || x.timestamp || x.quando || '').slice(0,10) === data).length; } } catch(e){}
    const deServico = turnoAtual ? turnoAtual.operador_nome : (turnos.length ? turnos[turnos.length-1].operador_nome : '');
    const html = `<!doctype html><html><head><meta charset="utf-8"><title>DPR ${_pvEsc(cfg.navio||'Genesis I')} — ${_pvEsc(data)}</title><style>${_dprCss()}</style></head><body>`
      + _dprHead(cfg, data, deServico)
      + _dprResumoExec(resumo)
      + _dprStrip(lista, npts, pob, movDia)
      + _dprLog(lista)
      + _dprNpt(npts)
      + _dprMeteo(horarios, _logGraphCfg())
      + _dprGaleria(lista)
      + _dprAssin()
      + `<div class="dfoot"><span>${_pvEsc(cfg.navio||'MPSV Genesis I')} · Daily Progress Report DPR-${data.replace(/-/g,'')}</span><span>generated by Portal Genesis · ${_pvEsc(new Date().toLocaleString('pt-BR'))}</span></div></body></html>`;
    const win = window.open('', '_blank');
    if (!win) { toast.push({icon:'x', title:'Libere os pop-ups', body:'O relatório abre numa aba nova'}); return; }
    win.document.write(html); win.document.close(); win.focus();
    setTimeout(() => { try { win.print(); } catch (e) {} }, 800);
  };
  const gerarPavao = async () => {
    if (!lista.length) { toast.push({icon:'x', title:'Sem entradas neste dia'}); return; }
    let turnos = [];
    try { const r = await fetch(`/api/logbook/turnos?data=${data}`, {headers:H()}); if (r.ok) turnos = await r.json(); } catch(e){}
    let horarios = [];
    try { const r = await fetch(`/api/logbook/horarios?data=${data}`, {headers:H()}); if (r.ok) horarios = await r.json(); } catch(e){}
    let npts = [];
    try { const r = await fetch(`/api/npt/eventos?desde=${data}T00:00:00&ate=${data}T23:59:59`, {headers:H()}); if (r.ok) npts = await r.json(); } catch(e){}
    const deServico = turnoAtual ? turnoAtual.operador_nome : (turnos.length ? turnos[turnos.length-1].operador_nome : '');
    const html = `<!doctype html><html><head><meta charset="utf-8"><title>Logbook Genesis I — ${_pvEsc(data)}</title><style>${_pvCss()}</style></head><body>`
      + _pvCapa(data, deServico) + _pvResumo(lista, npts) + _pvTimeline(lista) + _pvMeteo(horarios, _logGraphCfg()) + _pvNpt(npts) + _pvLog(lista) + _pvAssinaturas(turnos) + _pvGaleria(lista)
      + `<div class="foot">Portal Genesis · gerado eletronicamente em ${_pvEsc(new Date().toLocaleString('pt-BR'))}</div></body></html>`;
    const win = window.open('', '_blank');
    if (!win) { toast.push({icon:'x', title:'Libere os pop-ups', body:'O relatório abre numa aba nova'}); return; }
    win.document.write(html); win.document.close(); win.focus();
    setTimeout(() => { try { win.print(); } catch (e) {} }, 800);
  };

  const lista = entradas || [];
  const ehHoje = data === _logHojeStr();
  const autoresDoDia = [...new Set(lista.map(e => e.autor_nome).filter(Boolean))].sort();
  let listaView = lista.slice();
  if (fAutor) listaView = listaView.filter(e => e.autor_nome === fAutor);
  if (fSoAnexo) listaView = listaView.filter(e => e.foto_url);
  if (!fSemCanc) listaView = listaView.filter(e => !e.cancelada); // removidas ficam ocultas por padrão (auditoria via filtro)
  if (fOrdem === 'desc') listaView = listaView.reverse();
  const filtroAtivo = fOrdem !== 'asc' || !!fAutor || fSoAnexo || fSemCanc;

  const painelFrases = <LogFrasesPanel abas={abas} abaAtiva={abaAtiva} setAbaAtiva={setAbaAtiva} onFrase={registrarFrase} macros={macros} onMacro={(m)=>{ if ((m.passos||[]).some(p=>p.tipo==='logica')) setSeqRun(m); else setMacroConfirm(m); }} canManage={canManage} onGerenciar={()=>{ setGerenciarAba("biblioteca"); setGerenciar(true); }} isMobile={isMobile} colapsavel={false}/>;

  const painelLog = (
    <div style={{height:'100%',display:'flex',flexDirection:'column',minHeight:0}}>
      {/* composer */}
      <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderTop:'3px solid var(--accent)',borderRadius:18,padding:'14px 16px',marginBottom:12,boxShadow:'var(--shadow-sm)',flexShrink:0}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-end',gap:12,flexWrap:'wrap',marginBottom:10}}>
          <div>
            <div style={{fontFamily:'var(--fm)',fontSize:9,letterSpacing:1,textTransform:'uppercase',color:'var(--accent)',marginBottom:3}}>{lang==='pt'?'autor':'author'}</div>
            <div style={{display:'flex',alignItems:'center',gap:8}}>
              {autorSel ? (
                <span onClick={ehContaAdmin ? ()=>setHandover(true) : undefined} title={ehContaAdmin ? 'trocar operador · liberar o desk' : 'suas entradas saem no seu nome'} style={{display:'inline-flex',alignItems:'center',gap:8,cursor:ehContaAdmin?'pointer':'default',borderRadius:9,padding:'2px 6px 2px 2px',border:'1px solid transparent'}} onMouseEnter={e=>{ if(ehContaAdmin) e.currentTarget.style.border='1px solid var(--line-soft)'; }} onMouseLeave={e=>{ e.currentTarget.style.border='1px solid transparent'; }}>
                  <LogAvatar foto={autorSel.foto_url} initials={autorSel.initials} nome={autorSel.nome} size={26}/>
                  <span style={{fontFamily:'var(--fm)',fontSize:13,color:'var(--ink)',fontWeight:500}}>{autorSel.nome}</span>
                  {ehContaAdmin && <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" strokeWidth="2.4" strokeLinecap="round"><path d="M6 9l6 6 6-6"/></svg>}
                </span>
              ) : (
                <button onClick={()=>setHandover(true)} style={{display:'inline-flex',alignItems:'center',gap:8,background:'var(--accent)',color:'#fff',border:'none',borderRadius:9,padding:'6px 14px',cursor:'pointer',fontFamily:'var(--fm)',fontSize:12,fontWeight:600}}>
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3.2-5.5 7-5.5s7 2 7 5.5"/></svg>
                  entrar de serviço
                </button>
              )}
              <button onClick={()=>setBuscaOpen(true)} title={lang==='pt'?'buscar no log inteiro':'search the whole log'} style={{background:'var(--cream-2)',color:'var(--muted)',border:'1px solid var(--line-soft)',borderRadius:7,width:28,height:28,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
              </button>
              <button onClick={()=>setFiltroAberto(true)} title={filtroAtivo ? `filtros do log — mostrando ${listaView.length} de ${lista.length}` : 'filtros do log'} style={{position:'relative',background:filtroAtivo?'var(--accent)':'var(--cream-2)',color:filtroAtivo?'#fff':'var(--muted)',border:'1px solid '+(filtroAtivo?'var(--accent)':'var(--line-soft)'),borderRadius:7,width:28,height:28,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M3 5h18l-7 8v6l-4 2v-8z"/></svg>
                {filtroAtivo && <span style={{position:'absolute',top:-3,right:-3,width:9,height:9,borderRadius:'50%',background:'var(--sun)',border:'2px solid var(--surface)'}}/>}
              </button>
              {fila > 0 && <span title={lang==='pt'?'registros guardados neste aparelho — reenvio automático quando a rede voltar':'entries queued on this device — auto retry when the network is back'} style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--sun)',background:'rgba(198,138,31,0.1)',border:'1px solid var(--sun)',borderRadius:999,padding:'3px 9px',display:'inline-flex',alignItems:'center',gap:4,flexShrink:0,whiteSpace:'nowrap'}}>⏳ {fila}</span>}
            </div>
          </div>
          <div style={{display:'flex',alignItems:'center',gap:6}}>
            <LogHoraField value={horaManual} onChange={setHoraManual} placeholder="agora" allowClear/>
            <label style={{cursor:'pointer',margin:0,background:'var(--accent)',borderRadius:7,width:28,height:28,display:'grid',placeItems:'center',flexShrink:0}} title={lang==='pt'?'anexar foto':'attach photo'}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
              <input type="file" accept="image/*" style={{display:'none'}} onChange={e=>{ upFoto(e.target.files[0]); e.target.value=''; }}/>
            </label>
            <button onClick={registrar} disabled={enviando} style={{..._logBtnPri, opacity: enviando ? 0.6 : 1}}>{enviando ? '…' : (lang==='pt'?'Registrar':'Log')}</button>
          </div>
        </div>
        <textarea className="input" value={texto} disabled={semOperador} onChange={e=>setTexto(e.target.value)} placeholder={semOperador ? (lang==='pt'?'Entre de serviço pra registrar no log…':'Sign on watch to log…') : (lang==='pt'?'Registrar no log…':'Log an entry…')} onClick={()=>{ if (semOperador) setHandover(true); }} style={{width:'100%',boxSizing:'border-box',minHeight:52,resize:'vertical',fontFamily:'var(--f)',fontSize:13.5,opacity:semOperador?0.6:1,cursor:semOperador?'pointer':'text'}}/>
        {foto && <div style={{marginTop:8,display:'flex',alignItems:'center',gap:8}}><img src={foto} style={{width:44,height:44,borderRadius:8,objectFit:'cover'}}/><button className="btn btn-sm" onClick={()=>setFoto(null)}>{lang==='pt'?'remover foto':'remove photo'}</button></div>}
      </div>

      {/* o log do dia — rola dentro do widget (composer fica fixo) */}
      <div style={{flex:1,minHeight:0,overflowY:'auto'}}>
      {entradas === null ? <div style={{fontFamily:'var(--fm)',fontSize:13,color:'var(--muted)',padding:20}}>{lang==='pt'?'Carregando…':'Loading…'}</div> :
       lista.length === 0 ? (
        <div style={{textAlign:'center',padding:'50px 20px',background:'var(--surface)',border:'1px dashed var(--line)',borderRadius:14}}>
          <div style={{fontSize:30,marginBottom:8}}>🚢</div>
          <div style={{fontFamily:'var(--fs)',fontSize:18}}>Log em branco {ehHoje?'hoje':'neste dia'}</div>
          <div style={{fontFamily:'var(--fm)',fontSize:12,color:'var(--muted)',marginTop:4}}>Registre acima ou toque numa frase rápida.</div>
        </div>
      ) : (
        <div style={{border:'1px solid var(--cream-3)',borderRadius:10,overflow:'hidden'}}>
          <div style={isMobile ? {maxHeight:'58vh',overflowY:'auto',WebkitOverflowScrolling:'touch'} : undefined}>
            <table style={{width:'100%',borderCollapse:'collapse',tableLayout:'fixed'}}>
              <thead>
                <tr style={{position:'sticky',top:0,zIndex:1}}>
                  <th style={{..._thLog,width:isMobile?42:52,background:'var(--cream-2)'}}>{lang==='pt'?'Hora':'Time'}</th>
                  <th style={{..._thLog,textAlign:'left',background:'var(--cream-2)'}}>Registro</th>
                  <th style={{..._thLog,width:isMobile?64:110,background:'var(--cream-2)'}}>{lang==='pt'?'Autor':'Author'}</th>
                  {canManage && <th style={{..._thLog,width:52,borderRight:'none',background:'var(--cream-2)'}}></th>}
                </tr>
              </thead>
              <tbody>
                {listaView.map((e) => {
                  const cor = _logCorHex(e.cor);
                  const canc = e.cancelada;
                  return (
                    <tr key={e.id} style={{borderTop:'1px solid var(--cream-3)',opacity:canc?0.55:1}}>
                      <td style={{..._tdLog,fontFamily:'var(--fm)',fontWeight:600,textAlign:'center',whiteSpace:'nowrap',borderLeft:`3px solid ${canc?'var(--muted)':(cor||'transparent')}`}}>{_logFmtHora(e.ts)}</td>
                      <td style={{..._tdLog,textAlign:'left'}}>
                        <span style={{fontFamily:'var(--f)',whiteSpace:'pre-wrap',wordBreak:'break-word',textDecoration:canc?'line-through':'none'}}>{e.texto}</span>
                        {e.foto_url && !canc && <button onClick={()=>window.open(e.foto_url,'_blank')} title="ver anexo" style={{marginLeft:8,background:'var(--cream-2)',border:'1px solid var(--line-soft)',borderRadius:6,cursor:'pointer',fontSize:12,padding:'0px 6px',verticalAlign:'middle',lineHeight:1.6}}>📎</button>}
                        {e.editada && !canc && <button onClick={()=>setHistEntrada(e)} title="ver histórico de edição" style={{marginLeft:8,background:'transparent',border:'none',cursor:'pointer',fontFamily:'var(--fm)',fontSize:10,color:'var(--sky)',verticalAlign:'middle'}}>✎ editada</button>}
                        {canc && <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--rose)',marginTop:2}}>cancelada{e.cancelada_por?` por ${e.cancelada_por}`:''}{e.cancelada_motivo?` · ${e.cancelada_motivo}`:''} <button onClick={()=>setHistEntrada(e)} style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:10,textDecoration:'underline'}}>{lang==='pt'?'histórico':'history'}</button></div>}
                      </td>
                      <td style={{..._tdLog,fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',whiteSpace:'nowrap',maxWidth:96,overflow:'hidden',textOverflow:'ellipsis'}} title={e.autor_nome||''}>{e.autor_nome||'—'}</td>
                      {canManage && <td style={{..._tdLog,borderRight:'none',whiteSpace:'nowrap',textAlign:'center',width:64}}>
                        {!canc && <>
                          <button onClick={()=>setEditar(e)} title="editar" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--muted)',fontSize:12}}>✎</button>
                          <button onClick={()=>cancelarEntrada(e)} title="cancelar (risca, não apaga)" style={{background:'transparent',border:'none',cursor:'pointer',color:'var(--rose)',fontSize:12,marginLeft:4}}>✕</button>
                        </>}
                      </td>}
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}
      </div>
    </div>
  );

  return (
    <main style={{width:'100%',boxSizing:'border-box',maxWidth:1440,margin:'0 auto',padding:isMobile?'16px 14px 80px':'26px 30px 90px'}}>
      {/* header */}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:12,flexWrap:'wrap',marginBottom:14}}>
        <div>
          <div style={{fontFamily:'var(--fs)',fontSize:isMobile?24:30,letterSpacing:'-0.5px'}}>{lang==='pt'?<>Logbook <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'Operacional':'Operational'}</em></>:<>Operational <em style={{fontStyle:'italic',color:'var(--accent)'}}>Logbook</em></>}</div>
          <div style={{display:'flex',alignItems:'center',gap:8,marginTop:3,flexWrap:'wrap'}}>
            <span style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>{lang==='pt'?'Console de turno · registro do dia':'Watch console · daily log'}</span>
            {turnoAtual && <span style={{display:'inline-flex',alignItems:'center',gap:5,fontFamily:'var(--fm)',fontSize:10.5,color:'var(--leaf)'}}><span style={{width:7,height:7,borderRadius:'50%',background:'var(--leaf)'}}/>{turnoAtual.operador_nome}</span>}
          </div>
        </div>
        <div style={{display:'flex',gap:8,flexWrap:'wrap',alignItems:'center'}}>
          <LogDPStatusBadge nivel={(dpCfg && dpCfg.dp_status) || 'cinza'} niveis={_DP_IMCA} fallback="cinza" titulo={lang==='pt'?'status DP (semáforo IMCA)':'DP status (IMCA)'} canManage={podeRegistrar && !semOperador} onMudar={(n)=>mudarDpChave('dp_status', n, _DP_IMCA, lang==='pt'?'DP Status':'DP status')}/>
          <LogDPStatusBadge nivel={(dpCfg && dpCfg.dp_modo) || 'fora'} canManage={podeRegistrar && !semOperador} onMudar={(n)=>mudarDpChave('dp_modo', n, _DP_NIVEIS, lang==='pt'?'Modo DP':'DP mode')}/>
          <button onClick={()=>setAlertasOpen(true)} title={lang==='pt'?'alertas do serviço (notas compartilhadas do passadiço)':'service notes (shared bridge notes)'}
            style={{position:'relative',background:temAlertas?'var(--sun)':'var(--cream-3)',border:'none',borderRadius:9,width:34,height:34,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={temAlertas?'#fff':'var(--muted)'} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.3 3.9L1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
            {temAlertas && <span style={{position:'absolute',top:-3,right:-3,width:10,height:10,borderRadius:'50%',background:'var(--rose)',border:'2px solid var(--cream)'}}/>}
          </button>
          <button onClick={()=>{ if (editMode) { const c = { ...layoutRef.current, widgets: layoutRef.current.widgets.map(w=>({...w})) }; _gridCompactarTudo(c.widgets); setLayoutCfg(c); persistirLayout(c); toast.push({icon:'check', title:'Painel salvo'}); } setEditMode(v=>!v); }} title={editMode?'concluir edição do painel':'personalizar painel (arraste, redimensione, oculte)'} style={{background:'var(--accent)',border:'none',borderRadius:9,width:34,height:34,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
            {editMode
              ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12.5l5 5L20 6.5"/></svg>
              : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>}
          </button>
          <button onClick={()=>setNoturno(v=>!v)} title={noturno?'voltar ao modo dia':'modo noturno (preserva a visão no passadiço)'} style={{background:'var(--accent)',border:'none',borderRadius:9,width:34,height:34,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
            {noturno
              ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M4.9 19.1l1.8-1.8M17.3 6.7l1.8-1.8"/></svg>
              : <svg width="15" height="15" viewBox="0 0 24 24" fill="#fff"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>}
          </button>
          <button onClick={gerarPavao} title="relatório do dia (PDF)" style={{background:'var(--accent)',border:'none',borderRadius:9,width:34,height:34,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6"/></svg>
          </button>
          {podeDpr && (
            <button onClick={()=>setDprOpen(true)} title="DPR — Daily Progress Report (cliente)" style={{background:'#16324A',border:'none',borderRadius:9,width:34,height:34,cursor:'pointer',display:'grid',placeItems:'center',flexShrink:0}}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v18h18"/><path d="M7 14l4-4 3 3 5-6"/></svg>
            </button>
          )}
          {!isMobile && (
            <>
              <button onClick={abrirCalendario} title="toque pra escolher o dia" style={{fontFamily:'var(--fm)',fontSize:11.5,color:'var(--ink)',background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:9,height:34,padding:'0 13px',cursor:'pointer',display:'inline-flex',alignItems:'center',gap:7,boxSizing:'border-box',flexShrink:0}}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
                {data.slice(8,10)}/{data.slice(5,7)}/{data.slice(0,4)}
              </button>
              <input ref={dateRef} type="date" value={data} max={_logHojeStr()} onChange={e=>setData(e.target.value)} style={{position:'absolute',width:1,height:1,opacity:0,pointerEvents:'none'}}/>
              {!ehHoje && <button onClick={()=>setData(_logHojeStr())} style={_logBtnPri}>{lang==='pt'?'hoje':'today'}</button>}
              <LogRelogioVivo/>
            </>
          )}
        </div>
      </div>

      {/* navegação de dia — só no mobile (no desktop a data vive no header) */}
      {isMobile && (
        <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:16,flexWrap:'wrap'}}>
          <button className="btn" onClick={abrirCalendario} style={{flex:1,display:'flex',alignItems:'center',justifyContent:'space-between',gap:8,textTransform:'capitalize'}}>
            <span>{_logDataLonga(data)}</span>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
          </button>
          <input ref={dateRef} type="date" value={data} max={_logHojeStr()} onChange={e=>setData(e.target.value)} style={{position:'absolute',width:1,height:1,opacity:0,pointerEvents:'none'}}/>
          {!ehHoje && <button className="btn btn-sm" onClick={()=>setData(_logHojeStr())}>{lang==='pt'?'hoje':'today'}</button>}
        </div>
      )}

      {/* corpo: canvas de widgets com snap fino (12 col × linhas de 8px) */}
      {(() => {
        const COLS = 12, ROW = 8, PAD = 6;
        const W = {
          meteo3: <LogGraficoWidget data={data} token={token} toast={toast} tipo="todos" onAtraso={setMeteoAtrasado}/>,
          frases: painelFrases,
          dp: <LogPainelDP token={token} toast={toast} canManage={canManage} bloqueado={semOperador} onBloqueado={()=>setHandover(true)} onLog={(t, c) => postEntrada({ texto: t, cor: c })}/>,
          asog: <LogAsogWidget token={token} toast={toast} canManage={canManage} podeStatus={podeRegistrar} bloqueado={semOperador} onBloqueado={()=>setHandover(true)} onLog={(t, c) => postEntrada({ texto: t, cor: c })}/>,
          refsens: <LogPainelRef token={token} toast={toast} canManage={canManage} bloqueado={semOperador} onBloqueado={()=>setHandover(true)} onLog={(t, c) => postEntrada({ texto: t, cor: c })}/>,
          log: painelLog,
        };
        const widgets = layoutCfg.widgets;
        const vis = widgets.filter(w => w.on && W[w.id]);
        const ocultos = widgets.filter(w => !w.on && W[w.id]);
        const setLayout = (mut) => { const c = { v: 3, widgets: layoutCfg.widgets.map(w => ({ ...w })) }; mut(c.widgets); setLayoutCfg(c); };

        const calcGuias = (widgets, id) => {
          const a = widgets.find(w => w.id === id); if (!a) return null;
          const hs = new Set(), vs = new Set();
          for (const o of widgets) {
            if (!o.on || o.id === id) continue;
            if (o.y === a.y) hs.add(a.y);
            if (o.y + o.h === a.y + a.h) hs.add(a.y + a.h);
            if (o.y === a.y + a.h) hs.add(a.y + a.h);
            if (o.y + o.h === a.y) hs.add(a.y);
            if (o.x === a.x) vs.add(a.x);
            if (o.x + o.w === a.x + a.w) vs.add(a.x + a.w);
            if (o.x === a.x + a.w) vs.add(a.x + a.w);
            if (o.x + o.w === a.x) vs.add(a.x);
          }
          return (hs.size || vs.size) ? { hs: [...hs], vs: [...vs] } : null;
        };

        const onDragStart = (e, id) => {
          if (!editMode) return;
          e.preventDefault();
          const grid = gridRef.current; if (!grid) return;
          const r0 = grid.getBoundingClientRect();
          const wgt0 = layoutRef.current.widgets.find(x => x.id === id); if (!wgt0) return;
          const colW0 = r0.width / COLS;
          dragInfo.current = { tipo: 'drag', ox: e.clientX - (r0.left + wgt0.x * colW0), oy: e.clientY - (r0.top + wgt0.y * ROW) };
          setDragId(id);
          const mover = (ev) => {
            const rect = grid.getBoundingClientRect();
            const colW = rect.width / COLS;
            const wgt = layoutRef.current.widgets.find(x => x.id === id); if (!wgt) return;
            let nx = Math.round((ev.clientX - rect.left - dragInfo.current.ox) / colW);
            let ny = Math.round((ev.clientY - rect.top - dragInfo.current.oy) / ROW);
            nx = Math.min(COLS - wgt.w, Math.max(0, nx));
            ny = Math.max(0, ny);
            if (nx === wgt.x && ny === wgt.y) return;
            const c = { v: 3, widgets: layoutRef.current.widgets.map(x => ({ ...x })) };
            const alvo = c.widgets.find(x => x.id === id);
            alvo.x = nx; alvo.y = ny;
            _gridArrumar(c.widgets, id);
            setLayoutCfg(c);
            setGuias(calcGuias(c.widgets, id));
          };
          const soltar = () => {
            window.removeEventListener('pointermove', mover);
            window.removeEventListener('pointerup', soltar);
            const c = { v: 3, widgets: layoutRef.current.widgets.map(x => ({ ...x })) };
            _gridCompactarTudo(c.widgets);
            setLayoutCfg(c); persistirLayout(c);
            setDragId(null); setGuias(null); dragInfo.current = null;
          };
          window.addEventListener('pointermove', mover);
          window.addEventListener('pointerup', soltar);
        };

        // Redimensionar arrastando o canto — snap fino (8px / coluna)
        const onResizeStart = (e, id, eixo) => {
          if (!editMode) return;
          e.preventDefault(); e.stopPropagation();
          dragInfo.current = { tipo: 'resize', eixo: eixo || 'xy' };
          setDragId(id);
          const grid = gridRef.current; if (!grid) return;
          const mover = (ev) => {
            const rect = grid.getBoundingClientRect();
            const colW = rect.width / COLS;
            const wgt = layoutRef.current.widgets.find(x => x.id === id); if (!wgt) return;
            const px = ev.clientX - rect.left, py = ev.clientY - rect.top;
            const eixo = (dragInfo.current && dragInfo.current.eixo) || 'xy';
            const minW = (_LOG_WIDGETS[id] || {}).minW || 2;
            let nw = eixo.includes('x') ? Math.round((px - wgt.x * colW) / colW) : wgt.w;
            let nh = eixo.includes('y') ? Math.round((py - wgt.y * ROW) / ROW) : wgt.h;
            nw = Math.min(COLS - wgt.x, Math.max(minW, nw));
            nh = Math.min(240, Math.max(6, nh));
            if (nw === wgt.w && nh === wgt.h) return;
            const c = { v: 3, widgets: layoutRef.current.widgets.map(x => ({ ...x })) };
            const alvo = c.widgets.find(x => x.id === id);
            alvo.w = nw; alvo.h = nh;
            _gridArrumar(c.widgets, id);
            setLayoutCfg(c);
            setGuias(calcGuias(c.widgets, id));
          };
          const soltar = () => {
            window.removeEventListener('pointermove', mover);
            window.removeEventListener('pointerup', soltar);
            const c = { v: 3, widgets: layoutRef.current.widgets.map(x => ({ ...x })) };
            _gridCompactarTudo(c.widgets);
            setLayoutCfg(c); persistirLayout(c);
            setDragId(null); setGuias(null); dragInfo.current = null;
          };
          window.addEventListener('pointermove', mover);
          window.addEventListener('pointerup', soltar);
        };

        const ocultar = (id) => setLayout(ws => { const wgt = ws.find(x => x.id === id); if (wgt && !(_LOG_WIDGETS[id] || {}).fixo) wgt.on = false; _gridCompactarTudo(ws); });
        const adicionar = (id) => setLayout(ws => {
          const wgt = ws.find(x => x.id === id); if (!wgt) return;
          wgt.on = true; wgt.x = 0; wgt.y = Math.max(0, ...ws.filter(x => x.on).map(x => x.y + x.h));
          _gridCompactarTudo(ws);
        });
        const moverMobile = (id, d) => setLayout(ws => {
          const ord = ws.filter(x => x.on).sort((a, b) => (a.y - b.y) || (a.x - b.x));
          const i = ord.findIndex(x => x.id === id); const j = i + d;
          if (i < 0 || j < 0 || j >= ord.length) return;
          const a = ord[i], b = ord[j];
          [a.x, b.x] = [b.x, a.x]; [a.y, b.y] = [b.y, a.y];
        });

        const overlayEdicao = (wgt) => (
          <div style={{position:'absolute',inset:PAD,zIndex:5,borderRadius:16,border:'2px dashed '+(dragId===wgt.id?'var(--accent)':'var(--line)'),background:'rgba(251,247,240,0.55)',boxSizing:'border-box'}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:6,padding:8}}>
              <div onPointerDown={(e)=>onDragStart(e, wgt.id)} title="arraste pra mover" style={{touchAction:'none',cursor:'grab',background:'var(--ink)',color:'var(--cream)',borderRadius:8,padding:'4px 10px',fontFamily:'var(--fm)',fontSize:10.5,display:'inline-flex',alignItems:'center',gap:6,userSelect:'none',maxWidth:'75%',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>⠿ {(_LOG_WIDGETS[wgt.id]||{}).nome || wgt.id}</div>
              {!(_LOG_WIDGETS[wgt.id]||{}).fixo && <button onClick={()=>ocultar(wgt.id)} title="ocultar" style={{width:24,height:24,borderRadius:'50%',border:'none',background:'var(--rose)',color:'#fff',cursor:'pointer',fontSize:12,flexShrink:0}}>×</button>}
            </div>
            <span style={{position:'absolute',left:8,bottom:8,fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:6,padding:'2px 7px'}}>{wgt.h*ROW}px</span>
            <div onPointerDown={(e)=>onResizeStart(e, wgt.id, 'x')} title="arraste pra mudar só a largura" style={{position:'absolute',right:-3,top:'50%',transform:'translateY(-50%)',width:12,height:44,borderRadius:6,background:'var(--accent)',cursor:'ew-resize',touchAction:'none',userSelect:'none',opacity:0.85}}/>
            <div onPointerDown={(e)=>onResizeStart(e, wgt.id, 'y')} title="arraste pra mudar só a altura" style={{position:'absolute',bottom:-3,left:'50%',transform:'translateX(-50%)',width:44,height:12,borderRadius:6,background:'var(--accent)',cursor:'ns-resize',touchAction:'none',userSelect:'none',opacity:0.85}}/>
            <div onPointerDown={(e)=>onResizeStart(e, wgt.id, 'xy')} title="arraste pra redimensionar" style={{position:'absolute',right:4,bottom:4,width:30,height:30,borderRadius:10,background:'var(--accent)',color:'#fff',display:'grid',placeItems:'center',cursor:'nwse-resize',touchAction:'none',userSelect:'none',fontSize:14}}>⤡</div>
          </div>
        );

        if (isMobile) {
          const ord = vis.slice().sort((a, b) => (a.y - b.y) || (a.x - b.x));
          return (
            <div>
              {editMode && ocultos.length > 0 && (
                <div style={{marginBottom:12,display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'}}>
                  <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>adicionar:</span>
                  {ocultos.map(w => <button key={w.id} className="btn btn-sm" onClick={()=>adicionar(w.id)}>+ {(_LOG_WIDGETS[w.id]||{}).nome}</button>)}
                </div>
              )}
              {ord.map(w => {
                const aberto = !colapsados.has(w.id);
                const avisa = w.id === 'meteo3' && meteoAtrasado;
                return (
                <div key={w.id} style={{marginBottom:12,position:'relative'}}>
                  <div onClick={()=>toggleColapso(w.id)}
                    style={{display:'flex',alignItems:'center',gap:8,padding:'9px 13px',marginBottom:aberto?8:0,cursor:'pointer',userSelect:'none',
                      background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:aberto?'14px 14px 5px 5px':14,boxShadow:'var(--shadow-sm)'}}>
                    <span style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',width:12,flexShrink:0}}>{aberto?'▾':'▸'}</span>
                    <span style={{fontFamily:'var(--fm)',fontSize:11.5,letterSpacing:0.8,textTransform:'uppercase',color:'var(--ink)',fontWeight:600,flex:1,minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{(_LOG_WIDGETS[w.id]||{}).nome}</span>
                    {avisa && !aberto && (
                      <svg width="16" height="16" viewBox="0 0 24 24" style={{flexShrink:0}}><path d="M12 3L2 20h20L12 3z" fill="var(--sun)"/><path d="M12 10v4.5" stroke="#1F1A14" strokeWidth="2.1" strokeLinecap="round"/><circle cx="12" cy="17.4" r="1.1" fill="#1F1A14"/></svg>
                    )}
                    {editMode && (
                      <span style={{display:'flex',gap:5,flexShrink:0}} onClick={e=>e.stopPropagation()}>
                        <button className="btn btn-sm" onClick={()=>moverMobile(w.id,-1)}>▲</button>
                        <button className="btn btn-sm" onClick={()=>moverMobile(w.id,1)}>▼</button>
                        {!(_LOG_WIDGETS[w.id]||{}).fixo && <button className="btn btn-sm" onClick={()=>ocultar(w.id)} style={{color:'var(--rose)'}}>×</button>}
                      </span>
                    )}
                  </div>
                  {aberto && W[w.id]}
                </div>
                );
              })}
            </div>
          );
        }

        const alturaPx = Math.max(ROW * 6, ...vis.map(w => (w.y + w.h) * ROW)) + (editMode ? 180 : 0);
        const ativo = dragId ? vis.find(w => w.id === dragId) : null;
        const emResize = ativo && dragInfo.current && dragInfo.current.tipo === 'resize';
        return (
          <div>
            {editMode && (
              <div style={{marginBottom:12,display:'flex',gap:10,flexWrap:'wrap',alignItems:'center',background:'var(--cream-2)',border:'1px dashed var(--line)',borderRadius:12,padding:'10px 14px'}}>
                <span style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)'}}>✎ arraste pelo <b>⠿</b> · redimensione pelo <b>⤡</b> · × oculta · cards da mesma altura/largura acendem</span>
                {ocultos.length > 0 && <span style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginLeft:6}}>adicionar:</span>}
                {ocultos.map(w => <button key={w.id} className="btn btn-sm" onClick={()=>adicionar(w.id)}>+ {(_LOG_WIDGETS[w.id]||{}).nome}</button>)}
                <button className="btn btn-sm" onClick={()=>{ const c = JSON.parse(JSON.stringify(_LOG_LAYOUT_DEFAULT)); setLayoutCfg(c); persistirLayout(c); }} style={{marginLeft:'auto'}}>{lang==='pt'?'restaurar padrão':'restore default'}</button>
              </div>
            )}
            <div ref={gridRef} style={{position:'relative',height:alturaPx}}>
              {vis.map(w => {
                const igual = emResize && w.id !== dragId && (w.h === ativo.h || w.w === ativo.w);
                return (
                  <div key={w.id} style={{position:'absolute',left:`calc(${w.x} / ${COLS} * 100%)`,width:`calc(${w.w} / ${COLS} * 100%)`,top:w.y*ROW,height:w.h*ROW,padding:PAD,boxSizing:'border-box',transition:dragId?'none':'all .15s ease'}}>
                    <div style={{height:'100%',overflowY:'auto',overflowX:'auto',borderRadius:16,...(editMode?{pointerEvents:'none',opacity:0.85}:{}),...(igual?{outline:'2px dotted var(--sun)',outlineOffset:2}:{})}}>{W[w.id]}</div>
                    {editMode && overlayEdicao(w)}
                  </div>
                );
              })}
              {guias && (
                <div style={{position:'absolute',inset:0,pointerEvents:'none',zIndex:20}}>
                  {guias.hs.map(r => <div key={'h'+r} style={{position:'absolute',left:0,right:0,top:r*ROW,borderTop:'2px dashed var(--accent)'}}/>)}
                  {guias.vs.map(c => <div key={'v'+c} style={{position:'absolute',top:0,bottom:0,borderLeft:'2px dashed var(--accent)',...(c===0?{left:0}:c===COLS?{right:0}:{left:`calc(${c} / ${COLS} * 100%)`})}}/>)}
                </div>
              )}
            </div>
          </div>
        );
      })()}

      {editar && <LogEditModal entrada={editar} token={token} toast={toast} porNome={(autorSel||{}).nome||'—'} onClose={()=>setEditar(null)} onSaved={()=>{ setEditar(null); loadEntradas(); }}/>}
      {histEntrada && <LogHistModal entrada={histEntrada} token={token} onClose={()=>setHistEntrada(null)}/>}
      {handover && <LogDeskModal tripulacao={tripulacao} turnoAtual={turnoAtual} token={token} toast={toast} onClose={()=>setHandover(false)} onSaved={()=>{ setHandover(false); loadTurno(); loadEntradas(); }}/>}
      {filtroAberto && <LogFiltroModal fOrdem={fOrdem} setFOrdem={setFOrdem} fAutor={fAutor} setFAutor={setFAutor} fSoAnexo={fSoAnexo} setFSoAnexo={setFSoAnexo} fSemCanc={fSemCanc} setFSemCanc={setFSemCanc} autores={autoresDoDia} onClose={()=>setFiltroAberto(false)} onLimpar={()=>{ setFOrdem('asc'); setFAutor(''); setFSoAnexo(false); setFSemCanc(false); }}/>}
      {gerenciar && gerenciarAba === 'biblioteca' && <LogFrasesGerenciarModal token={token} toast={toast} abaGer={gerenciarAba} setAbaGer={setGerenciarAba} onClose={()=>setGerenciar(false)} onChanged={loadAbas}/>}
      {gerenciar && gerenciarAba === 'sequencias' && <LogMacrosModal token={token} toast={toast} abaGer={gerenciarAba} setAbaGer={setGerenciarAba} onClose={()=>setGerenciar(false)} onChanged={loadMacros}/>}
      {template && <LogTemplateModal frase={template} onConfirm={(txt)=>{ registrarFraseFinal(template, txt); setTemplate(null); }} onClose={()=>setTemplate(null)}/>}
      {macroConfirm && <LogMacroConfirmModal macro={macroConfirm} onConfirm={(horas)=>{ dispararMacro(macroConfirm, horas); setMacroConfirm(null); }} onClose={()=>setMacroConfirm(null)}/>}
      {seqRun && <LogSeqRunner macro={seqRun} token={token} toast={toast} data={data} postEntrada={postEntrada} onDpModo={(m)=>setDpCfg(c=>({...(c||{}), dp_modo:m}))} onClose={()=>setSeqRun(null)}/>}
      {dprOpen && <LogDprModal data={data} token={token} toast={toast} onGerar={gerarDpr} onClose={()=>setDprOpen(false)}/>}
      {alertasOpen && <LogAlertasModal token={token} toast={toast} onClose={()=>setAlertasOpen(false)} onSalvo={(t)=>setTemAlertas(!!(t && t.trim()))}/>}
      {buscaOpen && <LogBuscaModal token={token} onIrParaDia={(d)=>{ setData(d); setBuscaOpen(false); }} onClose={()=>setBuscaOpen(false)}/>}
      {cancelando && <LogCancelEntradaModal entrada={cancelando} onOk={confirmarCancelamento} onClose={()=>setCancelando(null)}/>}
    </main>
  );
};
