// Tidal — shared UI primitives, hooks, helpers
// Loaded as global. Exposes onto window at end.

const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;

// ─── i18n: bilingual PT (primary) + EN (secondary) ───
const I18N = {
  pt: {
    // nav
    nav_home:'Início', nav_agenda:'Agenda', nav_avisos:'Avisos', nav_solicitacoes:'Solicitações',
    nav_cinema:'Cinema', nav_musica:'Música', nav_saude:'Saúde', nav_coach:'Coach', nav_conquistas:'Conquistas', nav_habilidades:'Habilidades',
    nav_bridge:'Bridge', nav_logbook:'Logbook', nav_gangway:'Gangway', nav_cabines:'Cabines', nav_manifesto:'Manifesto', nav_pessoas:'Pessoas', nav_importar:'Importar', nav_solicitacoes_op:'Painel solicitações', nav_config:'Configurações',
    sec_abordo:'A bordo', sec_conviv:'Convivência', sec_oper:'Operação',
    // common
    search:'Buscar', new_aviso:'Novo aviso', new_request:'Nova solicitação',
    today:'hoje', tomorrow:'amanhã', yesterday:'ontem', now:'agora',
    save:'Salvar', cancel:'Cancelar', confirm:'Confirmar', close:'Fechar', delete:'Remover', edit:'Editar', open:'Abrir',
    add:'Adicionar', export:'Exportar', print:'Imprimir', filter:'Filtrar', clear:'Limpar', loading:'Carregando…',
    yes:'Sim', no:'Não', all:'Todos', none:'Nenhum',
    name:'Nome', company:'Empresa', role:'Função', email:'E-mail', cabin:'Cabine', shift:'Turno',
    status:'Status', date:'Data', time:'Hora', actions:'Ações',
    onboard:'a bordo', pending:'pendente', off:'desembarcado',
    // pages
    page_home_t1:'Boa tarde,', page_home_t2:'{{nome}}.', page_home_sub:'o navio está em paz hoje — mar calmo, vento leve.',
    page_agenda_t1:'Esta', page_agenda_t2:'semana.', page_agenda_sub:'tudo o que está acontecendo a bordo.',
    page_avisos_t1:'Avisos', page_avisos_t2:'da ponte.', page_avisos_sub:'mensagens do comando, hotelaria e cozinha.',
    page_solic_t1:'Solicitações', page_solic_t2:'do dia.', page_solic_sub:'manutenções, ideias, pedidos e propostas de eventos.',
    page_cinema_t1:'Cinema', page_cinema_t2:'do navio.', page_cinema_sub:'o que passa hoje, e o clube do filme.',
    page_musica_t1:'Rádio do', page_musica_t2:'Genesis.', page_musica_sub:'tocando agora, playlists e pedidos.',
    page_saude_t1:'Sua', page_saude_t2:'saúde.', page_saude_sub:'treino, dieta e bem-estar — só você e o Prof. veem.',
    page_conq_t1:'Conquistas', page_conq_t2:'a bordo.', page_conq_sub:'os pequenos rituais que fazem a comunidade.',
    page_habil_t1:'Banco de', page_habil_t2:'habilidades.', page_habil_sub:'quem sabe o quê, e quem precisa de uma mão.',
    page_manif_t1:'Quem está', page_manif_t2:'a bordo.', page_manif_sub:'manifesto completo — uso da gestão.',
    page_gang_t1:'Gangway', page_gang_t2:'do dia.', page_gang_sub:'embarques, desembarques e visitantes.',
    page_cab_t1:'Cabines &', page_cab_t2:'vagas.', page_cab_sub:'arraste alguém da coluna ao lado para qualquer berço.',
    page_pessoas_t1:'Pessoas', page_pessoas_t2:'& equipes.', page_pessoas_sub:'cadastro completo de tripulação, operações e visitantes.',
    page_imp_t1:'Importar', page_imp_t2:'planilha.', page_imp_sub:'envie um Excel com novos embarques — eu cuido do resto.',
    page_cfg_t1:'Configurações', page_cfg_t2:'do sistema.', page_cfg_sub:'backup, segurança e preferências do Portal Genesis.',
    page_solop_t1:'Painel de', page_solop_t2:'solicitações.', page_solop_sub:'kanban de tudo que precisa cuidado.',
    // dashboard
    online_now:'Online agora', bridge:'Ponte de comando', pob_subtitle:'são {n} pessoas dividindo este pedaço de mar com você.',
    embarques_7d:'Embarques · 7 dias', eventos_semana:'Eventos · semana', bem_estar_pulse:'Bem-estar · pulse',
    // gangway
    crew_scan:'Tripulação · scan', visitor_reg:'Visitante · cadastro', visitor_disembark:'Saída de visitante', history_tab:'Histórico',
    // cabines
    occupancy:'ocupação', females_aboard:'mulheres a bordo', mixed:'misto', female_cabin:'feminina', shift_mix:'turnos misturados',
    // saude
    consent_health:'Termos de uso de dados de saúde',
    privacy_note:'Suas métricas são privadas. Apenas você e o Prof. de EF têm acesso. O app não substitui acompanhamento médico.',
    // request types
    req_maintenance:'Manutenção', req_idea:'Ideia / melhoria', req_event:'Proposta de evento', req_supplies:'Compras', req_content:'Conteúdo', req_other:'Outro',
    // status
    s_open:'Aberta', s_triage:'Triagem', s_progress:'Em andamento', s_done:'Resolvida', s_rejected:'Recusada',
  },
  en: {
    nav_home:'Home', nav_agenda:'Agenda', nav_avisos:'Notices', nav_solicitacoes:'Requests',
    nav_cinema:'Cinema', nav_musica:'Music', nav_saude:'Health', nav_coach:'Coach', nav_conquistas:'Streaks', nav_habilidades:'Skills',
    nav_bridge:'Bridge', nav_logbook:'Logbook', nav_gangway:'Gangway', nav_cabines:'Cabins', nav_manifesto:'Manifest', nav_pessoas:'People', nav_importar:'Import', nav_solicitacoes_op:'Requests board', nav_config:'Settings',
    sec_abordo:'On board', sec_conviv:'Together', sec_oper:'Operations',
    search:'Search', new_aviso:'New notice', new_request:'New request',
    today:'today', tomorrow:'tomorrow', yesterday:'yesterday', now:'now',
    save:'Save', cancel:'Cancel', confirm:'Confirm', close:'Close', delete:'Remove', edit:'Edit', open:'Open',
    add:'Add', export:'Export', print:'Print', filter:'Filter', clear:'Clear', loading:'Loading…',
    yes:'Yes', no:'No', all:'All', none:'None',
    name:'Name', company:'Company', role:'Role', email:'Email', cabin:'Cabin', shift:'Shift',
    status:'Status', date:'Date', time:'Time', actions:'Actions',
    onboard:'on board', pending:'pending', off:'disembarked',
    page_home_t1:'Good afternoon,', page_home_t2:'{{nome}}.', page_home_sub:'the ship is at peace — calm sea, light wind.',
    page_agenda_t1:'This', page_agenda_t2:'week.', page_agenda_sub:'everything happening on board.',
    page_avisos_t1:'Bridge', page_avisos_t2:'notices.', page_avisos_sub:'messages from command, hotel and galley.',
    page_solic_t1:"Today's", page_solic_t2:'requests.', page_solic_sub:'maintenance, ideas, supplies and event proposals.',
    page_cinema_t1:'Ship', page_cinema_t2:'cinema.', page_cinema_sub:"tonight's session and the film club.",
    page_musica_t1:'Genesis', page_musica_t2:'radio.', page_musica_sub:'playing now, playlists and requests.',
    page_saude_t1:'Your', page_saude_t2:'health.', page_saude_sub:'training, diet & wellness — only you and the coach see this.',
    page_conq_t1:'On-board', page_conq_t2:'streaks.', page_conq_sub:'the small rituals that build community.',
    page_habil_t1:'Skill', page_habil_t2:'bank.', page_habil_sub:'who knows what — and who needs a hand.',
    page_manif_t1:'Who is', page_manif_t2:'aboard.', page_manif_sub:'full manifest — operations only.',
    page_gang_t1:"Today's", page_gang_t2:'gangway.', page_gang_sub:'boarding, disembarking and visitors.',
    page_cab_t1:'Cabins &', page_cab_t2:'beds.', page_cab_sub:'drag someone onto any berth.',
    page_pessoas_t1:'People', page_pessoas_t2:'& teams.', page_pessoas_sub:'full registry of crew, ops and visitors.',
    page_imp_t1:'Import', page_imp_t2:'spreadsheet.', page_imp_sub:'send an Excel with new boardings — I handle the rest.',
    page_solop_t1:'Requests', page_solop_t2:'board.', page_solop_sub:'kanban of everything that needs care.',
    online_now:'Online now', bridge:'Bridge command', pob_subtitle:'{n} people sharing this stretch of sea with you.',
    embarques_7d:'Boardings · 7d', eventos_semana:'Events · week', bem_estar_pulse:'Wellness · pulse',
    crew_scan:'Crew · scan', visitor_reg:'Visitor · check-in', visitor_disembark:'Visitor · check-out', history_tab:'History',
    occupancy:'occupancy', females_aboard:'women aboard', mixed:'mixed', female_cabin:'female', shift_mix:'mixed shifts',
    consent_health:'Health data consent',
    privacy_note:'Your metrics are private. Only you and the coach can see them. This app is not a substitute for medical care.',
    req_maintenance:'Maintenance', req_idea:'Idea / improvement', req_event:'Event proposal', req_supplies:'Supplies', req_content:'Content', req_other:'Other',
    s_open:'Open', s_triage:'Triage', s_progress:'In progress', s_done:'Done', s_rejected:'Rejected',
  }
};
const t = (k, lang='pt', vars) => {
  let s = (I18N[lang]||I18N.pt)[k] || (I18N.pt[k]) || k;
  if (vars) Object.entries(vars).forEach(([kk,vv]) => { s = s.replace(`{${kk}}`, vv); });
  return s;
};

// ─── Lang context + useT hook ───
// Returns a callable: tt('key') resolves; tt.lang exposes current lang.
const LangContext = createContext({lang:'pt'});
const useT = () => {
  const {lang} = useContext(LangContext) || {lang:'pt'};
  const fn = useCallback((k, vars) => t(k, lang, vars), [lang]);
  fn.lang = lang;
  return fn;
};
const LangProvider = ({lang, children}) => (
  <LangContext.Provider value={{lang}}>{children}</LangContext.Provider>
);

// ─── Role-based permissions ───
// Roles: 'admin' (operação/comando), 'crew' (tripulação), 'guest' (visitante)
const can = {
  post:    r => r === 'admin',
  manage:  r => r === 'admin',
  seeOps:  r => r === 'admin',
  seeManifest: r => r === 'admin',
  edit:    r => r === 'admin',
  approve: r => r === 'admin',
};
const canPost = r => can.post(r);

// Quick translation merge — extend I18N without rewriting it whole
const __extra = {
  pt: {
    show_expired:'mostrar expirados', new_notice:'novo aviso', expired:'expirado', expires:'expira em',
    no_notices:'nenhum aviso por aqui.', notice_posted:'aviso publicado.', category:'categoria',
    from:'de', role_label:'função/depto', message:'mensagem', msg_placeholder:'escreva o aviso aqui…',
    expires_optional:'expira em (opcional)', cancel:'cancelar', post:'publicar',
    print:'imprimir', list_view:'lista', card_view:'cartões', search:'buscar',
    on_board:'a bordo', pending:'pendente', cab:'cab.',
  },
  en: {
    show_expired:'show expired', new_notice:'new notice', expired:'expired', expires:'expires',
    no_notices:'no notices here.', notice_posted:'notice posted.', category:'category',
    from:'from', role_label:'role/dept', message:'message', msg_placeholder:'write the notice here…',
    expires_optional:'expires on (optional)', cancel:'cancel', post:'post',
    print:'print', list_view:'list', card_view:'cards', search:'search',
    on_board:'on board', pending:'pending', cab:'cab.',
  }
};
Object.assign(I18N.pt, __extra.pt);
Object.assign(I18N.en, __extra.en);

// ─── Mobile detection ───
const useMobile = () => {
  const [m, setM] = useState(() => typeof window !== 'undefined' && window.innerWidth < 820);
  useEffect(() => {
    const onR = () => setM(window.innerWidth < 820);
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);
  return m;
};

// ─── useSwipeToClose — gesto de arrastar para fechar drawers (mobile) ───
// direction: 'right' (drawer lateral) ou 'down' (bottom sheet)
const useSwipeToClose = (onClose, direction = 'right') => {
  const [offset, setOffset] = useState(0);
  const start = useRef(null);
  const dragging = useRef(false);

  const onTouchStart = (e) => {
    const t = e.touches[0];
    start.current = { x: t.clientX, y: t.clientY };
    dragging.current = false;
  };
  const onTouchMove = (e) => {
    if (!start.current) return;
    const t = e.touches[0];
    const dx = t.clientX - start.current.x;
    const dy = t.clientY - start.current.y;
    if (direction === 'right') {
      // só conta se o movimento horizontal domina e é para a direita
      if (Math.abs(dx) > Math.abs(dy) && dx > 0) {
        dragging.current = true;
        setOffset(dx);
      }
    } else {
      if (Math.abs(dy) > Math.abs(dx) && dy > 0) {
        dragging.current = true;
        setOffset(dy);
      }
    }
  };
  const onTouchEnd = () => {
    if (dragging.current && offset > 90) {
      onClose();
    }
    setOffset(0);
    start.current = null;
    dragging.current = false;
  };

  const handlers = { onTouchStart, onTouchMove, onTouchEnd };
  const style = offset > 0
    ? {
        transform: direction === 'right' ? `translateX(${offset}px)` : `translateY(${offset}px)`,
        transition: 'none',
        opacity: Math.max(1 - offset / 400, 0.5),
      }
    : { transition: 'transform .25s ease, opacity .25s ease' };

  return { handlers, style, swiping: offset > 0 };
};

// ─── icons ───
const Icon = ({n, size=16, stroke=1.7}) => {
  const s = {width:size,height:size,fill:'none',stroke:'currentColor',strokeWidth:stroke,strokeLinecap:'round',strokeLinejoin:'round'};
  const paths = {
    home:    <><path d="M3 11l9-7 9 7v9a2 2 0 0 1-2 2h-4v-6h-6v6H5a2 2 0 0 1-2-2v-9z"/></>,
    calendar:<><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></>,
    bell:    <><path d="M6 8a6 6 0 0 1 12 0c0 4 1.5 5 2 6H4c.5-1 2-2 2-6z"/><path d="M10 19a2 2 0 0 0 4 0"/></>,
    film:    <><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 8h18M3 16h18M8 4v16M16 4v16"/></>,
    music:   <><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></>,
    star:    <><path d="M12 3l2.5 6 6.5.5-5 4.5L17 21l-5-3-5 3 1-7-5-4.5 6.5-.5z"/></>,
    list:    <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
    anchor:  <><circle cx="12" cy="5" r="2"/><path d="M12 7v14M5 14a7 7 0 0 0 14 0M3 14h4M17 14h4"/></>,
    grid:    <><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></>,
    users:   <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></>,
    upload:  <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></>,
    download:<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></>,
    search:  <><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></>,
    plus:    <><path d="M12 5v14M5 12h14"/></>,
    check:   <><path d="M5 13l4 4L19 7"/></>,
    x:       <><path d="M18 6L6 18M6 6l12 12"/></>,
    arrow_r: <><path d="M5 12h14M13 6l6 6-6 6"/></>,
    arrow_l: <><path d="M19 12H5M11 18l-6-6 6-6"/></>,
    chev_d:  <><path d="M6 9l6 6 6-6"/></>,
    chev_r:  <><path d="M9 6l6 6-6 6"/></>,
    cam:     <><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"/></>,
    clock:   <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    flame:   <><path d="M12 2s4 4 4 8a4 4 0 0 1-8 0c0-2 2-3 2-3s-1 4 2 4 0-4 0-4 0-3 0-5z"/></>,
    play:    <><path d="M8 5v14l11-7z"/></>,
    radio:   <><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49M7.76 16.24a6 6 0 0 1 0-8.49M19.07 4.93a10 10 0 0 1 0 14.14M4.93 19.07a10 10 0 0 1 0-14.14"/></>,
    settings:<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 0 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 0 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 0 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 0 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></>,
    undo:    <><path d="M3 7v6h6"/><path d="M3 13a9 9 0 1 0 3-7l-3 3"/></>,
    edit:    <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></>,
    trash:   <><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></>,
    lock:    <><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></>,
    sun:     <><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></>,
    moon:    <><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></>,
    drop:    <><path d="M12 2s7 8 7 13a7 7 0 0 1-14 0c0-5 7-13 7-13z"/></>,
    flag:    <><path d="M4 21V4M4 4h13l-2 4 2 4H4"/></>,
    chat:    <><path d="M21 11.5A8.5 8.5 0 0 1 3.6 16L3 21l5-1A8.5 8.5 0 0 0 21 11.5z"/></>,
    heart:   <><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 1 0-7.78 7.78L12 21l8.84-8.61a5.5 5.5 0 0 0 0-7.78z"/></>,
    health:  <><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></>,
    tools:   <><path d="M14.7 6.3a4 4 0 0 1-5.4 5.4L4 17l3 3 5.3-5.3a4 4 0 0 1 5.4-5.4 4 4 0 0 0-3-3z"/></>,
    qr:      <><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3M21 14v3M14 21v-3M21 21h-3"/></>,
    printer: <><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></>,
    female:  <><circle cx="12" cy="9" r="6"/><path d="M12 15v7M9 19h6"/></>,
    map:     <><path d="M9 3 3 6v15l6-3 6 3 6-3V3l-6 3z"/><path d="M9 3v15M15 6v15"/></>,
    history: <><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5M12 7v5l3 2"/></>,
    bolt:    <><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></>,
    book:    <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V2H6.5A2.5 2.5 0 0 0 4 4.5z"/><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5"/></>,
    fork:    <><path d="M3 2v7c0 1.1.9 2 2 2h4M9 2v7c0 1.1-.9 2-2 2H5M3 2h6M9 11v11M17 14V2c-2 0-3 1-3 3v6c0 1.7 1.3 3 3 3v8"/></>,
    leaf:    <><path d="M11 20A7 7 0 0 1 4 13c0-5 4-9 9-9 5 0 9 4 9 9a7 7 0 0 1-7 7"/><path d="M11 20c0-3.5 2.5-6.5 6-7"/></>,
    "arrow-down": <><path d="M12 5v14M5 12l7 7 7-7"/></>,
    "arrow-up":   <><path d="M12 19V5M5 12l7-7 7 7"/></>,
    "log-out":    <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></>,
    logout:       <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></>,
    menu:         <><path d="M3 12h18M3 6h18M3 18h18"/></>,
    refresh:      <><path d="M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5"/></>,
  };
  return <svg viewBox="0 0 24 24" style={s}>{paths[n]||paths.list}</svg>;
};

// ─── Avatar ───
const Avatar = ({user, size='md', className=''}) => {
  if (!user) return <div className={`av av-${size} muted ${className}`}>?</div>;
  const cls = user.role === 'crew' || user.role === 'staff' ? (user.funcao === 'comandante' ? 'cmd' : 'crew')
    : user.role === 'ops' ? 'ops' : user.role === 'catering' ? 'catering' : user.role === 'terceirizada' ? 'terceirizada' : user.role === 'guest' ? 'guest' : 'crew';
  if (user.foto_url) return <div className={`av av-${size} ${cls} ${className}`} title={user.nome||''} style={{backgroundImage:`url(${user.foto_url})`,backgroundSize:'cover',backgroundPosition:'center',backgroundRepeat:'no-repeat',color:'transparent'}}/>;
  return <div className={`av av-${size} ${cls} ${className}`}>{user.initials || user.nome?.[0]}</div>;
};

// ─── Pill ───
const Pill = ({children, tone='', live=false}) => (
  <span className={`pill ${tone} ${live?'live':''}`}>{live && <span className="dot"/>} {children}</span>
);

// ─── Toast system ───
const ToastContext = createContext(null);
const ToastProvider = ({children}) => {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((toast) => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, {...toast, id}]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), toast.duration || 4500);
    return id;
  }, []);
  const dismiss = useCallback((id) => setToasts(t => t.filter(x => x.id !== id)), []);
  return (
    <ToastContext.Provider value={{push, dismiss}}>
      {children}
      <div style={{
        position:'fixed', bottom:24, right:24, zIndex:9999,
        display:'flex', flexDirection:'column', gap:10, alignItems:'flex-end', pointerEvents:'none'
      }}>
        {toasts.map(toast => (
          <div key={toast.id} style={{
            background:'var(--ink)', color:'var(--cream)',
            padding:'14px 18px', borderRadius:18, minWidth:280, maxWidth:380,
            boxShadow:'0 16px 40px -12px rgba(0,0,0,0.4)',
            display:'flex', alignItems:'center', gap:12,
            animation:'slideUp .3s cubic-bezier(.2,.7,.3,1) both',
            pointerEvents:'auto', cursor:'pointer'
          }} onClick={() => toast.action ? toast.action() : dismiss(toast.id)}>
            {toast.icon && <div style={{width:32,height:32,borderRadius:'50%',background:'var(--accent-soft)',color:'var(--accent)',display:'grid',placeItems:'center',flexShrink:0}}><Icon n={toast.icon} size={16}/></div>}
            <div style={{flex:1, minWidth:0}}>
              {toast.title && <div style={{fontSize:13.5, fontWeight:500, lineHeight:1.3}}>{toast.title}</div>}
              {toast.body && <div style={{fontSize:12, color:'rgba(251,247,240,0.65)', marginTop:2}}>{toast.body}</div>}
            </div>
            {toast.cta && <button onClick={e=>{e.stopPropagation();toast.cta.onClick();dismiss(toast.id)}} style={{background:'var(--accent)',color:'var(--cream)',border:'none',padding:'6px 12px',borderRadius:999,fontSize:11.5,fontWeight:600,cursor:'pointer',fontFamily:'var(--f)'}}>{toast.cta.label}</button>}
          </div>
        ))}
      </div>
    </ToastContext.Provider>
  );
};
const useToast = () => useContext(ToastContext);

// ─── Animated number (ticking) ───
const TickNumber = ({value, duration=600, format=(v)=>Math.round(v)}) => {
  const [display, setDisplay] = useState(value);
  const fromRef = useRef(value);
  const startRef = useRef(performance.now());
  useEffect(() => {
    fromRef.current = display;
    startRef.current = performance.now();
    const target = value;
    let raf;
    const tick = (now) => {
      const t = Math.min(1, (now - startRef.current) / duration);
      const eased = 1 - Math.pow(1-t, 3);
      const v = fromRef.current + (target - fromRef.current) * eased;
      setDisplay(v);
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [value]);
  return <>{format(display)}</>;
};

// ─── Time helpers ───
const fmtRelative = (mins, lang='pt') => {
  if (mins < 1) return lang==='pt' ? 'agora' : 'now';
  if (mins < 60) return (lang==='pt' ? 'há ' : '') + Math.round(mins) + ' min' + (lang==='en' ? ' ago' : '');
  return (lang==='pt' ? 'há ' : '') + Math.round(mins/60) + 'h' + (lang==='en' ? ' ago' : '');
};
const fmtDate = (iso, lang='pt') => {
  const d = new Date(iso); const today = new Date(); today.setHours(0,0,0,0); d.setHours(0,0,0,0);
  const diff = (d - today) / 864e5;
  if (diff === 0) return lang==='pt' ? 'hoje' : 'today';
  if (diff === 1) return lang==='pt' ? 'amanhã' : 'tomorrow';
  if (diff === -1) return lang==='pt' ? 'ontem' : 'yesterday';
  const dowsPT = ['dom','seg','ter','qua','qui','sex','sáb'];
  const dowsEN = ['sun','mon','tue','wed','thu','fri','sat'];
  if (diff > 0 && diff <= 7) return (lang==='pt'?dowsPT:dowsEN)[d.getDay()];
  return d.toLocaleDateString(lang==='pt'?'pt-BR':'en-US', {day:'numeric', month:'short'});
};
const fmtDateLong = (iso, lang='pt') => new Date(iso).toLocaleDateString(lang==='pt'?'pt-BR':'en-US', {weekday:'long', day:'numeric', month:'long'});

// ─── Section header ───
const SectionH = ({eyebrow, title, em, meta, action}) => (
  <div className="section-h">
    <div>
      {eyebrow && <div className="t-eyebrow" style={{marginBottom:6}}>{eyebrow}</div>}
      <h2>{title}{em && <em> {em}</em>}</h2>
    </div>
    <div style={{display:'flex', alignItems:'center', gap:12}}>
      {meta && <div className="section-meta">{meta}</div>}
      {action}
    </div>
  </div>
);

// expose
Object.assign(window, {
  Icon, Avatar, Pill, ToastProvider, useToast, TickNumber, useMobile, useSwipeToClose,
  fmtRelative, fmtDate, fmtDateLong, SectionH, t, I18N,
  LangContext, LangProvider, useT, can, canPost,
});
