<script setup>
/*
 * MelissaLayout — Layout "lockscreen" (Direção B do redesign)
 * --------------------------------------------------
 * Tela-resumo (zen, info passiva) com botão único no canto inferior
 * esquerdo que sobe um workspace por cima (com blur+dim no fundo).
 *
 * Status atual: arquivo único contendo TODA a experiência Melissa
 * (background + resumo + overlays). Em fases futuras será dividido em:
 *   - MelissaLayout.vue (wrapper: bg + dim + ψ + overlays + slot)
 *   - MelissaResumo.vue (conteúdo do resumo: clock, greeting, timeline, cards)
 *
 * Dados ainda em mock. Próxima fase pluga Supabase.
 *
 * Rota: /melissa (com :secao? opcional). User com layout_variant='melissa'
 * em user_settings e' redirecionado da home do role pra ca pelo router.
 */
import { ref, computed, watch, onMounted, onBeforeUnmount, provide, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { useLayout } from '@/layout/composables/layout';
import { applyThemeEngine, surfaces as THEME_SURFACES, presetOptions as THEME_PRESETS } from '@/theme/theme.options';
import { MELISSA_THEME_NAMES, findMelissaTheme } from './melissaThemes';
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence';
import MelissaCronometro from './MelissaCronometro.vue';
import MelissaCard from './MelissaCard.vue';
import MelissaBusca from './MelissaBusca.vue';
import MelissaMenu from './MelissaMenu.vue';
import MelissaSettingsPanel from './MelissaSettingsPanel.vue';
import MelissaHeroClock from './MelissaHeroClock.vue';
import MelissaTimelineHoje from './MelissaTimelineHoje.vue';
import { useMelissaWallpaper } from './composables/useMelissaWallpaper';
import MelissaAgenda from './MelissaAgenda.vue';
import MelissaPacientes from './MelissaPacientes.vue';
import MelissaCompromissos from './MelissaCompromissos.vue';
import MelissaRecorrencias from './MelissaRecorrencias.vue';
import MelissaConversas from './MelissaConversas.vue';
import MelissaTags from './MelissaTags.vue';
import MelissaGrupos from './MelissaGrupos.vue';
import MelissaConfiguracoes from './MelissaConfiguracoes.vue';
import MelissaPerfil from './MelissaPerfil.vue';
import MelissaPaciente from './MelissaPaciente.vue';
import MelissaPlano from './MelissaPlano.vue';
import MelissaNegocio from './MelissaNegocio.vue';
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
import MelissaSeguranca from './MelissaSeguranca.vue';
import MelissaBloqueios from './MelissaBloqueios.vue';
import MelissaAgendador from './MelissaAgendador.vue';
import MelissaAgendaConfig from './MelissaAgendaConfig.vue';
import MelissaPagamento from './MelissaPagamento.vue';
import MelissaPrecificacao from './MelissaPrecificacao.vue';
import MelissaDescontos from './MelissaDescontos.vue';
import MelissaExcecoes from './MelissaExcecoes.vue';
import MelissaConvenios from './MelissaConvenios.vue';
import MelissaCfgWa from './MelissaCfgWa.vue';
import MelissaCfgWaPessoal from './MelissaCfgWaPessoal.vue';
import MelissaCfgWaOficial from './MelissaCfgWaOficial.vue';
import MelissaCfgWaTemplates from './MelissaCfgWaTemplates.vue';
import MelissaCfgConversasTags from './MelissaCfgConversasTags.vue';
import MelissaCfgConversasAutoreply from './MelissaCfgConversasAutoreply.vue';
import MelissaCfgConversasOptouts from './MelissaCfgConversasOptouts.vue';
import MelissaCfgConversasSla from './MelissaCfgConversasSla.vue';
import MelissaCfgConversasBots from './MelissaCfgConversasBots.vue';
import MelissaCfgLembretes from './MelissaCfgLembretes.vue';
import MelissaCfgCreditosWa from './MelissaCfgCreditosWa.vue';
import MelissaCfgSms from './MelissaCfgSms.vue';
import MelissaCfgEmailTemplates from './MelissaCfgEmailTemplates.vue';
import MelissaCfgRecursosExtras from './MelissaCfgRecursosExtras.vue';
import MelissaCfgRecursosExtrasExtrato from './MelissaCfgRecursosExtrasExtrato.vue';
import MelissaCfgAuditoria from './MelissaCfgAuditoria.vue';
// Sidebar global de configs removido — substituido por botao + popover
// (MelissaConfigPopover) dentro de cada pagina de config. Resolveu lag
// de scroll que o overlay sempre visivel causava em mobile.
import MelissaEmbed from './MelissaEmbed.vue';
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue';
import MelissaLinkExterno from './MelissaLinkExterno.vue';
import MelissaNotificacoes from './MelissaNotificacoes.vue';
import MelissaFinanceiro from './MelissaFinanceiro.vue';
import MelissaFinanceiroLancamentos from './MelissaFinanceiroLancamentos.vue';
import MelissaDocumentos from './MelissaDocumentos.vue';
import MelissaDocumentosTemplates from './MelissaDocumentosTemplates.vue';
import MelissaRelatorios from './MelissaRelatorios.vue';
import MelissaMedicos from './MelissaMedicos.vue';
import MelissaEventoPanel from './MelissaEventoPanel.vue';
import { TOQUE_IDS, useMelissaToques } from './composables/useMelissaToques';
import { useMelissaPacientes } from './composables/useMelissaPacientes';
import { useMelissaEventosHoje } from './composables/useMelissaEventos';
import { useMelissaWhatsapp } from './composables/useMelissaWhatsapp';
import { useMelissaAgenda, MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
import { useMelissaDockPins } from './composables/useMelissaDockPins';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro';
import { useTenantStore } from '@/stores/tenantStore';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
import ConversationDrawer from '@/components/conversations/ConversationDrawer.vue';
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
import BloqueioDialog from '@/features/agenda/components/BloqueioDialog.vue';
import AgendaStatusChangeConfirmDialog from '@/features/agenda/components/AgendaStatusChangeConfirmDialog.vue';
// Topbar system actions trazidos do AppTopbar pra Melissa: plan switcher
// (DEV), notificações e ajuda. AppTopbar não monta na rota /melissa
// (fullscreen), então duplicamos os triggers + drawers aqui.
import NotificationDrawer from '@/components/notifications/NotificationDrawer.vue';
import { useNotifications } from '@/composables/useNotifications';
import { useNotificationStore } from '@/stores/notificationStore';
import { useAjuda } from '@/composables/useAjuda';
import { useTopbarPlanMenu } from '@/composables/useTopbarPlanMenu';
import { useTopbarDevMenuExtras } from '@/composables/useTopbarDevMenuExtras';

// Pacientes + eventos do dia.
//
// PERF: quando o usuário entra direto numa seção (`/melissa/agenda`,
// `/melissa/pacientes`...), o resumo fica tapado pelo conteúdo da seção.
// Carregar os dois loaders na hora competiria com as queries da seção
// (que é o que o user efetivamente vai ver). Adiamos os loaders do
// resumo pra rodarem em background via `requestIdleCallback` quando a
// rota inicial já tem seção. Quando o user fecha a seção (volta pro
// resumo), o cache provavelmente já tá quente — se não estiver, o
// loading aparece naturalmente.
//
// CACHE: composables usam stale-while-revalidate via melissaCacheStore.
// Reabertura do Melissa na mesma sessão SPA é instantânea.
// Snapshot da rota no setup pra detectar deep-link com seção já no boot.
// (`route` reativo é declarado mais abaixo, mas só precisamos do params
// inicial aqui — `setup` roda 1× por mount, params do router já estão
// resolvidos nesse ponto.)
const _hasInitialSecao = !!useRoute().params?.secao;
const {
    pacientes: pacientesReais,
    loading: pacientesLoading,
    refetch: refetchPacientes,
    fetchCached: fetchPacientesCached
} = useMelissaPacientes({ autoFetch: !_hasInitialSecao });
const {
    eventos: eventosHojeReais,
    refetch: refetchEventosHoje,
    fetchCached: fetchEventosHojeCached
} = useMelissaEventosHoje({ autoFetch: !_hasInitialSecao });

// Defer manual quando a rota inicial é uma seção: agenda os fetches do
// resumo pra rodarem após a seção montar (idle callback) — fetchCached
// usa stale-while-revalidate, então não tomba o cache. setTimeout 200
// como fallback pra navegadores sem requestIdleCallback (Safari < 16).
if (_hasInitialSecao) {
    const _idleFetch = () => {
        fetchPacientesCached();
        fetchEventosHojeCached();
    };
    if (typeof window !== 'undefined') {
        if (typeof window.requestIdleCallback === 'function') {
            window.requestIdleCallback(_idleFetch, { timeout: 1500 });
        } else {
            setTimeout(_idleFetch, 200);
        }
    }
}

// ───────────────────────────────────────────────────────────────
// Catálogo de cards do resumo (extensível — novos cards entram aqui)
// ───────────────────────────────────────────────────────────────
const CARDS_CATALOG = [
    { id: 'proximo-paciente', label: 'Próximo paciente', icon: 'pi pi-user', implementado: true },
    { id: 'whatsapp', label: 'WhatsApp', icon: 'pi pi-whatsapp', implementado: true },
    { id: 'recebiveis', label: 'Recebíveis hoje', icon: 'pi pi-wallet', implementado: true },
    { id: 'copilot', label: 'Sugestão Copilot', icon: 'pi pi-sparkles', implementado: false },
    { id: 'agenda-amanha', label: 'Agenda de amanhã', icon: 'pi pi-calendar', implementado: false },
    { id: 'aniversariantes', label: 'Aniversariantes', icon: 'pi pi-gift', implementado: false },
    { id: 'faltas-recentes', label: 'Faltas recentes', icon: 'pi pi-exclamation-circle', implementado: false },
    { id: 'tempo-resposta', label: 'Tempo médio de resposta', icon: 'pi pi-stopwatch', implementado: false }
];
const CARD_IDS = new Set(CARDS_CATALOG.map((c) => c.id));

// User config — quais cards estão ativos e em que ordem; modo de visualização
const cardsAtivos = ref(['proximo-paciente', 'whatsapp', 'recebiveis', 'copilot']);
const cardsLayout = ref('linha-unica'); // 'linha-unica' | 'duas-linhas'
const centralOpen = ref(false);

function toggleCardAtivo(id) {
    const idx = cardsAtivos.value.indexOf(id);
    if (idx >= 0) cardsAtivos.value.splice(idx, 1);
    else cardsAtivos.value.push(id);
}

function getCatalogItem(id) {
    return CARDS_CATALOG.find((c) => c.id === id);
}

// ───────────────────────────────────────────────────────────────
// Seções (overlay sobre o resumo — não saem do layout Melissa)
// ───────────────────────────────────────────────────────────────
const SECOES = {
    agenda: { label: 'Agenda', icon: 'pi pi-calendar', descricao: 'Sessões agendadas, recorrências, compromissos especiais.' },
    pacientes: { label: 'Pacientes', icon: 'pi pi-users', descricao: 'Cadastro, grupos, tags, prontuários e histórico.' },
    conversas: { label: 'WhatsApp', icon: 'pi pi-whatsapp', descricao: 'Conversas em andamento, templates, automações e status.' },
    prontuarios: { label: 'Prontuários', icon: 'pi pi-file', descricao: 'Documentos clínicos, modelos e histórico por paciente.' },
    financeiro: { label: 'Financeiro', icon: 'pi pi-wallet', descricao: 'Recebíveis, lançamentos, relatórios e conciliação.' },
    configuracoes: { label: 'Configurações', icon: 'pi pi-cog', descricao: 'Preferências, integrações e equipe.' },
    copilot: { label: 'Copilot', icon: 'pi pi-sparkles', descricao: 'Insights da IA, sugestões e automações inteligentes.' },
    // Sub-itens da categoria "Agenda e Pacientes" do menu
    'cadastros-recebidos': { label: 'Cadastros recebidos', icon: 'pi pi-inbox', descricao: 'Solicitações vindas do agendador online.' },
    recorrencias: { label: 'Recorrências', icon: 'pi pi-sync', descricao: 'Sessões recorrentes e séries semanais.' },
    compromissos: { label: 'Compromissos determinados', icon: 'pi pi-flag', descricao: 'Compromissos especiais com janelas próprias.' },
    grupos: { label: 'Grupos de pacientes', icon: 'pi pi-th-large', descricao: 'Categorize pacientes por grupos.' },
    tags: { label: 'Tags', icon: 'pi pi-tag', descricao: 'Etiquetas livres pra organizar pacientes.' },
    medicos: { label: 'Médicos e referências', icon: 'pi pi-user-edit', descricao: 'Profissionais que encaminham ou recebem pacientes.' },
    // Pagina nativa do prontuario do paciente (MelissaPaciente) — Fase 1 foundation.
    // ID do paciente vem via route.query.id (?id=xxx). Substitui gradualmente o
    // PatientProntuario.vue legado (3593L Dialog) que continua nos 4 callsites
    // ate Fase 8 (wire-up final).
    paciente: { label: 'Paciente', icon: 'pi pi-user', descricao: 'Prontuario completo: visao geral, perfil, sessoes, financeiro, documentos, conversas.' },
    // Pagina unificada do Layout Melissa (Aparencia + Fundo + Relogio + Cronometro)
    aparencia: { label: 'Layout Melissa', icon: 'pi pi-palette', descricao: 'Aparência, plano de fundo, relógio e cronômetro do resumo.' },
    // Pagina nativa do perfil (MelissaPerfil) — saiu do MelissaConfiguracoes
    perfil:    { label: 'Meu Perfil',  icon: 'pi pi-user',        descricao: 'Identidade, contato, bio, redes — gamificacao no aside.' },
    // Pagina nativa do plano (MelissaPlano) — saiu do MelissaConfiguracoes
    plano:     { label: 'Meu Plano',   icon: 'pi pi-credit-card', descricao: 'Assinatura, recursos liberados e historico de mudancas.' },
    // Pagina nativa do negocio (MelissaNegocio) — saiu do MelissaConfiguracoes
    negocio:   { label: 'Meu Negócio', icon: 'pi pi-briefcase',   descricao: 'Identidade, fiscal, endereco, contato, redes — gamificacao no aside.' },
    // Pagina nativa de seguranca (MelissaSeguranca) — saiu do MelissaConfiguracoes
    seguranca: { label: 'Segurança',   icon: 'pi pi-shield',      descricao: 'Trocar senha + boas praticas + estado da sessao.' },
    // Pagina nativa de bloqueios (MelissaBloqueios) — saiu do MelissaConfiguracoes
    bloqueios: { label: 'Bloqueios e Feriados', icon: 'pi pi-ban', descricao: 'Feriados nacionais (auto), municipais e bloqueios manuais.' },
    // Pagina nativa de configs da agenda (MelissaAgendaConfig) — saiu do MelissaConfiguracoes
    'agenda-config': { label: 'Configurações da Agenda', icon: 'pi pi-calendar', descricao: 'Jornada (dias e horários), ritmo das sessões e agendamento online.' },
    // Pagina nativa de formas de pagamento (MelissaPagamento) — saiu do MelissaConfiguracoes
    pagamento: { label: 'Formas de Pagamento', icon: 'pi pi-wallet', descricao: 'Pix, depósito, dinheiro, cartão e convênio.' },
    // Paginas nativas de configuracoes financeiras — saidas do MelissaConfiguracoes
    'cfg-precificacao': { label: 'Precificação', icon: 'pi pi-tag', descricao: 'Valor padrão da sessão e preços por tipo de compromisso.' },
    'cfg-descontos':    { label: 'Descontos por Paciente', icon: 'pi pi-percentage', descricao: 'Descontos recorrentes aplicados automaticamente.' },
    'cfg-excecoes':     { label: 'Exceções Financeiras', icon: 'pi pi-exclamation-triangle', descricao: 'O que cobrar em faltas, cancelamentos e situações excepcionais.' },
    'cfg-convenios':    { label: 'Convênios', icon: 'pi pi-id-card', descricao: 'Cadastre os convênios que você atende e seus valores.' },
    'cfg-wa':                  { label: 'Canal WhatsApp',      icon: 'pi pi-whatsapp',     descricao: 'Escolha o canal de envio: oficial AgenciaPSI ou pessoal.' },
    'cfg-wa-pessoal':          { label: 'WhatsApp Pessoal',    icon: 'pi pi-mobile',       descricao: 'Conecte seu próprio número via QR code.' },
    'cfg-wa-oficial':          { label: 'WhatsApp Oficial',    icon: 'pi pi-verified',     descricao: 'Número provisionado pela AgenciaPSI via API oficial Meta.' },
    'cfg-wa-templates':        { label: 'Templates WhatsApp',  icon: 'pi pi-file-edit',    descricao: 'Personalize os textos enviados ou volte ao padrão.' },
    'cfg-conversas-tags':      { label: 'Tags de Conversa',    icon: 'pi pi-tag',          descricao: 'Etiquetas custom pra classificar threads no CRM.' },
    'cfg-conversas-autoreply': { label: 'Auto-reply WhatsApp', icon: 'pi pi-reply',        descricao: 'Resposta automática quando paciente escreve fora do horário.' },
    'cfg-conversas-optouts':   { label: 'Opt-outs (LGPD)',     icon: 'pi pi-ban',          descricao: 'Números que pediram pra não receber mensagens. LGPD Art. 18.' },
    'cfg-conversas-sla':       { label: 'SLA de resposta',     icon: 'pi pi-stopwatch',    descricao: 'Tempo máximo pra responder. Alerta quando estourar.' },
    'cfg-conversas-bots':      { label: 'Bot de triagem',      icon: 'pi pi-android',      descricao: 'Coleta nome e motivo via WhatsApp antes do humano.' },
    'cfg-lembretes':           { label: 'Lembretes de Sessão', icon: 'pi pi-bell',         descricao: 'WhatsApp automático antes das sessões agendadas.' },
    'cfg-creditos-wa':         { label: 'Créditos WhatsApp',   icon: 'pi pi-credit-card',  descricao: 'Compre pacotes de mensagens, veja saldo e extrato.' },
    'cfg-sms':                 { label: 'SMS',                 icon: 'pi pi-comment',      descricao: 'Backup quando WhatsApp falha. Gerencie créditos SMS.' },
    'cfg-email-templates':     { label: 'Templates de E-mail', icon: 'pi pi-envelope',     descricao: 'Personalize os e-mails enviados aos pacientes.' },
    'cfg-recursos-extras':     { label: 'Recursos Extras',     icon: 'pi pi-box',          descricao: 'Amplie as funcionalidades com recursos adicionais.' },
    'cfg-recursos-extras-extrato': { label: 'Extrato de Recursos Extras', icon: 'pi pi-list', descricao: 'Histórico de débitos e créditos exportável.' },
    'cfg-auditoria':           { label: 'Auditoria',           icon: 'pi pi-shield',       descricao: 'Registro imutável de operações (LGPD Art. 37).' },
    // Pagina nativa de alterar plano (MelissaAlterarPlano) — substitui /therapist/upgrade
    'alterar-plano': { label: 'Alterar Plano', icon: 'pi pi-arrow-up-right', descricao: 'Escolha um plano pessoal pra ativar todos os recursos.' },
    // Onda 1 — pages embedadas via MelissaEmbed (1-coluna, hero glass)
    'financeiro':              { label: 'Financeiro',                   icon: 'pi pi-wallet',          descricao: 'Visão geral, recebíveis e indicadores.' },
    'financeiro-lancamentos':  { label: 'Lançamentos financeiros',      icon: 'pi pi-list',            descricao: 'Cobranças, pagamentos e recebimentos.' },
    'documentos':              { label: 'Documentos',                   icon: 'pi pi-file',            descricao: 'Documentos clínicos do tenant.' },
    'documentos-templates':    { label: 'Templates de documentos',      icon: 'pi pi-file-edit',       descricao: 'Modelos pra prontuários e relatórios.' },
    'agendamentos-recebidos':  { label: 'Agendamentos recebidos',       icon: 'pi pi-inbox',           descricao: 'Solicitações do agendador online.' },
    'online-scheduling':       { label: 'Agendador online',             icon: 'pi pi-calendar-clock',  descricao: 'Configure o link publico, identidade visual, fluxo, pagamento e textos do agendador.' },
    'relatorios':              { label: 'Relatórios',                   icon: 'pi pi-chart-bar',       descricao: 'Indicadores clínicos e financeiros do tenant.' },
    'notificacoes':            { label: 'Notificações',                 icon: 'pi pi-bell',            descricao: 'Histórico de notificações enviadas.' },
    'link-externo':            { label: 'Link externo de cadastro',     icon: 'pi pi-share-alt',       descricao: 'Link público pra pacientes se cadastrarem.' }
};

// Set de keys que renderizam via MelissaEmbed (Onda 1 — pages 1-coluna).
// Todas foram promovidas pra paginas nativas Melissa.
const MELISSA_EMBED_KEYS = [];

// Slugs reservados pra páginas dedicadas (não-config) — agenda, pacientes,
// conversas etc. + as MELISSA_EMBED_KEYS. Usado no isConfigRoute pra
// evitar colisão (ex: /melissa/agenda → MelissaAgenda, não config).
const MELISSA_NON_CONFIG_SLUGS = new Set([
    'agenda', 'pacientes', 'paciente', 'compromissos', 'recorrencias', 'conversas',
    'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
    'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
    'documentos', 'documentos-templates', 'relatorios',
    'perfil', 'plano', 'negocio', 'seguranca', 'bloqueios', 'alterar-plano',
    'online-scheduling', 'agenda-config', 'pagamento',
    'cfg-precificacao', 'cfg-descontos', 'cfg-excecoes', 'cfg-convenios',
    'cfg-wa', 'cfg-wa-pessoal', 'cfg-wa-oficial', 'cfg-wa-templates',
    'cfg-conversas-tags', 'cfg-conversas-autoreply', 'cfg-conversas-optouts',
    'cfg-conversas-sla', 'cfg-conversas-bots',
    'cfg-lembretes', 'cfg-creditos-wa', 'cfg-sms',
    'cfg-email-templates', 'cfg-recursos-extras', 'cfg-recursos-extras-extrato', 'cfg-auditoria',
    ...MELISSA_EMBED_KEYS
]);
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
// Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá.
const MELISSA_CONFIG_ALIASES = new Set([
    'aparencia',
    'fundo', 'relogio', 'cronometro'
]);
function isMelissaConfigRoute(slug) {
    if (!slug) return false;
    const s = String(slug).toLowerCase();
    if (MELISSA_NON_CONFIG_SLUGS.has(s)) return false;
    if (MELISSA_CONFIG_ALIASES.has(s)) return true;
    // Slugs diretos: cfg-precificacao, cfg-agenda, cfg-bloqueios, etc.
    return s.startsWith('cfg-');
}

// Seção ativa = param `:secao?` da rota. URL é a fonte da verdade pra
// permitir back/forward e deep-link (ex: /melissa/agenda abre o
// overlay da agenda direto). Se a chave for inválida, vira null.
const router = useRouter();
const route = useRoute();
const secaoAberta = computed(() => {
    const s = route.params?.secao;
    if (!s) return null;
    // Aceita: keys conhecidas em SECOES (páginas dedicadas + 5 aliases legados)
    if (SECOES[s]) return s;
    // Aceita: rotas de config (fundo/relogio/cronometro inline e qualquer
    // cfg-* embed). Sem isso, /melissa/fundo viraria null e fecharia a
    // página de configs ao clicar nesses itens da sidebar.
    if (isMelissaConfigRoute(s)) return s;
    return null;
});

// (Removido) showConfigSidebar / configAsideLeftStyle — o sidebar
// global de configs foi substituido pelo MelissaConfigPopover
// dentro de cada pagina, ancorado no botao "Configuracoes" no
// topo da .xxx-side. Sem mais CSS var de left dinamico nas pages.

// Quando o usuário fecha a seção e volta pro resumo, garante que os
// dados estão prontos (caso o idle callback ainda não tenha disparado
// no fluxo deep-link). fetchCached é idempotente: cache hit → instant,
// cache miss → fetch real. Sem cache, não dispara nada estranho.
watch(secaoAberta, (newVal, oldVal) => {
    if (oldVal && !newVal) {
        fetchPacientesCached();
        fetchEventosHojeCached();
    }
});

function abrirSecao(key) {
    // Fecha overlays paralelos pra evitar empilhamento
    workspaceOpen.value = false;
    eventoSelecionado.value = null;
    if (key === secaoAberta.value) return; // no-op, evita push duplicado
    router.push({ name: 'Melissa', params: { secao: key } });
}
function fecharSecao() {
    if (!secaoAberta.value) return;
    router.push({ name: 'Melissa', params: {} });
}

// Click "Cores do Tema" no menu principal: fecha qualquer fake dialog
// aberto (perfil/plano/negocio/seguranca/pagamento/agendador/cfg-*) e
// abre o painel Personalizar (cog top-right).
function onMenuOpenSettings() {
    fecharSecao();
    settingsOpen.value = true;
}

// ── Pins dinâmicos do dock (híbrido: 4 fixos + 3 MRU) ──────────
const dockPins = useMelissaDockPins();
const pinContextMenu = ref(null);
const pinContextSlug = ref('');

// Toda vez que a seção muda, registra como "recente" no dock (se não
// for builtin nem pinned). Slugs de configuração (cfg-*) também viram
// recent — útil quando o user fica navegando entre páginas de config.
watch(secaoAberta, (slug) => {
    if (slug) dockPins.pushRecent(slug);
});

function openPinContextMenu(event, slug) {
    event.preventDefault();
    event.stopPropagation();
    pinContextSlug.value = slug;
    pinContextMenu.value?.show(event);
}

const pinContextMenuItems = computed(() => {
    const slug = pinContextSlug.value;
    if (!slug) return [];
    const isPinned = dockPins.isPinned(slug);
    const items = [];
    if (isPinned) {
        items.push({
            label: 'Desafixar',
            icon: 'pi pi-bookmark',
            command: () => dockPins.unpin(slug)
        });
    } else {
        items.push({
            label: 'Fixar no dock',
            icon: 'pi pi-bookmark-fill',
            command: () => {
                const result = dockPins.pin(slug);
                if (!result.ok && result.reason === 'full') {
                    toast.add({
                        severity: 'warn',
                        summary: 'Limite atingido',
                        detail: `Você pode fixar até ${dockPins.MAX_PINNED} atalhos. Desafixe um pra liberar espaço.`,
                        life: 3500
                    });
                }
            }
        });
    }
    items.push({ separator: true });
    items.push({
        label: 'Remover do dock',
        icon: 'pi pi-times',
        command: () => dockPins.remove(slug)
    });
    return items;
});

// Resolve label/ícone a partir do slug pra renderizar no pin.
// Usa o catálogo SECOES (já existente) — fallback genérico se slug
// for de uma rota cfg-* (configurações embeds).
function pinMeta(slug) {
    const fromSecoes = SECOES[slug];
    if (fromSecoes) return { label: fromSecoes.label, icon: fromSecoes.icon };
    if (slug?.startsWith('cfg-')) {
        return { label: 'Configuração', icon: 'pi pi-cog' };
    }
    return { label: slug, icon: 'pi pi-bookmark' };
}

// Prefs de layout/UI (toque, fundo, opacidade, formato hora)
// TODO: migrar pra configs do tenant — hoje só localStorage pra survive refresh
const LAYOUT_STORAGE_KEY = 'melissa.layout.v1';

// ───────────────────────────────────────────────────────────────
// Relógio (atualiza a cada segundo)
// ───────────────────────────────────────────────────────────────
const now = ref(new Date());
let clockTimer = null;

// layoutReady gate: MelissaAgenda/MelissaPacientes têm <Teleport to=".melissa-dock">
// (target vive aqui no MelissaLayout). Vue exige que o target esteja no DOM
// antes do Teleport montar. No deep-link (/melissa/agenda) os dois montam
// no mesmo render pass — race. Atrasar o mount em 1 nextTick garante que
// .melissa-dock já está no DOM. Evita "Cannot read properties of null
// (reading 'insertBefore')" em moveTeleport durante triggers reativos
// (ex: setTheme do PrimeVue) na fase de bootstrap.
const layoutReady = ref(false);

onMounted(() => {
    clockTimer = setInterval(() => (now.value = new Date()), 1000);
    window.addEventListener('keydown', onKeydown);
    nextTick(() => { layoutReady.value = true; });
});
onBeforeUnmount(() => {
    if (clockTimer) clearInterval(clockTimer);
    window.removeEventListener('keydown', onKeydown);
});

const use24h = ref(true);
const horaFormatada = computed(() => {
    const d = now.value;
    if (use24h.value) {
        return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
    }
    let h = d.getHours();
    const ampm = h >= 12 ? 'PM' : 'AM';
    h = h % 12 || 12;
    return `${h}:${String(d.getMinutes()).padStart(2, '0')} ${ampm}`;
});

const dataExtenso = computed(() => {
    return now.value.toLocaleDateString('pt-BR', {
        weekday: 'long',
        day: 'numeric',
        month: 'long'
    });
});

const saudacao = computed(() => {
    const h = now.value.getHours();
    if (h >= 5 && h < 12) return 'Bom dia';
    if (h >= 12 && h < 18) return 'Boa tarde';
    return 'Boa noite';
});

// ───────────────────────────────────────────────────────────────
// Background customizável (composable)
// ───────────────────────────────────────────────────────────────
// Refs (bgUrl/overlayOpacity/bgImageOpacity) sao criados pelo
// composable; persistencia continua aqui no pai (saveLayoutPrefs +
// applyPrefsPayload + watcher) pra agrupar com outras prefs.
const {
    bgUrl,
    overlayOpacity,
    bgImageOpacity,
    defaultBgStyle,
    photoStyle,
    onFileChange,
    clearBg
} = useMelissaWallpaper();

// Nome do tema selecionado em "Personalizar > Temas" (Freud/Klein/Jung).
// E o ID persistido — wallpaper e' resolvido a partir dele no boot
// (sem guardar a data URL gigante no DB). null = wallpaper custom ou padrao.
const themeName = ref(null);
function setThemeName(name) {
    if (name && !MELISSA_THEME_NAMES.has(name)) name = null;
    themeName.value = name || null;
}

// Quando ON, os textos do hero (relogio, saudacao, resumo) ganham um
// fundo solido translucido + borda + padding. Util pra wallpapers
// com pouca transparencia onde o text-shadow nao da legibilidade.
const textBgEnabled = ref(false);
function setTextBgEnabled(v) { textBgEnabled.value = !!v; }

// ───────────────────────────────────────────────────────────────
// Tema (dark/light + cor primária) — usa a infra existente do app
// ───────────────────────────────────────────────────────────────
// `toggleDarkMode` flipa a classe .app-dark + layoutConfig.darkTheme.
// `applyThemeEngine` re-aplica o preset com nova primary/surface.
// `userSettings.queuePatch` persiste no DB (debounced upsert em
// user_settings — colunas theme_mode/primary_color, não toca em melissa_prefs).
const { layoutConfig, toggleDarkMode, isDarkTheme } = useLayout();
const userSettings = useUserSettingsPersistence();
onMounted(() => userSettings.init());

// Paleta enxuta — espelha primaryColors da ProfilePage. 'noir' usa
// currentColor (preto/branco conforme tema) — visualmente vira a primary
// "neutra" pro user que quer monocromático.
const PRIMARY_COLORS = [
    { name: 'noir',     swatch: 'currentColor' },
    { name: 'emerald',  swatch: '#10b981' },
    { name: 'green',    swatch: '#22c55e' },
    { name: 'lime',     swatch: '#84cc16' },
    { name: 'orange',   swatch: '#f97316' },
    { name: 'amber',    swatch: '#f59e0b' },
    { name: 'yellow',   swatch: '#eab308' },
    { name: 'teal',     swatch: '#14b8a6' },
    { name: 'cyan',     swatch: '#06b6d4' },
    { name: 'sky',      swatch: '#0ea5e9' },
    { name: 'blue',     swatch: '#3b82f6' },
    { name: 'indigo',   swatch: '#6366f1' },
    { name: 'violet',   swatch: '#8b5cf6' },
    { name: 'purple',   swatch: '#a855f7' },
    { name: 'fuchsia',  swatch: '#d946ef' },
    { name: 'pink',     swatch: '#ec4899' },
    { name: 'rose',     swatch: '#f43f5e' }
];

function setDark(shouldBeDark) {
    if (isDarkTheme.value === shouldBeDark) return;
    toggleDarkMode();
    userSettings.queuePatch({ theme_mode: shouldBeDark ? 'dark' : 'light' });
}

function setPrimary(name) {
    if (!name || layoutConfig.primary === name) return;
    layoutConfig.primary = name;
    applyThemeEngine(layoutConfig);
    userSettings.queuePatch({ primary_color: name });
}

// Surface = base de fundo das superfícies (cards, dialog body, etc).
// Usa a mesma infra de tema do app — applyThemeEngine + persist em user_settings.
const SURFACES = THEME_SURFACES;
const activeSurface = computed(() => layoutConfig.surface || (isDarkTheme.value ? 'zinc' : 'slate'));
function setSurface(name) {
    if (!name || layoutConfig.surface === name) return;
    layoutConfig.surface = name;
    applyThemeEngine(layoutConfig);
    userSettings.queuePatch({ surface_color: name });
}

// Preset = engine de tokens do PrimeVue. Mantemos so Lara e Nora
// (Aura ficou de fora — decisao de design pra reduzir ruido visual).
const PRESETS = THEME_PRESETS.filter((p) => p === 'Lara' || p === 'Nora');
function setPreset(name) {
    if (!name || layoutConfig.preset === name) return;
    layoutConfig.preset = name;
    applyThemeEngine(layoutConfig);
    userSettings.queuePatch({ preset: name });
}


// ───────────────────────────────────────────────────────────────
// Settings popover (canto inferior direito — vive na .melissa-tray)
// ───────────────────────────────────────────────────────────────
const settingsOpen = ref(false);
const cogBtnEl = ref(null);

// "More" tray popup — visivel so em mobile (<md). Collapse de bell/
// help/cog/plan-DEV num menu vertical pra economizar largura.
const trayMoreOpen = ref(false);
const trayMoreBtnEl = ref(null);

// Fechar ao clicar fora: listener so existe enquanto o popover esta
// aberto. mousedown (capture) fecha antes do click chegar — mas o
// proprio cog precisa ser ignorado, senao fecha aqui e o @click do
// botao re-abre na sequencia.
function onSettingsDocMouseDown(e) {
    if (!settingsOpen.value) return;
    const t = e.target;
    if (!(t instanceof Element)) return;
    if (t.closest('.mp-panel')) return;          // clique dentro do panel
    if (cogBtnEl.value?.contains(t)) return;     // clique no proprio cog
    settingsOpen.value = false;
}
watch(settingsOpen, (open) => {
    if (open) document.addEventListener('mousedown', onSettingsDocMouseDown, true);
    else document.removeEventListener('mousedown', onSettingsDocMouseDown, true);
});
onBeforeUnmount(() => document.removeEventListener('mousedown', onSettingsDocMouseDown, true));

// Mesmo padrao pro popup "More" do tray em mobile: ignora o proprio
// botao trigger (senao fecha + reabre no click) e fecha em qualquer
// outro lugar fora do panel.
function onTrayMoreDocMouseDown(e) {
    if (!trayMoreOpen.value) return;
    const t = e.target;
    if (!(t instanceof Element)) return;
    if (t.closest('.melissa-tray__more-panel')) return;
    if (trayMoreBtnEl.value?.contains(t)) return;
    trayMoreOpen.value = false;
}
watch(trayMoreOpen, (open) => {
    if (open) document.addEventListener('mousedown', onTrayMoreDocMouseDown, true);
    else document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true);
});
onBeforeUnmount(() => document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true));

// ───────────────────────────────────────────────────────────────
// Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora"
// ───────────────────────────────────────────────────────────────
// Tudo (HORA_INICIO/FIM, hoursRange, eventosVisiveis, isFolga, todayFeriado,
// tlHScrollEl, tlEcoState, etc.) migrou pro componente MelissaTimelineHoje.
// Pai so passa eventos brutos + workRules/settings/feriados via props,
// e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo).

// Contagens por tipo + frase resumo do dia. Pra sessao tambem quebro
// por status (cancelado/remarcado) pra montar o sufixo "(x foi cancelado,
// x foi remarcado)" depois do chip de atendimentos.
const contagensDia = computed(() => {
    const c = { sessao: 0, supervisao: 0, reuniao: 0, sessaoCancelada: 0, sessaoRemarcada: 0 };
    for (const ev of eventosHojeReais.value) {
        c[ev.tipo] = (c[ev.tipo] || 0) + 1;
        if (ev.tipo === 'sessao') {
            const s = String(ev.status || '').toLowerCase();
            if (s === 'cancelado' || s === 'cancelada') c.sessaoCancelada += 1;
            else if (s === 'remarcado') c.sessaoRemarcada += 1;
        }
    }
    return c;
});

function pluralizar(n, singular, plural) {
    return `${n} ${n === 1 ? singular : plural}`;
}

// Sufixo "(1 foi cancelado, 2 foram remarcados)" depois do chip de
// atendimentos quando houver sessoes cancel/remarcado no dia.
function _statusSuffix(qtdCancel, qtdRemarc) {
    const partes = [];
    if (qtdCancel > 0) {
        partes.push(qtdCancel === 1
            ? `${qtdCancel} foi cancelado`
            : `${qtdCancel} foram cancelados`);
    }
    if (qtdRemarc > 0) {
        partes.push(qtdRemarc === 1
            ? `${qtdRemarc} foi remarcado`
            : `${qtdRemarc} foram remarcados`);
    }
    return partes.length ? ` (${partes.join(', ')})` : '';
}

// Partes estruturadas pro template renderizar cada contagem como link clicável
const resumoPartes = computed(() => {
    const c = contagensDia.value;
    const partes = [];
    if (c.sessao > 0) {
        partes.push({
            tipo: 'sessao',
            text: pluralizar(c.sessao, 'atendimento', 'atendimentos'),
            suffix: _statusSuffix(c.sessaoCancelada, c.sessaoRemarcada)
        });
    }
    if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') });
    if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') });
    return partes;
});

// Evento selecionado (dialog de detalhes)
const eventoSelecionado = ref(null);
const eventoBusy = ref(false); // bloqueia botões enquanto UPDATE roda
const melissaAgendaRef = ref(null); // pra chamar openProntuario + setView
const toast = useToast();
const confirm = useConfirm();
const tenantStore = useTenantStore();
const { gerarCobrancaManual } = useAgendaFinanceiro();
const conversationDrawerStore = useConversationDrawerStore();

// ───────────────────────────────────────────────────────────────
// Agenda completa (CRUD + recorrência via AgendaEventDialog)
// ───────────────────────────────────────────────────────────────
// Composable injetado em MelissaAgenda via provide/inject — orquestra
// useAgendaEvents + useRecurrence + useDeterminedCommitments + useFeriados
// e expõe dialog state + handlers (save/delete/série/persistMoveOrResize).
// MelissaAgenda lê M.eventos pra alimentar o FullCalendar e mutará
// M.viewStart/M.viewEnd via FC.datesSet.
const M = useMelissaAgenda();
provide(MELISSA_AGENDA_KEY, M);

// Destrutura refs/computeds pro template auto-unwrappar (refs em objetos
// aninhados NÃO unwrappam no template — só os top-level do setup).
const {
    dialogOpen: agendaDialogOpen,
    dialogEventRow: agendaDialogEventRow,
    dialogStartISO: agendaDialogStartISO,
    dialogEndISO: agendaDialogEndISO,
    dialogBlockOverlap: agendaDialogBlockOverlap,
    occDialogOpen: agendaOccDialogOpen,
    occDialogEventRow: agendaOccDialogEventRow,
    occDialogStartISO: agendaOccDialogStartISO,
    occDialogEndISO: agendaOccDialogEndISO,
    serieRefreshTick: agendaSerieRefreshTick,
    ownerId: agendaOwnerId,
    clinicTenantId: agendaClinicTenantId,
    commitmentOptions: agendaCommitmentOptions,
    workRules: agendaWorkRules,
    settings: agendaSettings,
    allEventsForDialog: agendaAllEvents,
    feriados: agendaFeriados,
    bloqueioDialogOpen: agendaBloqueioOpen,
    bloqueioMode: agendaBloqueioMode,
    // Status change confirm dialog (Fase 5, 2026-05-14)
    statusDialogOpen: agendaStatusDialogOpen,
    statusDialogProps: agendaStatusDialogProps,
    onStatusDialogConfirm: agendaOnStatusDialogConfirm,
    onStatusDialogCancel: agendaOnStatusDialogCancel
} = M;

function abrirEvento(ev) {
    eventoSelecionado.value = ev;
}
function fecharEvento() {
    eventoSelecionado.value = null;
    eventoBusy.value = false;
}

// Mantém eventoSelecionado em sincronia com a lista reativa M.eventos.
// Sem isso, eventoSelecionado.value é snapshot do clique e não acompanha
// updates do _paymentStateMap pós refetch (caso típico: revogar/antecipar
// pagamento — record muda mas popover continuava mostrando estado antigo).
// Lookup em 2 etapas:
//  1) match por id (caso comum — evento real persistente)
//  2) match por recurrence_id+recurrence_date (caso virtual → materializada:
//     id muda de `rec::rule::date` pra uuid real após antecipar/Usar/etc).
// Sem o 2o lookup, popover ficava preso na versão virtual após o evento
// virar real — exemplo: revoguei antecipação, "Usar" não aparecia porque
// ev ainda era a versão virtual stale.
watch(() => M.eventos.value, (novos) => {
    const sel = eventoSelecionado.value;
    if (!sel?.id) return;
    let fresh = novos.find((e) => e.id === sel.id);
    if (!fresh && sel.recurrence_id && sel.recurrence_date) {
        fresh = novos.find((e) =>
            !e.is_occurrence &&
            e.recurrence_id === sel.recurrence_id &&
            e.recurrence_date === sel.recurrence_date
        );
    }
    if (fresh && fresh !== sel) {
        eventoSelecionado.value = fresh;
    }
}, { flush: 'post' });

// ── Actions do MelissaEventoPanel ──────────────────────────────
// Fase 5 (2026-05-14): TODOS os status (realizado/faltou/cancelado/etc)
// passam por M.onUpdateSeriesEvent — que abre o AgendaStatusChangeConfirmDialog
// quando há regra de exceção, pacote saldo ou pending record. Antes, eventos
// reais faziam UPDATE direto sem passar pelo dialog (gap reportado pelo user).
async function updateEventoStatus(novoStatus, msgSucesso) {
    const ev = eventoSelecionado.value;
    if (!ev?.id || eventoBusy.value) return;
    eventoBusy.value = true;
    try {
        const isVirtual =
            !!ev.is_occurrence ||
            (typeof ev.id === 'string' && ev.id.startsWith('rec::'));

        await M.onUpdateSeriesEvent({
            id: isVirtual ? null : ev.id,
            status: novoStatus,
            recurrence_date:
                ev.recurrence_date ||
                ev.original_date ||
                String(ev.inicio_em || '').slice(0, 10),
            inicio_em: ev.inicio_em,
            fim_em: ev.fim_em,
            is_virtual: isVirtual,
            row: ev
        });
        toast.add({ severity: 'success', summary: msgSucesso, life: 2200 });
        // Refetch:
        //  - M.refetch() alimenta a Agenda (FullCalendar + ocorrências virtuais)
        //  - refetchEventosHoje() alimenta a timeline do resumo (composable
        //    separado em useMelissaEventosHoje); sem isso o card visualmente
        //    não muda apesar do DB estar atualizado.
        M.refetch();
        refetchEventosHoje();
        fecharEvento();
    } catch (e) {
        const msg = e?.message || 'Erro ao atualizar evento';
        toast.add({ severity: 'error', summary: 'Falha ao atualizar', detail: msg, life: 4000 });
    } finally {
        eventoBusy.value = false;
    }
}

function onAgendar()   { updateEventoStatus('agendado', 'Sessão marcada como agendada'); }
function onConcluir()  { updateEventoStatus('realizado', 'Sessão marcada como realizada'); }
function onFaltou()    { updateEventoStatus('faltou', 'Marcada como falta'); }
function onCancelar()  { updateEventoStatus('cancelado', 'Evento cancelado'); }

// Excluir evento via popover (Fase 5, 2026-05-14). Regra: não permitir
// exclusão direta de ocorrência de série recorrente — usar fluxo do dialog
// pra encerrar série ou editar ocorrência. Pra avulsa: confirm + delete +
// remove cobranças vinculadas (com aviso explícito no confirm).
// Ver lançamentos da sessão — abre dialog com financial_records vinculados.
// Reusa o mesmo padrão do dialog dentro do AgendaEventDialog. 2026-05-14.
const lancamentosDialogOpen = ref(false);
const lancamentosList = ref([]);
const lancamentosLoading = ref(false);
const lancamentosEventoTitulo = ref('');
// Por default esconde cancelled (poluem o que importa quando user só
// quer ver os ativos). Toggle 'Mostrar histórico' libera audit trail.
// Resetado a cada abertura do dialog (onVerLancamentos).
const lancamentosShowHistory = ref(false);
const lancamentosFiltered = computed(() => {
    if (lancamentosShowHistory.value) return lancamentosList.value;
    return lancamentosList.value.filter((r) => r.status !== 'cancelled');
});
const lancamentosCancelledCount = computed(() =>
    lancamentosList.value.filter((r) => r.status === 'cancelled').length
);
// Antecipar pagamento (Fase 5, 2026-05-14): paciente quer pagar antes da
// sessão (caso típico em pacote saldo). Materializa a ocorrência (se virtual)
// + cria financial_record paid (PIX/etc) ou pending (Asaas). NÃO decrementa
// sessions_used — só quando marcar Realizada depois.
const anteciparDialogOpen = ref(false);
const anteciparMethod = ref('pix');
const anteciparBusy = ref(false);
const anteciparEventoRef = ref(null); // snapshot do evento no momento do click
// mode: 'create' (Antecipar pagamento — novo record) vs 'update' (Trocar método
// — UPDATE no record paid existente, sem cancel+criar lixo de cancelled).
const anteciparMode = ref('create');
const anteciparMethodOptions = [
    { value: 'pix', label: 'Já recebi — PIX' },
    { value: 'dinheiro', label: 'Já recebi — Dinheiro' },
    { value: 'deposito', label: 'Já recebi — Depósito' },
    { value: 'cartao_maquininha', label: 'Já recebi — Cartão (maquininha)' },
    { value: 'link', label: 'Enviar link de pagamento (Asaas)' }
];

async function onAnteciparPagamento() {
    const ev = eventoSelecionado.value;
    if (!ev) return;
    // Valida: precisa ter paciente, valor (price)
    if (!ev.patient_id || !ev.price) {
        toast.add({
            severity: 'warn',
            summary: 'Não é possível antecipar',
            detail: 'Sessão precisa ter paciente e valor configurado.',
            life: 4000
        });
        return;
    }
    anteciparEventoRef.value = ev;
    anteciparMethod.value = 'pix';
    anteciparMode.value = 'create';
    anteciparDialogOpen.value = true;
}

// Trocar método de pagamento de uma antecipação ATIVA (record paid existente).
// Mesmo dialog do Antecipar, mas no submit faz UPDATE no record existente
// em vez de cancel+criar novo — evita acumular records cancelled no audit
// trail. Default seleciona o método atual pra UX clara.
async function onTrocarMetodoAntecipacao() {
    const ev = eventoSelecionado.value;
    if (!ev?.id) return;
    const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
    if (isVirtualId) {
        toast.add({ severity: 'warn', summary: 'Sessão virtual', detail: 'Sessão sem antecipação ativa.', life: 3000 });
        return;
    }
    // Busca método atual do record paid pra pré-selecionar no dialog
    try {
        const { data: paidRec } = await tenantDb().from('financial_records')
            .select('payment_method')
            .eq('agenda_evento_id', ev.id)
            .eq('status', 'paid')
            .order('paid_at', { ascending: false })
            .limit(1)
            .maybeSingle();
        const current = paidRec?.payment_method;
        anteciparMethod.value = ['pix', 'dinheiro', 'deposito', 'cartao_maquininha'].includes(current) ? current : 'pix';
    } catch {
        anteciparMethod.value = 'pix';
    }
    anteciparEventoRef.value = ev;
    anteciparMode.value = 'update';
    anteciparDialogOpen.value = true;
}

async function confirmAnteciparPagamento() {
    const ev = anteciparEventoRef.value;
    if (!ev || anteciparBusy.value) return;
    anteciparBusy.value = true;
    try {
        // ─── MODE 'update': Trocar método ───────────────────────────
        // Apenas UPDATE no record paid existente. Sem materializar (já é real),
        // sem RPC, sem novo record. Evita lixo cancelled no audit trail.
        if (anteciparMode.value === 'update') {
            const settlement = anteciparMethod.value;
            const today = new Date().toISOString();
            const { data: paidRec, error: fetchErr } = await tenantDb().from('financial_records')
                .select('id, payment_method, notes')
                .eq('agenda_evento_id', ev.id)
                .eq('status', 'paid')
                .order('paid_at', { ascending: false })
                .limit(1)
                .maybeSingle();
            if (fetchErr) throw fetchErr;
            if (!paidRec?.id) {
                throw new Error('Antecipação não encontrada para troca de método.');
            }
            const oldMethod = paidRec.payment_method || '—';
            const noteEntry = `[${today.slice(0, 10)}] Método trocado: ${oldMethod} → ${settlement}`;
            const newNotes = paidRec.notes ? `${paidRec.notes}\n${noteEntry}` : noteEntry;
            const patch = {
                payment_method: settlement === 'link' ? 'asaas' : settlement,
                status: settlement === 'link' ? 'pending' : 'paid',
                paid_at: settlement === 'link' ? null : today,
                notes: newNotes,
                updated_at: today
            };
            const { error: upErr } = await tenantDb().from('financial_records')
                .update(patch)
                .eq('id', paidRec.id);
            if (upErr) throw upErr;

            const methodLabel = anteciparMethodOptions.find((o) => o.value === settlement)?.label || settlement;
            toast.add({
                severity: 'success',
                summary: 'Método atualizado',
                detail: methodLabel,
                life: 3500
            });
            anteciparDialogOpen.value = false;
            await M.refetch();
            refetchEventosHoje();
            return;
        }
        // ─── MODE 'create' (Antecipar pagamento — fluxo original) ──
        const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
        // ownerId: ev.owner_id é prioridade. Fallback pra M.ownerId (composable
        // que conhece o user logado). Pra virtuais ou snapshots stale, ev pode
        // não trazer owner_id; M.ownerId é fonte autoritativa.
        const ownerId = ev.owner_id || ev.terapeuta_id || ev._raw?.owner_id || M?.ownerId?.value || null;
        if (!ownerId) {
            throw new Error('Não foi possível identificar o terapeuta da sessão.');
        }
        const settlement = anteciparMethod.value;
        const amount = Number(ev.price) || 0;
        const dueIso = ev.inicio_em ? new Date(ev.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);

        // 1) Materializa se virtual (cria agenda_evento real com status='agendado')
        const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
        const isVirtual = ev.is_occurrence || isVirtualId;
        let eventoId = ev.id;
        if (isVirtual) {
            const rid = ev.recurrence_id || ev.serie_id || null;
            const rDate = ev.recurrence_date || ev.original_date || (ev.inicio_em ? String(ev.inicio_em).slice(0, 10) : null);
            if (!rid || !rDate) throw new Error('Não foi possível identificar a regra de recorrência.');
            // Confere se já não foi materializada
            const { data: existing } = await tenantDb().from('agenda_eventos')
                .select('id')
                .eq('recurrence_id', rid)
                .eq('recurrence_date', rDate)
                .maybeSingle();
            if (existing?.id) {
                eventoId = existing.id;
            } else {
                const { data: created, error: cErr } = await tenantDb().from('agenda_eventos')
                    .insert({
                        owner_id: ownerId,
                        recurrence_id: rid,
                        recurrence_date: rDate,
                        tipo: 'sessao',
                        status: 'agendado',
                        titulo: ev.titulo || 'Sessão',
                        inicio_em: ev.inicio_em,
                        fim_em: ev.fim_em,
                        patient_id: ev.patient_id,
                        determined_commitment_id: ev.determined_commitment_id || null,
                        modalidade: ev.modalidade || 'presencial',
                        price: amount,
                        visibility_scope: 'public'
                    })
                    .select('id')
                    .single();
                if (cErr) throw cErr;
                eventoId = created.id;
            }
        }

        // 2) Verifica se já tem financial_record vinculado.
        // IMPORTANTE: filtra cancelled — caso típico após revogar a antecipação
        // (record vira cancelled) e user re-antecipa. Sem o filtro, o handler
        // reusava o record cancelled atualizando pra paid, mantendo notes
        // da revogação no audit trail (confuso).
        const { data: existRec } = await tenantDb().from('financial_records')
            .select('id, status')
            .eq('agenda_evento_id', eventoId)
            .is('deleted_at', null)
            .neq('status', 'cancelled')
            .order('created_at', { ascending: false })
            .limit(1)
            .maybeSingle();
        if (existRec?.status === 'paid') {
            toast.add({ severity: 'info', summary: 'Já está pago', detail: 'Esta sessão já tem cobrança paga.', life: 3500 });
            return;
        }

        // 3) Cria record via RPC (ou usa existente pending pra marcar paid)
        let recordId = existRec?.id || null;
        if (!recordId) {
            const { error: rpcErr } = await supabase.rpc('create_financial_record_for_session', {
                p_tenant_id: tenantId,
                p_owner_id: ownerId,
                p_patient_id: ev.patient_id,
                p_agenda_evento_id: eventoId,
                p_amount: amount,
                p_due_date: dueIso
            });
            if (rpcErr) throw rpcErr;
            const { data: newRec } = await tenantDb().from('financial_records')
                .select('id')
                .eq('agenda_evento_id', eventoId)
                .order('created_at', { ascending: false })
                .limit(1)
                .single();
            recordId = newRec?.id;
        }

        // 4) Aplica status conforme settlement
        if (recordId) {
            const patch = { updated_at: new Date().toISOString() };
            if (settlement === 'link') {
                patch.payment_method = 'asaas';
                // status fica pending
            } else {
                patch.status = 'paid';
                patch.paid_at = new Date().toISOString();
                patch.payment_method = settlement;
            }
            await tenantDb().from('financial_records').update(patch).eq('id', recordId);
        }

        const methodLabel = anteciparMethodOptions.find((o) => o.value === settlement)?.label || settlement;
        toast.add({
            severity: 'success',
            summary: settlement === 'link' ? 'Cobrança gerada' : 'Pagamento registrado',
            detail: `R$ ${amount.toFixed(2).replace('.', ',')} — ${methodLabel}`,
            life: 4000
        });
        anteciparDialogOpen.value = false;
        await M.refetch();
        refetchEventosHoje();
        // Não fecha o popover — watch em eventos sincroniza o ev pro novo
        // estado (paymentState='paid' agora). Botão alterna pra "Revogar".
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao antecipar pagamento.', life: 5000 });
    } finally {
        anteciparBusy.value = false;
    }
}

// Revogar antecipação de pagamento (C12): desfaz o `onAnteciparPagamento`.
// Cancela o record paid + nota de auditoria em notes. Só disponível pra
// sessão em status='agendado' (após Realizada o paid vira pagamento normal
// e estorno é via /financeiro).
async function onRevogarAntecipacao() {
    const ev = eventoSelecionado.value;
    if (!ev?.id || eventoBusy.value) return;
    const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
    if (isVirtualId) {
        toast.add({ severity: 'warn', summary: 'Sessão virtual', detail: 'Sessão sem antecipação ativa.', life: 3000 });
        return;
    }
    // Confirma com user — paid é sensível
    const ok = await new Promise((resolve) => {
        confirm.require({
            message: 'Revogar o pagamento antecipado desta sessão? O lançamento financeiro será cancelado e poderá antecipar de novo.',
            header: 'Revogar antecipação?',
            icon: 'pi pi-exclamation-triangle',
            acceptLabel: 'Revogar pagamento',
            rejectLabel: 'Cancelar',
            acceptClass: 'p-button-danger',
            accept: () => resolve(true),
            reject: () => resolve(false),
            onHide: () => resolve(false)
        });
    });
    if (!ok) return;

    eventoBusy.value = true;
    try {
        // Acha o paid record vinculado
        const { data: paidRec, error: fetchErr } = await tenantDb().from('financial_records')
            .select('id, notes, payment_method, final_amount, amount')
            .eq('agenda_evento_id', ev.id)
            .eq('status', 'paid')
            .order('paid_at', { ascending: false })
            .limit(1)
            .maybeSingle();
        if (fetchErr) throw fetchErr;
        if (!paidRec?.id) {
            toast.add({ severity: 'info', summary: 'Nada a revogar', detail: 'Esta sessão não tem pagamento antecipado.', life: 3500 });
            return;
        }
        const today = new Date().toISOString().slice(0, 10);
        const reason = `Antecipação revogada em ${today}`;
        const noteEntry = `[${today}] ${reason}`;
        const noteText = paidRec.notes ? `${paidRec.notes}\n${noteEntry}` : noteEntry;
        const { error: cancelErr } = await tenantDb().from('financial_records')
            .update({
                status: 'cancelled',
                notes: noteText,
                updated_at: new Date().toISOString()
            })
            .eq('id', paidRec.id);
        if (cancelErr) throw cancelErr;

        toast.add({
            severity: 'success',
            summary: 'Antecipação revogada',
            detail: `Cobrança de R$ ${Number(paidRec.final_amount || paidRec.amount || 0).toFixed(2).replace('.', ',')} cancelada.`,
            life: 4000
        });
        await M.refetch();
        refetchEventosHoje();
        // Não fecha popover — watch em eventos sincroniza paymentState='none'.
        // Botão alterna de volta pra "Antecipar pagamento".
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao revogar antecipação.', life: 5000 });
    } finally {
        eventoBusy.value = false;
    }
}

async function onVerLancamentos() {
    const ev = eventoSelecionado.value;
    if (!ev?.id) return;
    // Ocorrência virtual: id é sintético `rec::<rule>::<date>`, não bate com
    // agenda_evento_id (uuid). Mas se a virtual pertence a uma série com
    // contrato upfront, os records do contrato (linkados a sibling
    // materializada) cobrem ela — busca via recurrence_id.
    const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
    const isVirtual = ev.is_occurrence || isVirtualId;

    lancamentosEventoTitulo.value = ev.pacienteNome || ev.label || ev.titulo || 'Sessão';
    lancamentosShowHistory.value = false; // sempre abre limpo (sem cancelled)
    lancamentosDialogOpen.value = true;
    lancamentosLoading.value = true;
    try {
        let records = [];
        if (isVirtual && ev.recurrence_id) {
            // Pega records de QUALQUER sibling materializada (cobre o caso
            // pacote upfront onde só a 1ª tem record do pacote inteiro).
            const { data: siblings } = await tenantDb().from('agenda_eventos')
                .select('id')
                .eq('recurrence_id', ev.recurrence_id);
            const ids = (siblings || []).map((s) => s.id);
            if (ids.length) {
                const { data, error } = await tenantDb().from('financial_records')
                    .select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
                    .in('agenda_evento_id', ids)
                    .is('deleted_at', null)
                    .order('created_at', { ascending: true });
                if (error) throw error;
                records = data || [];
            }
        } else if (!isVirtual) {
            const { data, error } = await tenantDb().from('financial_records')
                .select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
                .eq('agenda_evento_id', ev.id)
                .is('deleted_at', null)
                .order('created_at', { ascending: true });
            if (error) throw error;
            records = data || [];
        }
        lancamentosList.value = records;
        if (!records.length && isVirtual) {
            // Caso virtual sem records de contrato (saldo, sem pacote): fecha
            // o dialog e avisa que ainda não materializou (comportamento antigo).
            lancamentosDialogOpen.value = false;
            toast.add({
                severity: 'info',
                summary: 'Sem lançamentos ainda',
                detail: 'Esta ocorrência ainda não foi materializada. Lançamentos aparecem após a primeira ação na sessão (status, edição etc).',
                life: 5000
            });
        }
    } catch (e) {
        toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao carregar lançamentos.', life: 4000 });
        lancamentosList.value = [];
    } finally {
        lancamentosLoading.value = false;
    }
}
function _fmtLancBRL(v) {
    return Number(v ?? 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
}
function _fmtLancDate(d) {
    if (!d) return '—';
    try { return new Date(d).toLocaleDateString('pt-BR'); } catch { return '—'; }
}
const _lancMethodLabels = {
    pix: 'PIX', dinheiro: 'Dinheiro', deposito: 'Depósito', cartao: 'Cartão',
    cartao_maquininha: 'Cartão (maquininha)', convenio: 'Convênio', asaas: 'Asaas'
};
const _lancStatusLabels = {
    pending: 'Pendente', paid: 'Pago', overdue: 'Vencido',
    cancelled: 'Cancelado', refunded: 'Reembolsado', partial: 'Parcial'
};
function _lancStatusSeverity(s) {
    return { pending: 'info', paid: 'success', overdue: 'danger', cancelled: 'secondary', refunded: 'warn', partial: 'warn' }[s] || 'secondary';
}

async function onDeleteEvento() {
    const ev = eventoSelecionado.value;
    if (!ev?.id || eventoBusy.value) return;

    const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
    const isVirtual = ev.is_occurrence || isVirtualId;

    // ── Ocorrência virtual: cria recurrence_exception (cancel_session) ──
    // Sem interação ainda — segura excluir. Mostra confirm simples.
    if (isVirtual) {
        const recId = ev.recurrence_id || ev.serie_id;
        const origDate = ev.recurrence_date || ev.original_date || (ev.inicio_em ? String(ev.inicio_em).slice(0, 10) : null);
        if (!recId || !origDate) {
            toast.add({ severity: 'warn', summary: 'Não excluível', detail: 'Não foi possível identificar a regra de recorrência.', life: 4000 });
            return;
        }
        confirm.require({
            header: 'Cancelar ocorrência',
            message: 'Esta ocorrência ainda não tem cobranças. Tem certeza que deseja cancelá-la? Ela some da agenda; as outras sessões da série continuam.',
            icon: 'pi pi-exclamation-triangle',
            acceptLabel: 'Sim, cancelar',
            rejectLabel: 'Manter',
            acceptClass: 'p-button-danger',
            accept: async () => {
                eventoBusy.value = true;
                try {
                    const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
                    // upsert pra ser idempotente: se já existe exception
                    // pra (recurrence_id, original_date) — edição anterior,
                    // conflito de criação, cancel duplicado — sobrescreve em
                    // vez de quebrar com unique violation.
                    const { error } = await tenantDb().from('recurrence_exceptions').upsert({
                        recurrence_id: recId,
                        original_date: origDate,
                        type: 'cancel_session',
                        reason: 'Cancelado pelo terapeuta antes de qualquer interação'
                    }, { onConflict: 'recurrence_id,original_date' });
                    if (error) throw error;
                    toast.add({ severity: 'success', summary: 'Ocorrência cancelada', life: 2500 });
                    M.refetch();
                    refetchEventosHoje();
                    fecharEvento();
                } catch (e) {
                    toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao cancelar.', life: 4000 });
                } finally {
                    eventoBusy.value = false;
                }
            }
        });
        return;
    }

    // ── Evento real: conta cobranças vinculadas ──
    let recordsCount = 0;
    let hasPaidRecord = false;
    try {
        const { data } = await tenantDb().from('financial_records')
            .select('id, status')
            .eq('agenda_evento_id', ev.id)
            .is('deleted_at', null);
        recordsCount = (data || []).length;
        hasPaidRecord = (data || []).some((r) => r.status === 'paid');
    } catch (e) {
        console.warn('[Excluir sessão] erro contando records:', e?.message);
    }

    // Cobrança PAGA bloqueia exclusão — precisa estornar pelo Financeiro
    if (hasPaidRecord) {
        toast.add({
            severity: 'warn',
            summary: 'Sessão com pagamento confirmado',
            detail: 'Esta sessão tem cobrança paga. Estorne primeiro pelo Financeiro antes de excluir.',
            life: 5500
        });
        return;
    }

    // Evento de série materializado (tem recurrence_id) → vira exception
    // (cancel_session) também, mas removendo records pendentes junto.
    const isMaterializedOccurrence = !!ev.recurrence_id || !!ev.serie_id;
    const msgRecords = recordsCount > 0
        ? `Esta sessão tem ${recordsCount} cobrança(s) pendente(s) que também será(ão) removida(s).`
        : 'A sessão não tem cobranças vinculadas.';

    confirm.require({
        header: isMaterializedOccurrence ? 'Cancelar ocorrência' : 'Excluir sessão',
        message: `${msgRecords} A ação não pode ser desfeita. Confirmar?`,
        icon: 'pi pi-exclamation-triangle',
        acceptLabel: 'Sim, confirmar',
        rejectLabel: 'Cancelar',
        acceptClass: 'p-button-danger',
        accept: async () => {
            eventoBusy.value = true;
            try {
                // 1) Remove cobranças vinculadas (não-pagas)
                if (recordsCount > 0) {
                    const { error: recErr } = await tenantDb().from('financial_records').delete().eq('agenda_evento_id', ev.id);
                    if (recErr) throw recErr;
                }
                if (isMaterializedOccurrence) {
                    // Cria exception cancel_session + DELETE da row (some da agenda)
                    const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
                    const origDate = ev.recurrence_date || ev.original_date || (ev.inicio_em ? String(ev.inicio_em).slice(0, 10) : null);
                    if (origDate) {
                        // upsert pra ser idempotente: se já existe exception
                        // pra (recurrence_id, original_date), sobrescreve.
                        const { error: exErr } = await tenantDb().from('recurrence_exceptions').upsert({
                            recurrence_id: ev.recurrence_id || ev.serie_id,
                            original_date: origDate,
                            type: 'cancel_session',
                            reason: 'Cancelado pelo terapeuta'
                        }, { onConflict: 'recurrence_id,original_date' });
                        if (exErr) console.warn('[Excluir] exception upsert falhou:', exErr?.message);
                    }
                }
                const { error } = await tenantDb().from('agenda_eventos').delete().eq('id', ev.id);
                if (error) throw error;
                const detail = recordsCount > 0 ? `Sessão e ${recordsCount} cobrança(s) removida(s).` : 'Sessão removida.';
                toast.add({ severity: 'success', summary: isMaterializedOccurrence ? 'Ocorrência cancelada' : 'Excluída', detail, life: 3000 });
                M.refetch();
                refetchEventosHoje();
                fecharEvento();
            } catch (e) {
                toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao excluir.', life: 4000 });
            } finally {
                eventoBusy.value = false;
            }
        }
    });
}

// Excluir SÉRIE INTEIRA — hard delete (escolha do user em 19/05).
// Deleta recurrence_rules (CASCADE leva exceptions + recurrence_rule_services),
// agenda_eventos materializados (linha por linha pra disparar handlers/triggers),
// e financial_records pendentes ligados. Bloqueia se houver QUALQUER record pago
// (precisa estornar pelo Financeiro primeiro).
async function onDeleteSeries() {
    const ev = eventoSelecionado.value;
    if (!ev || eventoBusy.value) return;
    const ruleId = ev.recurrence_id || ev.serie_id;
    if (!ruleId) {
        toast.add({ severity: 'warn', summary: 'Sem série', detail: 'Este evento não pertence a uma recorrência.', life: 3000 });
        return;
    }

    // Conta eventos materializados + records vinculados pra montar mensagem
    // e detectar paid blockers ANTES de confirmar.
    let materializedCount = 0;
    let recordsPending = 0;
    let hasPaid = false;
    try {
        const { data: evts } = await tenantDb().from('agenda_eventos')
            .select('id')
            .eq('recurrence_id', ruleId);
        materializedCount = (evts || []).length;
        const evtIds = (evts || []).map((e) => e.id);
        if (evtIds.length) {
            const { data: recs } = await tenantDb().from('financial_records')
                .select('id, status')
                .in('agenda_evento_id', evtIds)
                .is('deleted_at', null);
            for (const r of recs || []) {
                if (r.status === 'paid') hasPaid = true;
                else recordsPending++;
            }
        }
    } catch (e) {
        console.warn('[onDeleteSeries] contagem falhou:', e?.message);
    }

    if (hasPaid) {
        toast.add({
            severity: 'warn',
            summary: 'Série com pagamento confirmado',
            detail: 'Uma ou mais sessões desta série têm cobrança paga. Estorne primeiro pelo Financeiro antes de excluir a série.',
            life: 6000
        });
        return;
    }

    const msgParts = [];
    msgParts.push(`Esta ação remove **toda a série de recorrência**:`);
    if (materializedCount > 0) msgParts.push(`${materializedCount} sessão(ões) já materializada(s)`);
    if (recordsPending > 0) msgParts.push(`${recordsPending} cobrança(s) pendente(s)`);
    msgParts.push('e a própria regra. As ocorrências futuras param de aparecer na agenda. **A ação não pode ser desfeita.** Confirmar?');

    confirm.require({
        header: 'Excluir série inteira',
        message: msgParts.join(' '),
        icon: 'pi pi-exclamation-triangle',
        acceptLabel: 'Sim, excluir série',
        rejectLabel: 'Manter',
        acceptClass: 'p-button-danger',
        accept: async () => {
            eventoBusy.value = true;
            try {
                // 1) Apaga financial_records pendentes vinculados a eventos
                //    materializados desta série
                const { data: evts2 } = await tenantDb().from('agenda_eventos')
                    .select('id')
                    .eq('recurrence_id', ruleId);
                const evtIds = (evts2 || []).map((e) => e.id);
                if (evtIds.length) {
                    const { error: recErr } = await tenantDb().from('financial_records')
                        .delete()
                        .in('agenda_evento_id', evtIds);
                    if (recErr) throw recErr;
                }
                // 2) Apaga eventos materializados
                if (evtIds.length) {
                    const { error: evErr } = await tenantDb().from('agenda_eventos')
                        .delete()
                        .in('id', evtIds);
                    if (evErr) throw evErr;
                }
                // 3) Apaga a regra (CASCADE: exceptions + rule_services)
                const { error: ruleErr } = await tenantDb().from('recurrence_rules')
                    .delete()
                    .eq('id', ruleId);
                if (ruleErr) throw ruleErr;

                toast.add({
                    severity: 'success',
                    summary: 'Série excluída',
                    detail: `Regra + ${materializedCount} sessão(ões)${recordsPending > 0 ? ` + ${recordsPending} cobrança(s)` : ''} removida(s).`,
                    life: 3500
                });
                M.refetch();
                refetchEventosHoje();
                fecharEvento();
            } catch (e) {
                toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao excluir série.', life: 5000 });
            } finally {
                eventoBusy.value = false;
            }
        }
    });
}

// Atalho do popover: gera cobrança da sessão atual via gerarCobrancaManual
// sem precisar abrir o AgendaEventDialog. Mesmo RPC do botão "Gerar
// cobrança" do AgendaEventoFinanceiroPanel. Só vale pra sessão real
// (eventoSelecionado.id existe + não é virtual). Apos sucesso refetch
// pra badge $ aparecer (paymentState='pending').
async function onGerarCobrancaQuick() {
    const ev = eventoSelecionado.value;
    if (!ev || eventoBusy.value) return;
    if (!ev.id || ev.is_occurrence) {
        toast.add({ severity: 'warn', summary: 'Não disponível', detail: 'Esta ocorrência ainda não está materializada.', life: 3500 });
        return;
    }
    eventoBusy.value = true;
    try {
        // gerarCobrancaManual le evento.price; passamos a row crua via _raw
        // que ja inclui price/insurance_value/billing_contract_id.
        const eventoRaw = ev._raw || ev;
        const result = await gerarCobrancaManual(eventoRaw);
        if (!result.ok) throw new Error(result.error);
        const valor = eventoRaw.price ?? eventoRaw.insurance_value ?? 0;
        toast.add({
            severity: 'success',
            summary: 'Cobrança gerada',
            detail: `R$ ${Number(valor).toFixed(2).replace('.', ',')} agendado para recebimento.`,
            life: 3000
        });
        M.refetch();
        refetchEventosHoje();
        // Fecha o popover apos sucesso pra impedir click duplicado
        // gerando outra fatura. User reabre se quiser ver estado novo.
        fecharEvento();
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao gerar cobrança.', life: 4000 });
    } finally {
        eventoBusy.value = false;
    }
}

// "Usar" sessão de pacote saldo — combina em uma ação:
//   1. Materializa virtual (se ainda virtual)
//   2. Status='realizada'
//   3. Cria financial_record per_session via RPC (status='pending', dueDate=hoje)
//   4. Incrementa billing_contracts.sessions_used
//   5. Se sessions_used == total → contract.status='completed'
// Reflete o modelo Cliniko: paciente comparece, sessão é "usada" do saldo,
// nasce a cobrança individual.
async function onUsarSessao(payload = null) {
    // Aceita payload do dialog ({ eventRow, contract }) OU usa fallback
    // do estado do popover (eventoSelecionado + ev.contract injetado pelo
    // bulk-load). Permite reuse do handler em ambos os fluxos.
    const ev = payload?.eventRow || eventoSelecionado.value;
    if (!ev || eventoBusy.value) return;
    const contract = payload?.contract || ev?.contract;
    if (!contract?.id) {
        toast.add({ severity: 'warn', summary: 'Sem contrato', detail: 'Esta sessão não pertence a um pacote ativo.', life: 3000 });
        return;
    }
    if (contract.style !== 'saldo') {
        // Pra upfront, "Usar" não cabe (pacote já foi pago de uma vez)
        toast.add({ severity: 'info', summary: 'Pacote upfront', detail: 'Este pacote já foi cobrado integralmente. Use "Realizada" pra marcar status.', life: 4000 });
        return;
    }

    eventoBusy.value = true;
    try {
        const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
        const ownerId = M?.ownerId?.value || ev.owner_id;
        const perSession = contract.totalSessions > 0 ? (contract.packagePrice || 0) / contract.totalSessions : 0;
        const dueDate = ev.inicio_em ? String(ev.inicio_em).slice(0, 10) : new Date().toISOString().slice(0, 10);

        // 1) Materializa virtual se necessário
        let eventoId = ev.id;
        const isVirtualId = typeof eventoId === 'string' && eventoId.startsWith('rec::');
        if (ev.is_occurrence || isVirtualId) {
            const recurrenceDate = ev.recurrence_date || (ev.inicio_em ? String(ev.inicio_em).slice(0, 10) : null);
            // Preserva determined_commitment_id da regra (necessário pra
            // isSessionEvent=true no AgendaEventDialog; sem ele o campo
            // "Título" aparece indevidamente porque o dialog acha que não
            // é sessão).
            const raw = ev._raw || {};
            const { data: created, error: matErr } = await tenantDb().from('agenda_eventos')
                .insert({
                    owner_id: ownerId,
                    patient_id: ev.patient_id || ev.paciente_id,
                    recurrence_id: ev.recurrence_id,
                    recurrence_date: recurrenceDate,
                    tipo: 'sessao',
                    status: 'realizado',
                    titulo: ev.label || ev.titulo || raw.titulo || 'Sessão',
                    inicio_em: ev.inicio_em,
                    fim_em: ev.fim_em,
                    modalidade: ev.modalidade || raw.modalidade || 'presencial',
                    determined_commitment_id: raw.determined_commitment_id || null,
                    price: perSession,
                    billing_contract_id: contract.id,
                    visibility_scope: 'public'
                })
                .select('id')
                .single();
            if (matErr) throw matErr;
            eventoId = created.id;
        } else {
            // Sessão real: atualiza status + garante link com contrato.
            // Tambem backfilla determined_commitment_id se estiver NULL
            // (legado de uses antigos antes do fix). Sem isso o dialog
            // mostra campo "Título" indevidamente (isSessionEvent=false).
            const raw = ev._raw || {};
            const patch = { status: 'realizado', billing_contract_id: contract.id };
            const commitmentId = raw.determined_commitment_id || null;
            if (commitmentId) {
                patch.determined_commitment_id = commitmentId;
            } else {
                // Busca da rule (fonte autoritativa) se ev não tem
                const { data: rule } = await tenantDb().from('recurrence_rules')
                    .select('determined_commitment_id')
                    .eq('id', ev.recurrence_id)
                    .maybeSingle();
                if (rule?.determined_commitment_id) {
                    patch.determined_commitment_id = rule.determined_commitment_id;
                }
            }
            const { error: upErr } = await tenantDb().from('agenda_eventos')
                .update(patch)
                .eq('id', eventoId);
            if (upErr) throw upErr;
        }

        // 2) Cria financial_record per_session (RPC ignora cancelled na idempotência)
        const { error: cobErr } = await supabase.rpc('create_financial_record_for_session', {
            p_tenant_id: tenantId,
            p_owner_id: ownerId,
            p_patient_id: ev.patient_id || ev.paciente_id,
            p_agenda_evento_id: eventoId,
            p_amount: perSession,
            p_due_date: dueDate
        });
        if (cobErr) throw cobErr;

        // 3) Incrementa sessions_used + completa contract se necessário
        const newUsed = (contract.sessionsUsed || 0) + 1;
        const patchContract = { sessions_used: newUsed };
        if (newUsed >= contract.totalSessions) {
            patchContract.status = 'completed';
        }
        const { error: ctErr } = await tenantDb().from('billing_contracts')
            .update(patchContract)
            .eq('id', contract.id);
        if (ctErr) throw ctErr;

        toast.add({
            severity: 'success',
            summary: 'Sessão usada do pacote',
            detail: `Cobrança de R$ ${perSession.toFixed(2).replace('.', ',')} gerada. Saldo: ${newUsed}/${contract.totalSessions}.`,
            life: 4000
        });
        M.refetch();
        refetchEventosHoje();
        // Fecha popover + dialogs eventuais (chamada pode vir do popover OU
        // de qualquer um dos AgendaEventDialog empilhados)
        fecharEvento();
        if (M.dialogOpen) M.dialogOpen.value = false;
        if (M.occDialogOpen) M.occDialogOpen.value = false;
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao usar sessão.', life: 5000 });
    } finally {
        eventoBusy.value = false;
    }
}

// "Revogar" sessão usada do pacote saldo — desfaz onUsarSessao:
//   1. Cancela financial_record (status='cancelled')
//   2. Decrementa billing_contracts.sessions_used (se > 0)
//   3. Se contract estava completed, volta pra active
//   4. Status do evento volta pra 'agendado'
// Constraint: só funciona se record ainda pending. Pago = bloqueado (precisa
// estorno formal pelo Financeiro).
async function onRevogarSessao(payload = null) {
    // Aceita payload do dialog ({ eventRow, contract }) OU usa fallback
    // do popover (eventoSelecionado + ev.contract).
    const ev = payload?.eventRow || eventoSelecionado.value;
    if (!ev || eventoBusy.value) return;
    const contract = payload?.contract || ev?.contract;
    if (!contract?.id) {
        toast.add({ severity: 'warn', summary: 'Sem contrato', detail: 'Esta sessão não pertence a um pacote ativo.', life: 3000 });
        return;
    }
    if (!ev.id || ev.is_occurrence) {
        toast.add({ severity: 'warn', summary: 'Não disponível', detail: 'Sessão ainda não foi usada.', life: 3000 });
        return;
    }

    eventoBusy.value = true;
    try {
        // Acha record vinculado a essa sessão e ao contrato
        const { data: recs } = await tenantDb().from('financial_records')
            .select('id, status')
            .eq('agenda_evento_id', ev.id)
            .neq('status', 'cancelled')
            .order('created_at', { ascending: false });
        const activeRec = (recs || []).find((r) => r.status !== 'cancelled');
        if (activeRec && activeRec.status === 'paid') {
            toast.add({
                severity: 'warn',
                summary: 'Cobrança já paga',
                detail: 'Esta sessão já foi paga. Estorne primeiro pelo Financeiro antes de revogar.',
                life: 5000
            });
            return;
        }

        // 1) Cancela record (se ainda pending)
        if (activeRec) {
            const { error: cancelErr } = await tenantDb().from('financial_records')
                .update({ status: 'cancelled', updated_at: new Date().toISOString() })
                .eq('id', activeRec.id);
            if (cancelErr) throw cancelErr;
        }

        // 2) Decrementa sessions_used + reativa contract se estava completed
        const newUsed = Math.max(0, (contract.sessionsUsed || 0) - 1);
        const patchContract = { sessions_used: newUsed };
        // Se contrato estava completed (sessionsUsed == total) e agora < total, reativa
        if ((contract.sessionsUsed || 0) >= contract.totalSessions) {
            patchContract.status = 'active';
        }
        const { error: ctErr } = await tenantDb().from('billing_contracts')
            .update(patchContract)
            .eq('id', contract.id);
        if (ctErr) throw ctErr;

        // 3) Status do evento volta pra agendado.
        //    Backfill determined_commitment_id se estiver NULL (legado de uses
        //    antigos antes do fix). Garante que próxima abertura do dialog
        //    não exiba campo "Título" indevidamente.
        const raw = ev._raw || {};
        const patch = { status: 'agendado' };
        if (!raw.determined_commitment_id && ev.recurrence_id) {
            const { data: rule } = await tenantDb().from('recurrence_rules')
                .select('determined_commitment_id')
                .eq('id', ev.recurrence_id)
                .maybeSingle();
            if (rule?.determined_commitment_id) {
                patch.determined_commitment_id = rule.determined_commitment_id;
            }
        }
        const { error: upErr } = await tenantDb().from('agenda_eventos')
            .update(patch)
            .eq('id', ev.id);
        if (upErr) throw upErr;

        toast.add({
            severity: 'success',
            summary: 'Sessão revogada',
            detail: `Cobrança cancelada. Saldo: ${newUsed}/${contract.totalSessions}.`,
            life: 4000
        });
        M.refetch();
        refetchEventosHoje();
        fecharEvento();
        if (M.dialogOpen) M.dialogOpen.value = false;
        if (M.occDialogOpen) M.occDialogOpen.value = false;
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao revogar sessão.', life: 5000 });
    } finally {
        eventoBusy.value = false;
    }
}

// Bubble up do AgendaEventoFinanceiroPanel via AgendaEventDialog.
// Disparado quando cobrança é gerada / paga / cancelada dentro do
// dialog. Re-roda o bulk-load do useMelissaAgenda pra que o FC
// atualize badge $ / borda verde sem precisar trocar de view.
function onCobrancaAtualizada() {
    M.refetch();
    refetchEventosHoje();
}

async function onWhatsapp() {
    const ev = eventoSelecionado.value;
    if (!ev?.patient_id) {
        toast.add({ severity: 'warn', summary: 'Paciente sem id', life: 2200 });
        return;
    }
    const patientId = String(ev.patient_id);
    fecharEvento();
    // openForPatient é async — busca thread existente ou cria stub.
    // Se paciente não tem telefone (ou outro erro), o store seta `error`
    // e mantém `isOpen=false` silenciosamente. Aguardamos pra dar feedback.
    await conversationDrawerStore.openForPatient(patientId);
    if (!conversationDrawerStore.isOpen) {
        const detail = conversationDrawerStore.error?.message || 'Não foi possível abrir a conversa.';
        toast.add({ severity: 'warn', summary: 'WhatsApp', detail, life: 3500 });
        conversationDrawerStore.error = null;
    }
}

// ── Pending agenda actions ────────────────────────────────────
// Eventos clicados no resumo (timeline) podem disparar actions que
// dependem do MelissaAgenda estar montado (openProntuario, setView).
// Como o resumo NÃO tem a Agenda montada, abrimos a seção e enfileiramos
// a action — quando `melissaAgendaRef` aparece, o watch dispara.
const _pendingAgendaAction = ref(null);
watch(melissaAgendaRef, (refValue) => {
    if (!refValue || !_pendingAgendaAction.value) return;
    const action = _pendingAgendaAction.value;
    _pendingAgendaAction.value = null;
    try { action(refValue); } catch (e) { /* eslint-disable-next-line no-console */ console.warn('[pending-agenda]', e); }
});
function _callOnAgenda(action) {
    if (melissaAgendaRef.value) {
        try { action(melissaAgendaRef.value); } catch (e) { /* eslint-disable-next-line no-console */ console.warn('[agenda-action]', e); }
        return;
    }
    _pendingAgendaAction.value = action;
    if (secaoAberta.value !== 'agenda') abrirSecao('agenda');
}

// MelissaBusca @goto-date — usuario digitou "hoje"/"20/06" na busca
// global. Abre a agenda se fechada (via _callOnAgenda que enfileira a
// action ate o ref aparecer) e chama gotoDate exposto pela MelissaAgenda.
function onBuscaGotoDate(date) {
    if (!(date instanceof Date) || Number.isNaN(date.getTime())) return;
    _callOnAgenda((agenda) => agenda.gotoDate?.(date));
}

// Ref + provide pra qualquer secao filha pedir pra abrir a busca global
// programaticamente. UI nao tem mais botao por secao (lupa unica fica
// na .melissa-tray), mas o inject permanece exposto pra acoes contextuais
// futuras (ex: "buscar paciente" num componente filho que quer abrir
// o spotlight com query pre-preenchida).
const melissaBuscaRef = ref(null);
provide('openMelissaBusca', () => melissaBuscaRef.value?.openDialog?.());

function onAbrirProntuario() {
    const ev = eventoSelecionado.value;
    if (!ev?.patient_id) {
        toast.add({ severity: 'warn', summary: 'Paciente não vinculado', life: 2200 });
        return;
    }
    const p = pacientesReais.value.find((x) => String(x.id) === String(ev.patient_id));
    if (!p) {
        toast.add({ severity: 'warn', summary: 'Paciente não encontrado na lista', life: 2200 });
        return;
    }
    fecharEvento();
    _callOnAgenda((agenda) => agenda.openProntuario?.(p));
}

function onHistoricoSessoes() {
    const ev = eventoSelecionado.value;
    if (!ev?.patient_id) {
        toast.add({ severity: 'warn', summary: 'Paciente não vinculado', life: 2200 });
        return;
    }
    const patientId = ev.patient_id;
    fecharEvento();
    // Abre a Agenda (se não estiver) e dispara o overlay "Todas as sessões"
    // filtrado pelo paciente — mesmo fluxo do botão Sessões em .ma-dock-actions.
    _callOnAgenda((agenda) => agenda.openSessoesPaciente?.(patientId));
}

// Editar cadastro do paciente vinculado à sessão. Difere de onEditEvento
// (que abre o AgendaEventDialog pra mexer na sessão em si). Reusa o
// PatientCadastroDialog já montado dentro do MelissaAgenda via método
// exposto openEditPatient.
function onEditPaciente() {
    const ev = eventoSelecionado.value;
    if (!ev?.patient_id) {
        toast.add({ severity: 'warn', summary: 'Paciente não vinculado', life: 2200 });
        return;
    }
    const patientId = ev.patient_id;
    fecharEvento();
    _callOnAgenda((agenda) => agenda.openEditPatient?.(patientId));
}

async function onEditEvento() {
    // Abre AgendaEventDialog completo via composable. Eventos vindos do
    // resumo (useMelissaEventosHoje) não têm `_raw` — buscamos o row
    // completo agora pra alimentar o dialog (campos de recorrência,
    // financeiro, terapeuta_id, etc.).
    const ev = eventoSelecionado.value;
    if (!ev?.id || eventoBusy.value) return;
    // Se já tiver _raw (caso futuro: vier da Agenda), usa direto.
    if (ev._raw) {
        const raw = ev._raw;
        fecharEvento();
        M.onEditEvento(raw);
        return;
    }
    eventoBusy.value = true;
    try {
        const { data, error } = await tenantDb().from('agenda_eventos')
            .select('*')
            .eq('id', ev.id)
            .maybeSingle();
        if (error) throw error;
        if (!data) {
            toast.add({ severity: 'warn', summary: 'Evento não encontrado', life: 2200 });
            return;
        }
        fecharEvento();
        M.onEditEvento(data);
    } catch (e) {
        toast.add({ severity: 'error', summary: 'Falha ao abrir editor', detail: e?.message || 'Erro inesperado', life: 4000 });
    } finally {
        eventoBusy.value = false;
    }
}

function onRemarcar() {
    // MVP: muda status pra 'remarcar'. Reagendar de fato (mover horário)
    // sai daqui pra dentro do AgendaEventDialog, via "Editar".
    updateEventoStatus('remarcar', 'Marcada pra remarcar');
}

// Filtro da timeline por tipo (clicado no resumo do hero, lido pela
// timeline e pelo hero pra destacar chip ativo). Permanece aqui no
// pai porque eh state compartilhado entre dois filhos.
const filtroTipo = ref(null);
function toggleFiltro(tipo) {
    filtroTipo.value = filtroTipo.value === tipo ? null : tipo;
}
function limparFiltro() {
    filtroTipo.value = null;
}

// Helper de formatacao de hora — usado pelo card "proximo paciente"
// abaixo. A timeline tem sua propria copia interna.
function fmtHora(h) {
    const horas = Math.floor(h);
    const mins = Math.round((h - horas) * 60);
    return `${String(horas).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}

// ───────────────────────────────────────────────────────────────
// Cards do resumo — proximo-paciente derivado de eventosHojeReais.
// ───────────────────────────────────────────────────────────────
function _iniciaisDe(nome) {
    const partes = String(nome || '').trim().split(/\s+/).filter(Boolean);
    if (!partes.length) return '?';
    if (partes.length === 1) return partes[0].slice(0, 2).toUpperCase();
    return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase();
}
function _fmtTempoAte(min) {
    if (min < 1) return 'agora';
    if (min < 60) return `em ${min} min`;
    const h = Math.floor(min / 60);
    const m = min % 60;
    if (m === 0) return `em ${h}h`;
    return `em ${h}h${String(m).padStart(2, '0')}`;
}
const proximoPaciente = computed(() => {
    const d = now.value;
    const hNow = d.getHours() + d.getMinutes() / 60;
    const futuros = (eventosHojeReais.value || [])
        .filter((ev) => {
            // Só atendimentos (tipo sessao). Bloqueios e outros não contam.
            if (ev.tipo && ev.tipo !== 'sessao') return false;
            const s = String(ev.status || '').toLowerCase();
            if (s === 'cancelado' || s === 'cancelada' || s === 'faltou' || s === 'realizado' || s === 'realizada') return false;
            // Inclui em-curso (endH > hNow) — paciente atual conta como "próximo".
            return ev.endH > hNow;
        })
        .sort((a, b) => a.startH - b.startH);
    const ev = futuros[0];
    if (!ev) return null;
    const emMin = Math.max(0, Math.round((ev.startH - hNow) * 60));
    const emCurso = hNow >= ev.startH && hNow < ev.endH;
    const nome = ev.pacienteNome || ev.label || ev.titulo || '—';
    return {
        ev,
        nome,
        iniciais: _iniciaisDe(nome),
        horario: fmtHora(ev.startH),
        emMin,
        emCurso,
        statusLabel: emCurso ? 'em curso' : _fmtTempoAte(emMin)
    };
});

// WhatsApp: total de unread + preview do thread mais recente.
// Lê de `conversation_threads` (mesma view do drawer/kanban).
const { summary: whatsappPendente } = useMelissaWhatsapp();

// Notificações + ajuda + plan switcher (DEV) — triggers do "topbar" Melissa.
const notificationStore = useNotificationStore();
useNotifications(); // hook side-effect: realtime + toasts de system_alerts
const { openDrawer: openAjudaDrawer, closeDrawer: closeAjudaDrawer, drawerOpen: ajudaDrawerOpen } = useAjuda();
function toggleAjuda() {
    ajudaDrawerOpen.value ? closeAjudaDrawer() : openAjudaDrawer();
}
const {
    planBtn,
    planMenu,
    planMenuModel,
    planMenuLoading,
    trocandoPlano,
    showPlanDevMenu,
    openPlanMenu
} = useTopbarPlanMenu();

// Extras DEV (layout switcher + atalhos M1) que aparecem no MESMO menu do botão sliders.
const { devExtrasModel } = useTopbarDevMenuExtras();
const combinedDevMenuModel = computed(() => [
    ...planMenuModel.value,
    { separator: true },
    ...devExtrasModel.value
]);

// Recebíveis derivados de agenda_eventos.{price,billed}: aproximação MVP.
// `billed=true` é o flag de "marcado como pago/cobrado" no agenda — não
// é a fonte de verdade financeira (essa é financial_records.status='paid'),
// mas é coerente com o resto da timeline e suficiente pro card de resumo.
// Refinar depois se quiser fidelidade real.
const recebiveis = computed(() => {
    const eventos = (eventosHojeReais.value || []).filter((ev) => {
        if (ev.tipo && ev.tipo !== 'sessao') return false;
        const s = String(ev.status || '').toLowerCase();
        if (s === 'cancelado' || s === 'cancelada' || s === 'faltou') return false;
        return true;
    });
    let previsto = 0;
    let recebido = 0;
    let sessoesPagas = 0;
    for (const ev of eventos) {
        const p = Number(ev.price || 0);
        previsto += p;
        if (ev.billed) {
            recebido += p;
            sessoesPagas += 1;
        }
    }
    return {
        previsto,
        recebido,
        sessoes: eventos.length,
        sessoesPagas,
        hasPrice: previsto > 0
    };
});
const recebidoPct = computed(() => {
    const r = recebiveis.value;
    return r.previsto > 0 ? Math.round((r.recebido / r.previsto) * 100) : 0;
});

// Copilot: backend real (LLM/insights) ainda não existe. Mantido no
// catálogo como `implementado: false` pra renderizar via branch placeholder
// "Em breve" — sem mock que finja funcionalidade.

function brl(v) {
    return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
}

// ───────────────────────────────────────────────────────────────
// Cronômetro de sessão (componente externo)
// ───────────────────────────────────────────────────────────────
// TODO: vir das configurações do tenant (página /configuracoes já existe)
const CONFIG_DURACAO_MIN = 50;

// pacientesMock removido — agora usa `pacientesReais` (composable Supabase).
// Mantido como fallback comentado pra debug rápido em dev se precisar:
// const pacientesMock = [{ id: 'p1', nome: 'Marina Silva' }, ...];

const cronoRef = ref(null);
const cronoVisible = ref(false); // sincronizado via @visible-change

// Preferencia de toque + funcao de teste (composable). cronoRef/abrir/fechar
// continuam aqui — sao instance state do <MelissaCronometro>, nao da pref.
const { toqueTermino, testarToque } = useMelissaToques('sino');

function abrirCronometro() {
    cronoRef.value?.abrir();
}

// Click no botao ⏱ de um evento da timeline (ou no CTA do card
// "Proximo paciente" quando o evento esta em curso). Pre-seleciona
// o paciente + autostart. Se ja houver cronometro rodando de outro
// paciente, mostra toast sem trocar (opcao b decidida 2026-05-22).
function onIniciarCronometroFromEvento(ev) {
    if (!ev?.patient_id) return;
    // Plano programado: horario original do evento na agenda. So passa se
    // os campos forem numericos validos — abrir() sanitiza de novo internamente.
    const sessionPlan = (typeof ev.startH === 'number' && typeof ev.endH === 'number')
        ? { startH: ev.startH, endH: ev.endH }
        : null;
    const ret = cronoRef.value?.abrir({ pacienteId: ev.patient_id, autostart: true, sessionPlan });
    if (ret && !ret.opened && ret.alreadyRunning && !ret.samePaciente) {
        const atualNome = pacientesReais.value.find((p) => String(p.id) === String(ret.pacienteId))?.nome
            || 'outro paciente';
        toast.add({
            severity: 'warn',
            summary: 'Cronômetro já ativo',
            detail: `Sessão de ${atualNome} em andamento. Pare o cronômetro atual antes de iniciar outro.`,
            life: 3500
        });
    }
}
function fecharCronometro() {
    cronoRef.value?.fechar();
}

// ── Cronometro: salvar tempo na sessao agendada ──────────────
// Quando o user para o cronometro com paciente selecionado, busca a
// sessao agendada do dia desse paciente em agenda_eventos e grava o
// tempo cronometrado em extra_fields.cronometro_*. Se nao encontrar
// sessao agendada hoje, avisa e nao falha silencioso.
async function onCronometroSessionEnd({ pacienteId, elapsedSec, stoppedAt }) {
    if (!pacienteId || !Number.isFinite(elapsedSec) || elapsedSec <= 0) return;
    const tenantId = tenantStore?.activeTenantId || tenantStore?.tenantId;
    if (!tenantId) {
        toast.add({ severity: 'warn', summary: 'Sem tenant ativo', detail: 'Não foi possível salvar o tempo.', life: 3500 });
        return;
    }
    // Janela do dia em ISO (00:00 ate 23:59:59 local).
    const now = new Date();
    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).toISOString();
    const tomorrowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0).toISOString();

    try {
        const { data: sessao, error } = await tenantDb().from('agenda_eventos')
            .select('id, extra_fields, inicio_em')
            
            .eq('patient_id', pacienteId)
            .eq('tipo', 'sessao')
            .gte('inicio_em', todayStart)
            .lt('inicio_em', tomorrowStart)
            .order('inicio_em', { ascending: false })
            .limit(1)
            .maybeSingle();
        if (error) throw error;
        if (!sessao) {
            toast.add({
                severity: 'warn',
                summary: 'Sessão não encontrada',
                detail: 'Não há sessão agendada hoje pra este paciente — tempo não foi salvo.',
                life: 4500
            });
            return;
        }
        const newExtra = {
            ...(sessao.extra_fields && typeof sessao.extra_fields === 'object' ? sessao.extra_fields : {}),
            cronometro_duracao_seg: Math.round(elapsedSec),
            cronometro_parado_em: stoppedAt
        };
        const { error: upErr } = await tenantDb().from('agenda_eventos')
            .update({ extra_fields: newExtra })
            .eq('id', sessao.id);
        if (upErr) throw upErr;

        const min = Math.round(elapsedSec / 60);
        toast.add({
            severity: 'success',
            summary: 'Tempo registrado',
            detail: `${min} min cronometrados salvos na sessão.`,
            life: 3000
        });
    } catch (e) {
        toast.add({
            severity: 'error',
            summary: 'Falha ao salvar tempo',
            detail: e?.message || 'Tente novamente.',
            life: 4500
        });
    }
}

// Provide das prefs/refs pro MelissaConfiguracoes (página interna de
// configs). Posicionado aqui pra que TODAS as refs/funções referenciadas
// já estejam definidas no momento do setup. A página lê/escreve direto
// nas refs — watchers de persistência (localStorage + DB) ficam neste
// arquivo, mais abaixo.
provide('melissaSettings', {
    layoutConfig,
    isDarkTheme,
    activeSurface,
    PRIMARY_COLORS,
    SURFACES,
    PRESETS,
    setPrimary,
    setSurface,
    setPreset,
    setDark,
    // bg + opacidades + handler do input <type="file">
    bgUrl,
    overlayOpacity,
    bgImageOpacity,
    onFileChange,
    clearBg,
    // relógio
    use24h,
    // cronômetro
    toqueTermino,
    testarToque,
    // tema (bundle wallpaper + cores) — Freud/Klein/Jung
    themeName,
    setThemeName,
    // fundo nos textos do hero (relogio, saudacao, resumo)
    textBgEnabled,
    setTextBgEnabled
});

// ───────────────────────────────────────────────────────────────
// Persistência de prefs UI
// ───────────────────────────────────────────────────────────────
// Camadas:
//   1. localStorage — cache rápido pra evitar flash no boot e hold do bgUrl
//      (data URL pesada não vai pro DB)
//   2. user_settings.melissa_prefs (jsonb) — fonte de verdade pras prefs
//      pequenas (toque, opacidades, formato hora, cards). Sobrevive a
//      troca de navegador/dispositivo.
//
// Fluxo: onMounted → load localStorage (paint imediato) → load DB (sobrescreve
// se houver). Watch das refs → save localStorage (sync) + save DB (debounced).

// Sanitiza um payload de prefs (DB ou localStorage — ambos são input externo)
function applyPrefsPayload(prefs) {
    if (!prefs || typeof prefs !== 'object') return;

    if (typeof prefs.toqueTermino === 'string' && TOQUE_IDS.has(prefs.toqueTermino)) {
        toqueTermino.value = prefs.toqueTermino;
    }
    const op = Number(prefs.overlayOpacity);
    if (Number.isFinite(op) && op >= 0 && op <= 0.8) {
        overlayOpacity.value = op;
    }
    const bo = Number(prefs.bgImageOpacity);
    if (Number.isFinite(bo) && bo >= 0.01 && bo <= 1) {
        bgImageOpacity.value = bo;
    }
    if (typeof prefs.use24h === 'boolean') {
        use24h.value = prefs.use24h;
    }
    if (Array.isArray(prefs.cardsAtivos)) {
        const valid = prefs.cardsAtivos.filter((id) => typeof id === 'string' && CARD_IDS.has(id));
        if (valid.length > 0) cardsAtivos.value = valid;
    }
    if (prefs.cardsLayout === 'linha-unica' || prefs.cardsLayout === 'duas-linhas') {
        cardsLayout.value = prefs.cardsLayout;
    }
    if (typeof prefs.themeName === 'string' && MELISSA_THEME_NAMES.has(prefs.themeName)) {
        themeName.value = prefs.themeName;
    } else if (prefs.themeName === null) {
        themeName.value = null;
    }
    if (typeof prefs.textBgEnabled === 'boolean') {
        textBgEnabled.value = prefs.textBgEnabled;
    }
}

function currentPrefsSnapshot() {
    return {
        toqueTermino: toqueTermino.value,
        overlayOpacity: overlayOpacity.value,
        bgImageOpacity: bgImageOpacity.value,
        use24h: use24h.value,
        cardsAtivos: cardsAtivos.value,
        cardsLayout: cardsLayout.value,
        themeName: themeName.value,
        textBgEnabled: textBgEnabled.value
    };
}

function saveLayoutPrefs() {
    // localStorage inclui bgUrl (data URL); DB não — bgUrl fica local
    const prefs = { ...currentPrefsSnapshot(), bgUrl: bgUrl.value || '' };
    try {
        localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(prefs));
    } catch {
        // Quota provavelmente estourou por causa do bgUrl (data URL grande).
        // Tenta sem ele — as outras prefs são minúsculas.
        try {
            localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify({ ...prefs, bgUrl: '' }));
        } catch {}
    }
}

function loadLocalPrefs() {
    let raw;
    try { raw = localStorage.getItem(LAYOUT_STORAGE_KEY); } catch { return; }
    if (!raw) return;

    let prefs;
    try { prefs = JSON.parse(raw); } catch { return; }
    if (!prefs || typeof prefs !== 'object') return;

    applyPrefsPayload(prefs);

    // bgUrl: aceita só data URL de imagem (evita injeção de URL externa via storage)
    if (typeof prefs.bgUrl === 'string' && prefs.bgUrl.startsWith('data:image/')) {
        bgUrl.value = prefs.bgUrl;
    }
}

// ── DB sync ────────────────────────────────────────────────────
let dbSaveTimer = null;
let dbReady = false; // só salva no DB depois do load inicial (evita sobrescrever c/ defaults)

async function loadDbPrefs() {
    try {
        const { data: u } = await supabase.auth.getUser();
        const uid = u?.user?.id;
        if (!uid) return;

        const { data, error } = await supabase
            .from('user_settings')
            .select('melissa_prefs')
            .eq('user_id', uid)
            .maybeSingle();
        if (error) return;
        if (data?.melissa_prefs && typeof data.melissa_prefs === 'object') {
            applyPrefsPayload(data.melissa_prefs);
        }
    } catch {
        // silencioso — falha de rede/auth não pode quebrar a UI
    } finally {
        dbReady = true;
    }
}

async function saveDbPrefs() {
    if (!dbReady) return;
    try {
        const { data: u } = await supabase.auth.getUser();
        const uid = u?.user?.id;
        if (!uid) return;

        await supabase
            .from('user_settings')
            .upsert(
                { user_id: uid, melissa_prefs: currentPrefsSnapshot(), updated_at: new Date().toISOString() },
                { onConflict: 'user_id' }
            );
    } catch {
        // silencioso — pref save não-crítico; localStorage segura o estado até voltar
    }
}

// Quando themeName carregou mas bgUrl ainda esta vazio (ex: 1a vez em outro
// device), resolve a imagem do tema e gera a data URL. Idempotente: se ja
// tem bgUrl, nao mexe (custom upload ou data URL ja restaurada do storage).
async function resolveThemeWallpaperIfNeeded() {
    if (!themeName.value) return;
    if (bgUrl.value) return;
    const t = findMelissaTheme(themeName.value);
    if (!t) return;
    try {
        const res = await fetch(t.image);
        const blob = await res.blob();
        const dataUrl = await new Promise((resolve, reject) => {
            const r = new FileReader();
            r.onload = () => resolve(r.result);
            r.onerror = reject;
            r.readAsDataURL(blob);
        });
        bgUrl.value = dataUrl;
    } catch {
        bgUrl.value = t.image; // fallback: URL direta funciona na sessao
    }
}

function queueDbSave() {
    if (dbSaveTimer) clearTimeout(dbSaveTimer);
    dbSaveTimer = setTimeout(saveDbPrefs, 600);
}

// Salva em qualquer mudança das prefs (deep no array de cardsAtivos pra pegar splice/push)
watch(
    [toqueTermino, overlayOpacity, bgImageOpacity, use24h, bgUrl, cardsAtivos, cardsLayout, themeName, textBgEnabled],
    () => {
        saveLayoutPrefs();
        queueDbSave();
    },
    { deep: true }
);

// Carrega antes do paint (use onMounted; o flash inicial é aceitável)
onMounted(async () => {
    loadLocalPrefs();   // sync: paint imediato com valores cached
    await loadDbPrefs(); // async: sobrescreve com valores autoritativos do DB
    // Se o user logou em outro device, themeName vem do DB mas bgUrl
    // (data URL) nao — resolvemos a imagem do tema agora.
    await resolveThemeWallpaperIfNeeded();
});

// Auto-scroll inicial + ResizeObserver da timeline migrou pro
// componente MelissaTimelineHoje (agora self-contained).

// ───────────────────────────────────────────────────────────────
// Workspace overlay
// ───────────────────────────────────────────────────────────────
const workspaceOpen = ref(false);

function openWorkspace() {
    workspaceOpen.value = true;
}
function closeWorkspace() {
    workspaceOpen.value = false;
}
function toggleWorkspace() {
    workspaceOpen.value ? closeWorkspace() : openWorkspace();
}

// Resumo recebe blur quando qualquer overlay tá aberto (cronômetro minimizado NÃO conta)
const summaryDimmed = computed(
    () => workspaceOpen.value
        || cronoVisible.value
        || !!eventoSelecionado.value
        || !!secaoAberta.value
        || centralOpen.value
);

function onKeydown(e) {
    // Ctrl+\ ou ⌘+\ → abre/fecha workspace (psi-btn)
    if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
        e.preventDefault();
        toggleWorkspace();
        return;
    }

    if (e.key !== 'Escape') return;
    // Bail-out: se há um overlay PrimeVue (Dialog/Drawer) aberto, deixa
    // o componente cuidar do ESC pelo seu próprio closeOnEscape. Sem
    // este guard, o ESC fechava o overlay E uma camada do cascade —
    // o usuário via "duas janelas fechando" (drawer WhatsApp + agenda,
    // AgendaEventDialog + evento panel, ConfirmDialog + workspace, etc).
    if (document.querySelector('.p-dialog-mask, .p-drawer-mask')) return;

    // Cascata top-down do z-order — ESC fecha SOMENTE a camada do topo.
    // Ordem (mais sobreposto → menos): central modal > evento panel >
    // cronômetro > seção (agenda/pacientes/etc) > workspace > settings.
    if (centralOpen.value) centralOpen.value = false;
    else if (eventoSelecionado.value) fecharEvento();
    else if (cronoVisible.value) fecharCronometro();
    else if (secaoAberta.value) fecharSecao();
    else if (workspaceOpen.value) closeWorkspace();
    else if (settingsOpen.value) settingsOpen.value = false;
}

// modulos[] removido — MelissaMenu agora tem catálogo próprio (CATEGORIAS)
</script>

<template>
    <div class="win11-root" :class="{ 'win11-has-photo': !!bgUrl }" :style="defaultBgStyle">
        <!-- Camada da foto custom (acima do gradiente, abaixo do dim).
             Opacidade controlada pelo slider — permite blend com o gradiente
             default que vive no .win11-root. -->
        <div v-if="bgUrl" class="win11-photo" :style="photoStyle" />

        <!-- Overlay escurecedor (controlado pelo slider) -->
        <div class="win11-dim" :style="{ backgroundColor: `rgba(var(--m-dim-rgb), ${overlayOpacity})` }" />

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- PLANO DE TRÁS — Resumo (recebe blur quando workspace abre) -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <div class="win11-summary" :class="{ 'is-behind': summaryDimmed }">
            <!-- Conteúdo central -->
            <div class="win11-summary__inner">
                <!-- Bloco hero: relógio + data + saudação + resumo do dia -->
                <MelissaHeroClock
                    :hora="horaFormatada"
                    :data-extenso="dataExtenso"
                    :saudacao="saudacao"
                    :resumo-partes="resumoPartes"
                    :filtro-tipo="filtroTipo"
                    :text-bg="textBgEnabled"
                    @cronometro="abrirCronometro"
                    @toggle-filtro="toggleFiltro"
                />

                <!-- Busca rápida (entre o "Hoje há" e a timeline) -->
                <MelissaBusca
                    ref="melissaBuscaRef"
                    class="mt-8"
                    :pacientes="pacientesReais"
                    :eventos="eventosHojeReais"
                    @acao="abrirSecao"
                    @paciente="(p) => p?.id ? router.push({ path: '/melissa/paciente', query: { id: String(p.id) } }) : abrirSecao('pacientes')"
                    @evento="abrirEvento"
                    @documento="(d) => d?.patient_id ? router.push({ path: '/melissa/paciente', query: { id: String(d.patient_id), tab: 'documentos' } }) : abrirSecao('pacientes')"
                    @intake="() => abrirSecao('cadastros-recebidos')"
                    @goto-date="onBuscaGotoDate"
                />

                <!-- Timeline horizontal + vertical (responsivo) -->
                <MelissaTimelineHoje
                    :eventos="eventosHojeReais"
                    :now="now"
                    :work-rules="agendaWorkRules"
                    :agenda-settings="agendaSettings"
                    :feriados="agendaFeriados"
                    :filtro-tipo="filtroTipo"
                    @evento="abrirEvento"
                    @clear-filter="limparFiltro"
                    @iniciar-cronometro="onIniciarCronometroFromEvento"
                />

                <!-- Cards (catálogo + ativos + layout switchável) -->
                <section
                    class="cards-shell mt-5 w-full mx-auto"
                    :class="{ 'is-wrap': cardsLayout === 'duas-linhas' }"
                >
                    <div
                        class="cards-row"
                        :class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'"
                    >
                    <template v-for="cardId in cardsAtivos" :key="cardId">
                        <!-- Próximo paciente. Se o evento esta em curso E tem
                             paciente, action vira "Iniciar cronômetro" pra
                             facilitar o fluxo "estou comecando a sessao agora". -->
                        <MelissaCard
                            v-if="cardId === 'proximo-paciente'"
                            icon="pi pi-user"
                            icon-color="text-emerald-300"
                            title="Próximo paciente"
                            :action-title="proximoPaciente
                                ? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
                                    ? 'Iniciar cronômetro'
                                    : 'Abrir sessão')
                                : 'Abrir Pacientes'"
                            @open="proximoPaciente
                                ? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
                                    ? onIniciarCronometroFromEvento(proximoPaciente.ev)
                                    : abrirEvento(proximoPaciente.ev))
                                : abrirSecao('pacientes')"
                        >
                            <div v-if="proximoPaciente" class="flex items-center gap-3">
                                <div
                                    class="w-12 h-12 rounded-full grid place-items-center text-white font-semibold border"
                                    :class="proximoPaciente.emCurso
                                        ? 'bg-emerald-500/40 border-emerald-300/60 ring-2 ring-emerald-400/40 ring-offset-0 animate-pulse'
                                        : 'bg-emerald-500/30 border-emerald-300/40'"
                                >
                                    {{ proximoPaciente.iniciais }}
                                </div>
                                <div class="min-w-0">
                                    <div class="text-white font-medium truncate">{{ proximoPaciente.nome }}</div>
                                    <div class="text-white/60 text-xs">
                                        {{ proximoPaciente.horario }} · {{ proximoPaciente.statusLabel }}
                                    </div>
                                </div>
                            </div>
                            <div v-else class="flex items-center gap-3">
                                <div class="w-12 h-12 rounded-full bg-white/5 border border-white/10 grid place-items-center text-white/40">
                                    <i class="pi pi-check text-base" />
                                </div>
                                <div class="min-w-0">
                                    <div class="text-white/75 text-sm font-medium">Sem atendimentos pendentes</div>
                                    <div class="text-white/45 text-xs">Tudo certo por hoje</div>
                                </div>
                            </div>
                        </MelissaCard>

                        <!-- WhatsApp -->
                        <MelissaCard
                            v-else-if="cardId === 'whatsapp'"
                            icon="pi pi-whatsapp"
                            icon-color="text-green-300"
                            title="WhatsApp"
                            :badge="whatsappPendente.count || null"
                            action-title="Abrir WhatsApp"
                            @open="abrirSecao('conversas')"
                        >
                            <template v-if="whatsappPendente.count > 0">
                                <div class="text-white/85 text-xs italic line-clamp-2 leading-relaxed">
                                    {{ whatsappPendente.ultimaMsg || '(mensagem sem texto)' }}
                                </div>
                                <div class="text-white/50 text-[0.7rem] mt-1">— {{ whatsappPendente.ultimoNome }}</div>
                            </template>
                            <template v-else>
                                <div class="text-white/75 text-sm font-medium">Caixa de entrada limpa</div>
                                <div class="text-white/45 text-[0.7rem] mt-1">Sem mensagens não lidas.</div>
                            </template>
                        </MelissaCard>

                        <!-- Recebíveis -->
                        <MelissaCard
                            v-else-if="cardId === 'recebiveis'"
                            icon="pi pi-wallet"
                            icon-color="text-amber-300"
                            title="Recebíveis hoje"
                            action-title="Abrir Financeiro"
                            @open="abrirSecao('financeiro')"
                        >
                            <template v-if="recebiveis.hasPrice">
                                <div class="flex items-baseline gap-1.5">
                                    <span class="text-white text-xl font-medium">{{ brl(recebiveis.recebido) }}</span>
                                    <span class="text-white/50 text-xs">/ {{ brl(recebiveis.previsto) }}</span>
                                </div>
                                <div class="h-1.5 rounded-full bg-white/10 mt-2 overflow-hidden">
                                    <div
                                        class="h-full bg-gradient-to-r from-amber-400 to-amber-300 transition-all"
                                        :style="{ width: `${recebidoPct}%` }"
                                    />
                                </div>
                                <div class="text-white/55 text-[0.7rem] mt-1.5">
                                    {{ recebiveis.sessoesPagas }}/{{ recebiveis.sessoes }} sessões pagas
                                </div>
                            </template>
                            <template v-else-if="recebiveis.sessoes > 0">
                                <div class="text-white/75 text-sm font-medium">
                                    {{ recebiveis.sessoes }} {{ recebiveis.sessoes === 1 ? 'sessão' : 'sessões' }} sem valor
                                </div>
                                <div class="text-white/45 text-[0.7rem] mt-1">
                                    Defina o preço nas sessões pra acompanhar.
                                </div>
                            </template>
                            <template v-else>
                                <div class="text-white/75 text-sm font-medium">Sem cobranças hoje</div>
                                <div class="text-white/45 text-[0.7rem] mt-1">Nenhuma sessão agendada.</div>
                            </template>
                        </MelissaCard>

                        <!-- Card "em breve" — placeholder pra ids do catálogo ainda não
                             implementados (inclui Copilot enquanto não há backend de IA) -->
                        <MelissaCard
                            v-else
                            :icon="getCatalogItem(cardId)?.icon || 'pi pi-clock'"
                            icon-color="text-white/40"
                            :title="getCatalogItem(cardId)?.label || cardId"
                            action-title="Em breve"
                            @open="abrirSecao('configuracoes')"
                        >
                            <div class="text-white/40 text-xs italic">Em breve</div>
                        </MelissaCard>
                    </template>

                    <!-- Em wrap: "+" segue o fluxo, ao final dos cards, com altura
                         espelhada (flex align-items: stretch faz isso natural) -->
                    <MelissaCard
                        v-if="cardsLayout === 'duas-linhas'"
                        variant="add"
                        action-title="Personalizar cards"
                        @add="centralOpen = true"
                    />
                    </div>

                    <!-- Em linha única: "+" fora do scroll, sempre visível à direita -->
                    <MelissaCard
                        v-if="cardsLayout === 'linha-unica'"
                        variant="add"
                        action-title="Personalizar cards"
                        @add="centralOpen = true"
                    />
                </section>
            </div>

        </div>
        <!-- /win11-summary fecha aqui -->

        <!-- Veu de blur: 1 layer unica com backdrop-filter por cima do resumo,
             em vez de aplicar filter:blur na arvore inteira do .win11-summary
             (5+ glass-panels com backdrop-filter proprio = layer explosion).
             Custo: 1 backdrop pass por frame, contra N×N. opacity-only
             transition + contain:strict isola repaints. -->
        <div class="win11-blur-veil" :class="{ 'is-active': summaryDimmed }" aria-hidden="true" />

        <!-- Botão único ψ — FORA da summary (não recebe blur/pointer-none
             quando overlays estão abertos; permanece sempre clicável).
             Continua position:absolute (não é filho do dock), mas vive na
             mesma faixa visual da .melissa-dock (ambos no bottom). -->
        <button
            class="psi-btn"
            :title="workspaceOpen ? 'Fechar workspace (Ctrl + \\)' : 'Abrir workspace (Ctrl + \\)'"
            @click="toggleWorkspace"
        >
            <span class="psi-glyph">ψ</span>
            <span class="psi-kbd" aria-hidden="true">Ctrl \</span>
        </button>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- TRAY Melissa (system tray win11-style, canto inf. direito) -->
        <!-- busca + plan-DEV + notificações + ajuda + cog. Sibling de  -->
        <!-- .dock (fora de .win11-summary) pra ficar sempre interativo,-->
        <!-- mesmo com secao aberta (que aplica blur+pointer-none na    -->
        <!-- summary). AjudaDrawer ja e global no App.vue.              -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <div class="melissa-tray">
            <!-- Busca global (Ctrl+K) — afordancia visivel pra mouse/touch.
                 Centraliza o ponto de acesso entre seccoes (em vez de
                 cada toolbar ter o proprio botao). -->
            <button
                class="glass-btn w-10 h-10 grid place-items-center"
                v-tooltip.top="'Buscar (Ctrl+K)'"
                aria-label="Busca global"
                @click="melissaBuscaRef?.openDialog?.()"
            >
                <i class="pi pi-search text-white/90 text-base" />
            </button>

            <!-- Plan switcher DEV (só aparece em dev / com flag).
                 Em mobile (<md) some — vai pro popup do "more". -->
            <button
                v-if="showPlanDevMenu"
                ref="planBtn"
                class="glass-btn w-10 h-10 hidden md:grid place-items-center"
                :disabled="planMenuLoading || trocandoPlano"
                v-tooltip.top="'Plano (DEV)'"
                @click="openPlanMenu"
            >
                <i v-if="planMenuLoading || trocandoPlano" class="pi pi-spin pi-spinner text-white/90 text-base" />
                <i v-else class="pi pi-sliders-h text-white/90 text-base" />
            </button>

            <!-- Notificações — em mobile (<md) some, vai pro popup. -->
            <button
                class="glass-btn w-10 h-10 hidden md:grid place-items-center relative"
                v-tooltip.top="'Notificações'"
                @click="notificationStore.drawerOpen = true"
            >
                <i class="pi pi-bell text-white/90 text-base" />
                <span
                    v-if="notificationStore.unreadCount > 0"
                    class="m-topbar-badge"
                >{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
            </button>

            <!-- Ajuda (conteudo muda com a rota via AjudaDrawer global).
                 data-ajuda-toggle: marca pro AjudaDrawer ignorar este botao
                 na deteccao de "clicou fora pra fechar" (senao fecha aqui
                 e o @click reabre). Em mobile (<md) some, vai pro popup. -->
            <button
                class="glass-btn w-10 h-10 hidden md:grid place-items-center"
                :class="{ 'glass-btn--active': ajudaDrawerOpen }"
                v-tooltip.top="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
                data-ajuda-toggle
                @click="toggleAjuda"
            >
                <i class="pi pi-question-circle text-white/90 text-base" />
            </button>

            <!-- Cog (settings popover abre pra CIMA — vide MelissaSettingsPanel).
                 Em mobile (<md) some, vai pro popup. -->
            <button
                ref="cogBtnEl"
                class="glass-btn w-10 h-10 hidden md:grid place-items-center"
                v-tooltip.top="'Personalizar'"
                @click="settingsOpen = !settingsOpen"
            >
                <i class="pi pi-cog text-white/90 text-base" />
            </button>

            <Transition name="settings-pop">
                <MelissaSettingsPanel
                    v-if="settingsOpen"
                    @close="settingsOpen = false"
                />
            </Transition>

            <!-- "More" — so em mobile (<md). Collapse de plan-DEV+bell+help+
                 cog num popup vertical. Dot vermelho aparece se houver
                 notificacoes nao-lidas (preserva o sinal visual da bell). -->
            <button
                ref="trayMoreBtnEl"
                class="glass-btn w-10 h-10 grid md:hidden place-items-center relative"
                :class="{ 'glass-btn--active': trayMoreOpen }"
                v-tooltip.top="trayMoreOpen ? 'Fechar' : 'Mais'"
                aria-label="Mais opcoes"
                @click="trayMoreOpen = !trayMoreOpen"
            >
                <i class="pi pi-ellipsis-v text-white/90 text-base" />
                <span
                    v-if="notificationStore.unreadCount > 0"
                    class="melissa-tray__more-dot"
                    aria-hidden="true"
                />
            </button>

            <Transition name="settings-pop">
                <div v-if="trayMoreOpen" class="melissa-tray__more-panel glass-panel">
                    <!-- Notificacoes -->
                    <button
                        class="melissa-tray__more-item"
                        @click="notificationStore.drawerOpen = true; trayMoreOpen = false"
                    >
                        <i class="pi pi-bell" />
                        <span class="flex-1 text-left">Notificações</span>
                        <span
                            v-if="notificationStore.unreadCount > 0"
                            class="m-topbar-badge"
                            style="position: static; transform: none;"
                        >{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
                    </button>

                    <!-- Ajuda -->
                    <button
                        class="melissa-tray__more-item"
                        :class="{ 'is-active': ajudaDrawerOpen }"
                        data-ajuda-toggle
                        @click="toggleAjuda(); trayMoreOpen = false"
                    >
                        <i class="pi pi-question-circle" />
                        <span class="flex-1 text-left">{{ ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda' }}</span>
                    </button>

                    <!-- Personalizar (cog) -->
                    <button
                        class="melissa-tray__more-item"
                        @click="settingsOpen = !settingsOpen; trayMoreOpen = false"
                    >
                        <i class="pi pi-cog" />
                        <span class="flex-1 text-left">Personalizar</span>
                    </button>

                    <!-- Plano DEV (so se flag) -->
                    <button
                        v-if="showPlanDevMenu"
                        class="melissa-tray__more-item"
                        :disabled="planMenuLoading || trocandoPlano"
                        @click="openPlanMenu($event); trayMoreOpen = false"
                    >
                        <i :class="planMenuLoading || trocandoPlano ? 'pi pi-spin pi-spinner' : 'pi pi-sliders-h'" />
                        <span class="flex-1 text-left">Plano (DEV)</span>
                    </button>
                </div>
            </Transition>
        </div>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- DOCK (taskbar Win11-style sem chrome) — receptáculo pra  -->
        <!-- elementos minimizados (cronômetro, futuros) + atalhos    -->
        <!-- pinned (Agenda, Pacientes, WhatsApp, Financeiro). Transp.-->
        <!-- só os items sao clicaveis. ψ vive ao lado (absolute,     -->
        <!-- bottom-left).                                            -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <div class="melissa-dock">
            <!-- Pinned builtins: 4 atalhos pras secoes principais.
                 Tamanho menor que ψ (44px vs 56px) + cantos arredondados
                 mas não full-circle, pra hierarquia visual ficar óbvia. -->
            <button
                type="button"
                class="dock-pin"
                v-tooltip.top="'Agenda'"
                :class="{ 'dock-pin--active': secaoAberta === 'agenda' }"
                @click="abrirSecao('agenda')"
            >
                <i class="pi pi-calendar" />
            </button>
            <button
                type="button"
                class="dock-pin"
                v-tooltip.top="'Pacientes'"
                :class="{ 'dock-pin--active': secaoAberta === 'pacientes' }"
                @click="abrirSecao('pacientes')"
            >
                <i class="pi pi-users" />
            </button>
            <button
                type="button"
                class="dock-pin"
                v-tooltip.top="'WhatsApp'"
                :class="{ 'dock-pin--active': secaoAberta === 'conversas' }"
                @click="abrirSecao('conversas')"
            >
                <i class="pi pi-whatsapp" />
                <span
                    v-if="whatsappPendente.count > 0"
                    class="dock-pin__badge"
                    :title="`${whatsappPendente.count} mensagens não lidas`"
                >{{ whatsappPendente.count > 99 ? '99+' : whatsappPendente.count }}</span>
            </button>
            <button
                type="button"
                class="dock-pin"
                v-tooltip.top="'Financeiro'"
                :class="{ 'dock-pin--active': secaoAberta === 'financeiro' }"
                @click="abrirSecao('financeiro')"
            >
                <i class="pi pi-wallet" />
            </button>

            <!-- Divisor entre builtins e pins dinâmicos. Só aparece se
                 o user tem pelo menos 1 pin (fixo ou recente).
                 Em mobile (<md), se so houver MRU (que e oculto), o
                 divisor tambem some pra nao "soltar" no fim do dock. -->
            <div
                v-if="dockPins.pinned.value.length || dockPins.recent.value.length"
                class="dock-divider"
                :class="{ 'hidden md:block': !dockPins.pinned.value.length }"
                aria-hidden="true"
            />

            <!-- Pins fixados pelo user (max 4). Click direito → menu
                 desafixar/remover. Hover mostra subtle ring. -->
            <button
                v-for="slug in dockPins.pinned.value" :key="`p-${slug}`"
                type="button"
                class="dock-pin dock-pin--user"
                :class="{ 'dock-pin--active': secaoAberta === slug }"
                v-tooltip.top="pinMeta(slug).label + ' (fixado)'"
                @click="abrirSecao(slug)"
                @contextmenu="openPinContextMenu($event, slug)"
            >
                <i :class="pinMeta(slug).icon" />
                <span class="dock-pin__pinned-mark" aria-hidden="true" />
            </button>

            <!-- Pins MRU (max 3) — empurrados pelas últimas seções abertas.
                 Visual mais leve (opacity menor) pra destacar dos fixos.
                 Em mobile (<md=768px) sao ocultos via media query no CSS
                 do .dock-pin--recent — utility 'hidden' do Tailwind perde
                 pro 'display: grid' base do .dock-pin (mesma specificity,
                 ordem de carga). -->
            <button
                v-for="slug in dockPins.recent.value" :key="`r-${slug}`"
                type="button"
                class="dock-pin dock-pin--recent"
                :class="{ 'dock-pin--active': secaoAberta === slug }"
                v-tooltip.top="pinMeta(slug).label + ' (recente — clique direito pra fixar)'"
                @click="abrirSecao(slug)"
                @contextmenu="openPinContextMenu($event, slug)"
            >
                <i :class="pinMeta(slug).icon" />
            </button>

            <!-- Menu de contexto dos pins dinâmicos (popup global) -->
            <Menu ref="pinContextMenu" :model="pinContextMenuItems" :popup="true" />
        </div>


        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- MENU FLOAT (Win11-style Start) — abre via ψ ou Ctrl+\ -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <Transition name="menu-rise">
            <MelissaMenu
                v-if="workspaceOpen"
                :secao-ativa="secaoAberta"
                @select="abrirSecao"
                @close="closeWorkspace"
                @open-settings="onMenuOpenSettings"
            />
        </Transition>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- DIALOG — Evento da timeline (sessão / supervisão / reunião) -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <Transition name="lift">
            <MelissaEventoPanel
                v-if="eventoSelecionado"
                :evento="eventoSelecionado"
                :busy="eventoBusy"
                @close="fecharEvento"
                @agendar="onAgendar"
                @concluir="onConcluir"
                @faltou="onFaltou"
                @cancelar="onCancelar"
                @remarcar="onRemarcar"
                @edit-sessao="onEditEvento"
                @delete-sessao="onDeleteEvento"
                @delete-series="onDeleteSeries"
                @gerar-cobranca="onGerarCobrancaQuick"
                @usar-sessao="onUsarSessao"
                @revogar-sessao="onRevogarSessao"
                @ver-lancamentos="onVerLancamentos"
                @antecipar-pagamento="onAnteciparPagamento"
                @trocar-metodo-antecipacao="onTrocarMetodoAntecipacao"
                @revogar-antecipacao="onRevogarAntecipacao"
                @edit-paciente="onEditPaciente"
                @abrir-prontuario="onAbrirProntuario"
                @whatsapp="onWhatsapp"
                @historico="onHistoricoSessoes"
            />
        </Transition>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- CENTRAL DE CARDS — toggle de cards + modo de visualização -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <Transition name="lift">
            <div
                v-if="centralOpen"
                class="central-layer"
                @click.self="centralOpen = false"
            >
                <div class="central-panel">
                    <header class="flex items-start justify-between mb-5">
                        <div>
                            <div class="text-white/55 text-xs uppercase tracking-[0.2em]">Central de cards</div>
                            <div class="text-white text-xl font-light mt-1">Personalize seu resumo</div>
                        </div>
                        <button
                            class="glass-btn w-9 h-9 grid place-items-center shrink-0"
                            title="Fechar (Esc)"
                            @click="centralOpen = false"
                        >
                            <i class="pi pi-times text-white/90 text-sm" />
                        </button>
                    </header>

                    <!-- Modo de visualização -->
                    <div class="mb-5">
                        <div class="text-[0.65rem] uppercase tracking-widest text-white/50 mb-2">Visualização</div>
                        <div class="flex flex-col gap-2">
                            <label class="central-radio">
                                <input type="radio" v-model="cardsLayout" value="linha-unica" />
                                <span>Linha única <span class="text-white/40">(scroll horizontal)</span></span>
                            </label>
                            <label class="central-radio">
                                <input type="radio" v-model="cardsLayout" value="duas-linhas" />
                                <span>Duas linhas <span class="text-white/40">(quebra automática)</span></span>
                            </label>
                        </div>
                    </div>

                    <!-- Cards disponíveis -->
                    <div>
                        <div class="text-[0.65rem] uppercase tracking-widest text-white/50 mb-2">
                            Cards disponíveis
                        </div>
                        <div class="flex flex-col gap-1">
                            <label
                                v-for="c in CARDS_CATALOG"
                                :key="c.id"
                                class="central-card-row"
                                :class="{ 'is-em-breve': !c.implementado }"
                            >
                                <input
                                    type="checkbox"
                                    :checked="cardsAtivos.includes(c.id)"
                                    @change="toggleCardAtivo(c.id)"
                                />
                                <i :class="c.icon" class="text-white/70 w-4 text-center" />
                                <span class="flex-1">{{ c.label }}</span>
                                <span v-if="!c.implementado" class="text-[0.65rem] text-white/40">em breve</span>
                            </label>
                        </div>
                    </div>
                </div>
            </div>
        </Transition>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- SESSÕES "promovidas" — páginas fullscreen dentro de Melissa -->
        <!-- (não usam o dialog placeholder, viram páginas próprias) -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- Sem <Transition> wrapper aqui: o leave delay criava janela onde
             os <Teleport> internos (.melissa-dock, #ma-mobile-drawer-target)
             ficavam com placeholders órfãos no target compartilhado, e o
             próximo patch lia .component anulado → "Cannot set properties of
             null (setting '__vnode')". A animação de entrada vive como
             keyframe em .ma-page / .mp-page nos próprios componentes. -->
        <MelissaAgenda
            v-if="layoutReady && secaoAberta === 'agenda'"
            ref="melissaAgendaRef"
            :pacientes="pacientesReais"
            :pacientes-loading="pacientesLoading"
            @select-evento="abrirEvento"
            @close="fecharSecao"
            @patient-created="refetchPacientes"
        />

        <MelissaPacientes
            v-if="layoutReady && secaoAberta === 'pacientes'"
            @close="fecharSecao"
            @patient-created="refetchPacientes"
            @goto-agenda="abrirSecao('agenda')"
            @goto-grupos="abrirSecao('grupos')"
            @goto-tags="abrirSecao('tags')"
        />

        <MelissaCompromissos
            v-if="layoutReady && secaoAberta === 'compromissos'"
            @close="fecharSecao"
        />

        <MelissaRecorrencias
            v-if="layoutReady && secaoAberta === 'recorrencias'"
            @close="fecharSecao"
        />

        <MelissaConversas
            v-if="layoutReady && secaoAberta === 'conversas'"
            @close="fecharSecao"
        />

        <MelissaTags
            v-if="layoutReady && secaoAberta === 'tags'"
            @close="fecharSecao"
        />

        <MelissaGrupos
            v-if="layoutReady && secaoAberta === 'grupos'"
            @close="fecharSecao"
        />

        <MelissaCadastrosRecebidos
            v-if="layoutReady && secaoAberta === 'cadastros-recebidos'"
            @close="fecharSecao"
        />

        <MelissaAgendamentosRecebidos
            v-if="layoutReady && secaoAberta === 'agendamentos-recebidos'"
            @close="fecharSecao"
        />

        <MelissaMedicos
            v-if="layoutReady && secaoAberta === 'medicos'"
            @close="fecharSecao"
        />

        <MelissaLinkExterno
            v-if="layoutReady && secaoAberta === 'link-externo'"
            @close="fecharSecao"
        />

        <MelissaNotificacoes
            v-if="layoutReady && secaoAberta === 'notificacoes'"
            @close="fecharSecao"
        />

        <MelissaFinanceiro
            v-if="layoutReady && secaoAberta === 'financeiro'"
            @close="fecharSecao"
        />

        <MelissaFinanceiroLancamentos
            v-if="layoutReady && secaoAberta === 'financeiro-lancamentos'"
            @close="fecharSecao"
        />

        <MelissaDocumentos
            v-if="layoutReady && secaoAberta === 'documentos'"
            @close="fecharSecao"
        />

        <MelissaDocumentosTemplates
            v-if="layoutReady && secaoAberta === 'documentos-templates'"
            @close="fecharSecao"
        />

        <MelissaRelatorios
            v-if="layoutReady && secaoAberta === 'relatorios'"
            @close="fecharSecao"
        />

        <MelissaPerfil
            v-if="layoutReady && secaoAberta === 'perfil'"
            @close="fecharSecao"
        />

        <!-- Pagina nativa do prontuario do paciente (Fase 8 wire-up).
             ID vem via route.query.id (?id=xxx). MelissaPaciente cuida
             internamente das acoes (close -> /melissa/pacientes; edit ->
             /melissa/pacientes?edit=<id>; open-whatsapp -> conversationDrawerStore). -->
        <MelissaPaciente
            v-if="layoutReady && secaoAberta === 'paciente'"
            :patient-id="String(route.query.id || '')"
            @close="fecharSecao"
        />

        <MelissaPlano
            v-if="layoutReady && secaoAberta === 'plano'"
            @close="fecharSecao"
        />

        <MelissaNegocio
            v-if="layoutReady && secaoAberta === 'negocio'"
            @close="fecharSecao"
        />

        <MelissaAlterarPlano
            v-if="layoutReady && secaoAberta === 'alterar-plano'"
            @close="fecharSecao"
            @goto="abrirSecao"
        />

        <MelissaSeguranca
            v-if="layoutReady && secaoAberta === 'seguranca'"
            @close="fecharSecao"
        />

        <MelissaBloqueios
            v-if="layoutReady && secaoAberta === 'bloqueios'"
            @close="fecharSecao"
        />

        <MelissaAgendador
            v-if="layoutReady && secaoAberta === 'online-scheduling'"
            @close="fecharSecao"
        />

        <MelissaAgendaConfig
            v-if="layoutReady && secaoAberta === 'agenda-config'"
            @close="fecharSecao"
        />

        <MelissaPagamento
            v-if="layoutReady && secaoAberta === 'pagamento'"
            @close="fecharSecao"
        />

        <MelissaPrecificacao
            v-if="layoutReady && secaoAberta === 'cfg-precificacao'"
            @close="fecharSecao"
        />

        <MelissaDescontos
            v-if="layoutReady && secaoAberta === 'cfg-descontos'"
            @close="fecharSecao"
        />

        <MelissaExcecoes
            v-if="layoutReady && secaoAberta === 'cfg-excecoes'"
            @close="fecharSecao"
        />

        <MelissaConvenios
            v-if="layoutReady && secaoAberta === 'cfg-convenios'"
            @close="fecharSecao"
        />

        <MelissaCfgWa
            v-if="layoutReady && secaoAberta === 'cfg-wa'"
            @close="fecharSecao"
        />
        <MelissaCfgWaPessoal
            v-if="layoutReady && secaoAberta === 'cfg-wa-pessoal'"
            @close="fecharSecao"
        />
        <MelissaCfgWaOficial
            v-if="layoutReady && secaoAberta === 'cfg-wa-oficial'"
            @close="fecharSecao"
        />
        <MelissaCfgWaTemplates
            v-if="layoutReady && secaoAberta === 'cfg-wa-templates'"
            @close="fecharSecao"
        />
        <MelissaCfgConversasTags
            v-if="layoutReady && secaoAberta === 'cfg-conversas-tags'"
            @close="fecharSecao"
        />
        <MelissaCfgConversasAutoreply
            v-if="layoutReady && secaoAberta === 'cfg-conversas-autoreply'"
            @close="fecharSecao"
        />
        <MelissaCfgConversasOptouts
            v-if="layoutReady && secaoAberta === 'cfg-conversas-optouts'"
            @close="fecharSecao"
        />
        <MelissaCfgConversasSla
            v-if="layoutReady && secaoAberta === 'cfg-conversas-sla'"
            @close="fecharSecao"
        />
        <MelissaCfgConversasBots
            v-if="layoutReady && secaoAberta === 'cfg-conversas-bots'"
            @close="fecharSecao"
        />
        <MelissaCfgLembretes
            v-if="layoutReady && secaoAberta === 'cfg-lembretes'"
            @close="fecharSecao"
        />
        <MelissaCfgCreditosWa
            v-if="layoutReady && secaoAberta === 'cfg-creditos-wa'"
            @close="fecharSecao"
        />
        <MelissaCfgSms
            v-if="layoutReady && secaoAberta === 'cfg-sms'"
            @close="fecharSecao"
        />
        <MelissaCfgEmailTemplates
            v-if="layoutReady && secaoAberta === 'cfg-email-templates'"
            @close="fecharSecao"
        />
        <MelissaCfgRecursosExtras
            v-if="layoutReady && secaoAberta === 'cfg-recursos-extras'"
            @close="fecharSecao"
        />
        <MelissaCfgRecursosExtrasExtrato
            v-if="layoutReady && secaoAberta === 'cfg-recursos-extras-extrato'"
            @close="fecharSecao"
        />
        <MelissaCfgAuditoria
            v-if="layoutReady && secaoAberta === 'cfg-auditoria'"
            @close="fecharSecao"
        />

        <MelissaConfiguracoes
            v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
            :secao-rota="secaoAberta"
            @close="fecharSecao"
        />

        <MelissaEmbed
            v-if="layoutReady && MELISSA_EMBED_KEYS.includes(secaoAberta)"
            :secao-rota="secaoAberta"
            @close="fecharSecao"
        />

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- SEÇÃO — placeholder dialog pras sessões ainda não promovidas -->
        <!-- (WhatsApp, Financeiro, Copilot...) -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <Transition name="lift">
            <div
                v-if="secaoAberta && !MELISSA_NON_CONFIG_SLUGS.has(secaoAberta) && !isMelissaConfigRoute(secaoAberta)"
                class="secao-layer"
                @click.self="fecharSecao"
            >
                <div class="secao-panel">
                    <header class="flex items-start justify-between mb-6">
                        <div class="flex items-center gap-3 min-w-0">
                            <div class="secao-icon">
                                <i :class="SECOES[secaoAberta].icon" class="text-xl text-white/85" />
                            </div>
                            <div class="min-w-0">
                                <div class="text-white/55 text-xs uppercase tracking-[0.2em]">Seção</div>
                                <div class="text-white text-2xl font-light mt-1">
                                    {{ SECOES[secaoAberta].label }}
                                </div>
                            </div>
                        </div>
                        <button
                            class="glass-btn w-9 h-9 grid place-items-center shrink-0"
                            title="Fechar (Esc)"
                            @click="fecharSecao"
                        >
                            <i class="pi pi-times text-white/90 text-sm" />
                        </button>
                    </header>

                    <div class="secao-body">
                        <i :class="SECOES[secaoAberta].icon" class="secao-body-icon" />
                        <div class="text-white/75 text-base font-light max-w-md text-center">
                            {{ SECOES[secaoAberta].descricao }}
                        </div>
                        <div class="text-white/35 text-xs mt-3 text-center">
                            Placeholder — aqui virá o módulo de {{ SECOES[secaoAberta].label }}.
                        </div>
                    </div>

                    <footer class="text-white/40 text-[0.7rem] text-center mt-4">
                        Pressione <kbd class="px-1.5 py-0.5 rounded bg-white/10 border border-white/15">Esc</kbd>
                        ou clique fora para voltar ao resumo
                    </footer>
                </div>
            </div>
        </Transition>

        <!-- Cronômetro (componente externo, gerencia próprio estado) -->
        <MelissaCronometro
            ref="cronoRef"
            :pacientes="pacientesReais"
            :default-paciente-id="null"
            :duracao-minutos="CONFIG_DURACAO_MIN"
            :toque-termino="toqueTermino"
            @visible-change="cronoVisible = $event"
            @session-end="onCronometroSessionEnd"
        />

        <!-- Drawer de conversas (WhatsApp): mesmo padrão do AppLayout.
             Sem ele montado, conversationDrawerStore.openForPatient() ativa o
             estado mas não tem componente reativo pra abrir. -->
        <ConversationDrawer />

        <!-- ConfirmDialog: usado pelos handlers da agenda (drag/resize  -->
        <!-- pede confirmação). Auto-resolvido via PrimeVueResolver.     -->
        <!--                                                              -->
        <!-- Slot #message override: persistMoveOrResize gera mensagens   -->
        <!-- com <strong> ao redor de datas/horários. v-html renderiza    -->
        <!-- HTML; nome do paciente já é escapado em useMelissaAgenda     -->
        <!-- (_esc) pra evitar XSS.                                       -->
        <ConfirmDialog>
            <template #message="slotProps">
                <span class="p-confirm-dialog-message" v-html="slotProps.message.message" />
            </template>
        </ConfirmDialog>

        <!-- ═══════════════════════════════════════════════════════ -->
        <!-- AgendaEventDialog — editor completo (CRUD + recorrência    -->
        <!-- + financeiro). Vive no nível do MelissaLayout pra cobrir   -->
        <!-- toda a tela e ser independente de qual seção está aberta. -->
        <!-- Os handlers (M.onDialogSave/Delete/etc) e o estado vêm do  -->
        <!-- composable useMelissaAgenda.                               -->
        <!-- ═══════════════════════════════════════════════════════ -->
        <AgendaEventDialog
            v-model="agendaDialogOpen"
            :eventRow="agendaDialogEventRow"
            :initialStartISO="agendaDialogStartISO"
            :initialEndISO="agendaDialogEndISO"
            :ownerId="agendaOwnerId"
            :tenantId="agendaClinicTenantId"
            :commitmentOptions="agendaCommitmentOptions"
            :workRules="agendaWorkRules"
            :blockedDates="[]"
            :agendaSettings="agendaSettings"
            :allEvents="agendaAllEvents"
            :pausasSemanais="agendaSettings?.pausas_semanais || []"
            :feriados="agendaFeriados"
            :serieRefreshTick="agendaSerieRefreshTick"
            :blockOverlapWarning="agendaDialogBlockOverlap"
            newPatientRoute="/therapist/patients/cadastro"
            @save="M.onDialogSave"
            @delete="M.onDialogDelete"
            @updateSeriesEvent="M.onUpdateSeriesEvent"
            @editSeriesOccurrence="M.onEditSeriesOccurrence"
            @usar-sessao="onUsarSessao"
            @revogar-sessao="onRevogarSessao"
            @cobranca-atualizada="onCobrancaAtualizada"
        />

        <!-- 2º AgendaEventDialog — empilhado por cima do principal pra editar
             uma OCORRÊNCIA específica de série. Acionado pelo botão "Editar"
             nas pills da lista "Recorrências Aplicadas". Reusa os mesmos
             handlers de save/delete/update — o composable distingue pelo
             id/recurrence_date. PrimeVue empilha automaticamente, então
             nenhum gerenciamento manual de z-index é necessário.
             Adicionado 2026-05-11; pendente replicar em Rail/Clínica. -->
        <AgendaEventDialog
            v-model="agendaOccDialogOpen"
            :eventRow="agendaOccDialogEventRow"
            :initialStartISO="agendaOccDialogStartISO"
            :initialEndISO="agendaOccDialogEndISO"
            :ownerId="agendaOwnerId"
            :tenantId="agendaClinicTenantId"
            :commitmentOptions="agendaCommitmentOptions"
            :workRules="agendaWorkRules"
            :blockedDates="[]"
            :agendaSettings="agendaSettings"
            :allEvents="agendaAllEvents"
            :pausasSemanais="agendaSettings?.pausas_semanais || []"
            :feriados="agendaFeriados"
            newPatientRoute="/therapist/patients/cadastro"
            :occurrenceMode="true"
            @save="M.onDialogSave"
            @delete="M.onDialogDelete"
            @updateSeriesEvent="M.onUpdateSeriesEvent"
            @usar-sessao="onUsarSessao"
            @revogar-sessao="onRevogarSessao"
            @cobranca-atualizada="onCobrancaAtualizada"
        />

        <!-- BloqueioDialog — bloqueio de horário/período/dia/feriados.
             Trigger é o menu na toolbar da MelissaAgenda. Após salvar,
             refetcha pra refletir o bloqueio na agenda. -->
        <BloqueioDialog
            v-model="agendaBloqueioOpen"
            :mode="agendaBloqueioMode"
            :workRules="agendaWorkRules"
            :settings="agendaSettings"
            :ownerId="agendaOwnerId"
            :tenantId="agendaClinicTenantId"
            @saved="M.refetch"
        />

        <!-- Dialog "Lançamentos da sessão" (2026-05-14): lista todos os
             financial_records vinculados ao evento atual. Abre via botão
             "Lançamentos" na seção Financeiro do MelissaEventoPanel. -->
        <Dialog
            v-model:visible="lancamentosDialogOpen"
            modal
            :draggable="false"
            :style="{ width: '640px', maxWidth: '96vw' }"
        >
            <template #header>
                <div class="flex flex-col gap-0.5">
                    <span class="text-base font-bold">Lançamentos da sessão</span>
                    <span class="text-xs opacity-70">{{ lancamentosEventoTitulo }}</span>
                </div>
            </template>
            <div v-if="lancamentosLoading" class="py-6 text-center text-sm opacity-70">
                <i class="pi pi-spin pi-spinner mr-1" /> Carregando…
            </div>
            <div v-else-if="!lancamentosList.length" class="py-6 text-center text-sm opacity-70">
                <i class="pi pi-info-circle mr-1" /> Nenhum lançamento vinculado a esta sessão.
            </div>
            <div v-else-if="!lancamentosFiltered.length" class="py-6 text-center text-sm opacity-70">
                <i class="pi pi-info-circle mr-1" /> Sem lançamentos ativos. {{ lancamentosCancelledCount }} cancelado(s) no histórico.
                <div class="mt-3">
                    <Button :label="`Mostrar ${lancamentosCancelledCount} cancelado(s)`" size="small" text @click="lancamentosShowHistory = true" />
                </div>
            </div>
            <div v-else class="flex flex-col gap-2.5">
                <!-- Toggle de histórico (só aparece quando há cancelled) -->
                <div v-if="lancamentosCancelledCount > 0" class="flex items-center justify-end gap-2 text-xs opacity-70">
                    <span v-if="!lancamentosShowHistory">{{ lancamentosCancelledCount }} cancelado(s) ocultos.</span>
                    <Button
                        :label="lancamentosShowHistory ? 'Ocultar histórico' : 'Mostrar histórico'"
                        :icon="lancamentosShowHistory ? 'pi pi-eye-slash' : 'pi pi-history'"
                        size="small"
                        text
                        @click="lancamentosShowHistory = !lancamentosShowHistory"
                    />
                </div>
                <div
                    v-for="(r, idx) in lancamentosFiltered"
                    :key="r.id"
                    class="ml-lanc-card"
                    :class="{ 'ml-lanc-card--child': idx > 0, 'ml-lanc-card--cancelled': r.status === 'cancelled' }"
                >
                    <div class="ml-lanc-card__head">
                        <i v-if="idx > 0" class="pi pi-arrow-right-and-arrow-left-up-down ml-lanc-card__indent" />
                        <span class="ml-lanc-card__desc">{{ r.description || (idx === 0 ? 'Sessão' : 'Cobrança extra') }}</span>
                        <Tag :value="_lancStatusLabels[r.status] || r.status" :severity="_lancStatusSeverity(r.status)" class="text-xs ml-auto" />
                    </div>
                    <div class="ml-lanc-card__body">
                        <div class="ml-lanc-card__row">
                            <i class="pi pi-money-bill" />
                            <span class="ml-lanc-card__amount">{{ _fmtLancBRL(r.final_amount || r.amount) }}</span>
                        </div>
                        <div v-if="r.payment_method" class="ml-lanc-card__row">
                            <i class="pi pi-credit-card" />
                            <span>{{ _lancMethodLabels[r.payment_method] || r.payment_method }}</span>
                        </div>
                        <div class="ml-lanc-card__row">
                            <i class="pi pi-calendar" />
                            <span>Vencimento: {{ _fmtLancDate(r.due_date) }}</span>
                        </div>
                        <div v-if="r.paid_at" class="ml-lanc-card__row">
                            <i class="pi pi-check-circle" />
                            <span>Pago em {{ _fmtLancDate(r.paid_at) }}</span>
                        </div>
                    </div>
                </div>
            </div>
            <template #footer>
                <Button label="Fechar" severity="secondary" outlined @click="lancamentosDialogOpen = false" />
            </template>
        </Dialog>

        <!-- Dialog "Antecipar pagamento" (Fase 5, 2026-05-14): paciente
             quer pagar antes da sessão. Materializa ocorrência se virtual
             e cria/atualiza financial_record. Não decrementa saldo. -->
        <Dialog
            v-model:visible="anteciparDialogOpen"
            modal
            :draggable="false"
            :header="anteciparMode === 'update' ? 'Trocar método de pagamento' : 'Antecipar pagamento'"
            :style="{ width: '480px', maxWidth: '96vw' }"
        >
            <div class="flex flex-col gap-3 pt-1">
                <div class="text-sm">
                    <template v-if="anteciparMode === 'update'">
                        Atualizar a forma de pagamento sem cancelar o registro atual (mais limpo no histórico).
                    </template>
                    <template v-else>
                        Receba antecipadamente o valor desta sessão.
                    </template>
                </div>
                <div v-if="anteciparEventoRef" class="flex flex-col gap-1 px-3 py-2 rounded-md bg-[var(--surface-section)] border border-[var(--surface-border)]">
                    <div class="text-sm font-semibold">{{ anteciparEventoRef.pacienteNome || 'Sessão' }}</div>
                    <div class="text-xs opacity-70">Valor: R$ {{ Number(anteciparEventoRef.price || 0).toFixed(2).replace('.', ',') }}</div>
                </div>
                <div class="flex flex-col gap-1.5">
                    <label class="text-xs font-medium">{{ anteciparMode === 'update' ? 'Novo método de pagamento' : 'Como o paciente pagou?' }}</label>
                    <Select
                        v-model="anteciparMethod"
                        :options="anteciparMethodOptions"
                        optionLabel="label"
                        optionValue="value"
                        size="small"
                    />
                </div>
                <small v-if="anteciparMode !== 'update'" class="text-xs opacity-60">
                    O saldo do pacote será decrementado quando você marcar a sessão como Realizada.
                </small>
                <small v-else class="text-xs opacity-60">
                    A troca é registrada no histórico do lançamento (auditoria), sem criar novo registro.
                </small>
            </div>
            <template #footer>
                <Button label="Cancelar" severity="secondary" outlined :disabled="anteciparBusy" @click="anteciparDialogOpen = false" />
                <Button :label="anteciparMode === 'update' ? 'Atualizar' : 'Confirmar'" icon="pi pi-check" :loading="anteciparBusy" @click="confirmAnteciparPagamento" />
            </template>
        </Dialog>

        <!-- AgendaStatusChangeConfirmDialog — Fase 5 (2026-05-14): aparece
             quando user muda status pra realizado/faltou/cancelado e há
             decisão a tomar (regra de exceção, pacote saldo, pending). -->
        <AgendaStatusChangeConfirmDialog
            v-model="agendaStatusDialogOpen"
            :evento="agendaStatusDialogProps.evento"
            :novoStatus="agendaStatusDialogProps.novoStatus"
            :regraExcecao="agendaStatusDialogProps.regraExcecao"
            :billingContract="agendaStatusDialogProps.billingContract"
            :billingContractStyle="agendaStatusDialogProps.billingContractStyle"
            :pendingRecord="agendaStatusDialogProps.pendingRecord"
            :sessionPrice="agendaStatusDialogProps.sessionPrice"
            @confirm="agendaOnStatusDialogConfirm"
            @update:modelValue="(v) => !v && agendaOnStatusDialogCancel()"
        />

        <!-- Toast: AppLayout não monta no Melissa (rota fullscreen),
             então as pages embedadas (config, agendador online, etc.)
             precisam de um Toast próprio aqui pra não silenciar o
             feedback de save. -->
        <Toast />

        <!-- Drawer de notificações (idem: AppTopbar não monta aqui) -->
        <NotificationDrawer />

        <!-- Plan menu DEV — popup ancorado no botão da topbar -->
        <Menu ref="planMenu" :model="combinedDevMenuModel" popup appendTo="body" :baseZIndex="3000" />
    </div>
</template>

<style scoped>
/* ─── Root ─────────────────────────────────────────────────── */
.win11-root {
    position: fixed;
    inset: 0;
    overflow: hidden;
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
/* Camada da foto custom — fica entre o gradiente default (.win11-root)
   e o dim. Opacidade vem do slider via inline style. */
.win11-photo {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    pointer-events: none;
    transition: opacity 200ms ease;
}
.win11-dim {
    position: absolute;
    inset: 0;
    transition: background-color 200ms ease;
}

/* (Removido) .melissa-config-aside-host — o sidebar global foi
   substituido pelo botao + MelissaConfigPopover em cada pagina de
   config. CSS var --m-config-aside-left tambem nao e mais setada
   aqui; as pages tem `inset: ... 6px` fixo de novo. */

/* ─── Plano de trás (resumo) ───────────────────────────────────
   Animacao composicao-pura (transform + opacity) — ambos sao free
   no compositor (nao re-pintam, nao re-layout). O blur deixou de
   estar AQUI (era filter no container = re-render de toda a arvore
   a cada frame, com layer explosion via backdrop-filter dos
   glass-panels filhos). Agora vem do .win11-blur-veil (irmao
   abaixo) — 1 backdrop pass unico em vez de N×N.

   will-change + translateZ(0): promove pra layer dedicada ANTES
   do transition disparar. Sem isso, o browser cria a layer no
   primeiro frame e gera stutter visivel.

   backface-visibility: evita sub-pixel jitter no scale(0.98).
*/
.win11-summary {
    position: relative;
    z-index: 10;
    width: 100%;
    height: 100%;
    transition: transform 320ms ease, opacity 320ms ease;
    will-change: transform, opacity;
    transform: translateZ(0);
    backface-visibility: hidden;
}
.win11-summary.is-behind {
    transform: scale(0.98) translateZ(0);
    opacity: 0.55;
    pointer-events: none;
}

/* ─── Veu de blur sobre o resumo ───────────────────────────────
   1 unica camada com backdrop-filter cobrindo o resumo. Substitui
   o filter:blur antigo aplicado no .win11-summary inteiro. Em vez
   de blurar 5+ glass-panels filhos (cada um com seu proprio
   backdrop-filter), e' UM blur unico que pinta o resultado
   acumulado do resumo abaixo.

   z-index 11: acima do .win11-summary (10) e abaixo dos overlays
   de seção/cronometro/menu (≥30). pointer-events: none deixa o
   click atravessar pra o overlay aberto.

   contain: strict promove layer + isola repaints do veu do resto
   do .win11-root (fix de "first-frame stutter"). will-change na
   opacity prepara o compositor pro transition.

   Mobile: blur de 3px ainda eh caro em GPU mid-tier; reduz pra
   2px no media query abaixo. */
.win11-blur-veil {
    position: absolute;
    inset: 0;
    z-index: 11;
    pointer-events: none;
    opacity: 0;
    transition: opacity 320ms ease;
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    will-change: opacity;
    contain: strict;
}
.win11-blur-veil.is-active {
    opacity: 1;
}
@media (max-width: 768px) {
    .win11-blur-veil {
        backdrop-filter: blur(2px);
        -webkit-backdrop-filter: blur(2px);
    }
}
.win11-summary__inner {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* `safe center`: centraliza vertical quando o conteúdo cabe;
       vira flex-start (sem clipar) quando excede e exige scroll */
    justify-content: safe center;
    padding: 4rem 2rem 6rem;
    /* Scrollbar discreta */
    scrollbar-width: thin;
    scrollbar-color: var(--m-border-strong) transparent;
}
.win11-summary__inner::-webkit-scrollbar {
    width: 6px;
}
.win11-summary__inner::-webkit-scrollbar-thumb {
    background: var(--m-border-strong);
    border-radius: 3px;
}

/* ─── Relógio ──────────────────────────────────────────────── */
/* ─── Glass primitives ─────────────────────────────────────── */
.glass-panel {
    background: var(--m-bg-soft);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    border: 1px solid var(--m-border);
    border-radius: 16px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.glass-btn {
    background: var(--m-bg-soft);
    backdrop-filter: blur(20px);
    border: 1px solid var(--m-border-strong);
    border-radius: 9999px;
    transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
    cursor: pointer;
}
.glass-btn:hover {
    background: var(--m-bg-soft-hover);
}
.glass-btn:disabled {
    cursor: default;
    opacity: 0.6;
}
/* Active state (drawer aberto, popover aberto, etc.) — primary tint */
.glass-btn--active {
    background: color-mix(in srgb, var(--p-primary-color) 24%, transparent);
    border-color: color-mix(in srgb, var(--p-primary-color) 55%, transparent);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--p-primary-color) 35%, transparent);
}

/* Badge de count (notificações não lidas) — espelha .dock-pin__badge
   mas em contexto de glass-btn topbar (canto sup. dir.). */
.m-topbar-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 9999px;
    background: rgb(239, 68, 68);
    color: white;
    font-size: 0.6rem;
    font-weight: 700;
    display: grid;
    place-items: center;
    line-height: 1;
    border: 1.5px solid var(--m-bg-medium, rgba(0, 0, 0, 0.4));
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    pointer-events: none;
}

/* ─── Shell: cards-row scroll/wrap + card "+" sempre fora ──── */
.cards-shell {
    display: flex;
    align-items: stretch; /* "+" estica até a altura do row (linha única) */
    gap: 1rem;
    /* Sem padding aqui: o respiro vai dentro do .cards-row (pra estar
       dentro da área que tem overflow controlado). */
}
.cards-shell.is-wrap {
    align-items: flex-start; /* em wrap o "+" não estica (cards-row pode ter 2+ linhas) */
}

/* Row interno: ocupa o espaço restante; comporta cards (scroll ou wrap) */
.cards-row {
    flex: 1;
    min-width: 0; /* essencial pra flex item permitir overflow-x */
    display: flex;
    gap: 1rem;
}
.cards-row--linha {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* `overflow-y: hidden` explícito suprime o scroll vertical que o
       browser adicionaria (overflow-x:auto força overflow-y:auto). O
       padding abaixo já deu altura suficiente pra lift e botão "+". */
    overflow-y: hidden;
    /* `safe center`: centraliza quando os cards cabem; vira start
       (sem clipar) quando overflow exigir scroll horizontal */
    justify-content: safe center;
    /* Padding interno (só nesse modo, pq o overflow-y:hidden exige):
        - top: 8px → respiro pro hover-lift
        - bottom: 24px → respiro pro botão "+" do card (bottom: -16px)
        - lateral: 4px → pra cards das pontas não colarem */
    padding: 8px 4px 24px;
    /* Scrollbar horizontal discreta */
    scrollbar-width: thin;
    scrollbar-color: var(--m-border-strong) transparent;
}
.cards-row--linha::-webkit-scrollbar {
    height: 6px;
}
.cards-row--linha::-webkit-scrollbar-thumb {
    background: var(--m-border-strong);
    border-radius: 3px;
}
.cards-row--wrap {
    flex-wrap: wrap;
    justify-content: center;
    /* Wrap não precisa do padding/overflow do linha-única.
       row-gap maior pra botão "+" da linha de cima não colar nas
       cards da linha de baixo. */
    row-gap: 2rem;
    padding-bottom: 16px;
}

/* Card "+" em linha única: tamanho fixo + margin pra alinhar
   visualmente com os cards (que estão dentro do padding-top: 8 da row).
   Usa :deep() pra atravessar o scope do MelissaCard.vue. */
.cards-shell:not(.is-wrap) :deep(.mc-card-add) {
    height: 100px;
    margin-top: 8px;
}

/* Mobile: "+" vira pill mínimo na linha única (economiza horizontal) */
@media (max-width: 640px) {
    .cards-shell:not(.is-wrap) :deep(.mc-card-add) {
        width: 30px;
        border-radius: 9px;
    }
}

/* ─── Central de cards (dialog de personalização) ─────────── */
.central-layer {
    position: absolute;
    inset: 0;
    z-index: 56;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
}
.central-panel {
    width: min(440px, 100%);
    max-height: 80vh;
    overflow: auto;
    background: var(--m-bg-medium);
    backdrop-filter: blur(32px) saturate(160%);
    -webkit-backdrop-filter: blur(32px) saturate(160%);
    border: 1px solid var(--m-border-strong);
    border-radius: 22px;
    padding: 1.75rem;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
}
.central-radio {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--m-text);
    font-size: 0.85rem;
    cursor: pointer;
    padding: 6px 8px;
    border-radius: 8px;
    transition: background-color 140ms ease;
}
.central-radio:hover {
    background: var(--m-bg-soft);
}
.central-radio input[type='radio'] {
    accent-color: #34d399;
    cursor: pointer;
}
.central-card-row {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--m-text);
    font-size: 0.85rem;
    cursor: pointer;
    padding: 8px 10px;
    border-radius: 8px;
    transition: background-color 140ms ease;
}
.central-card-row:hover {
    background: var(--m-bg-soft);
}
.central-card-row input[type='checkbox'] {
    accent-color: #34d399;
    cursor: pointer;
}
.central-card-row.is-em-breve {
    color: var(--m-text-muted);
}

/* ─── Botão ψ (avatar — futuramente logo da empresa) ─────── */
/* z-index alto: sempre acima de QUALQUER overlay (page, menu, dialogs)
   pra continuar acessível mesmo com cronômetro/agenda/secao abertos.
   Vive centralizado verticalmente na faixa do .melissa-dock (76px) —
   bottom calculado pra centralizar o ψ de 56px na faixa: (76-56)/2 = 10px. */
.psi-btn {
    position: absolute;
    bottom: 10px;
    left: 1.25rem;
    z-index: 70;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--m-bg-soft-hover);
    backdrop-filter: blur(24px) saturate(160%);
    -webkit-backdrop-filter: blur(24px) saturate(160%);
    border: 1px solid var(--m-border-strong);
    display: grid;
    place-items: center;
    cursor: pointer;
    transition: all 200ms ease;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

/* (.melissa-dock vive no bloco <style> global no fim do arquivo —
   tem que ser global porque (a) o nó <div class="melissa-dock"/> é
   static-hoisted pelo compiler e perde o data-v scoped, e (b) recebe
   filhos via Teleport de outros componentes que precisam herdar pointer-events) */
.psi-btn:hover {
    background: var(--m-border-strong);
    transform: translateY(-2px);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
.psi-glyph {
    font-family: 'Instrument Serif', Georgia, serif;
    font-size: 2rem;
    line-height: 1;
    color: white;
    font-style: italic;
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    /* ψ tem descender pronunciado (cauda que vai abaixo da baseline);
       o `place-items: center` do botão centraliza pela line-box, não pelo
       glifo, então empurro 3px pra cima pra equilibrar visualmente. */
    transform: translateY(-3px);
}

/* Pill overlay com atalho de teclado, ancorado no canto inferior-direito do ψ */
.psi-kbd {
    position: absolute;
    bottom: calc(-15% + 8px);
    left: 60%;
    /* Bg/color usam tokens — em dark vira pílula escura com texto claro,
       em light vira pílula clara com texto escuro. --m-kbd-* definidos
       no bloco global de tokens no fim do arquivo. */
    background: var(--m-kbd-bg);
    color: var(--m-kbd-text);
    padding: 3px 9px;
    border-radius: 9999px;
    font-size: 0.6rem;
    font-weight: 500;
    letter-spacing: 0.05em;
    white-space: nowrap;
    pointer-events: none;
    border: 1px solid var(--m-border-strong);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}

/* ─── Botão-trigger do cronômetro (irmão flex do relógio) ───── */
/* ─── Seção: overlay grande (Pacientes, WhatsApp, etc.) ─────── */
.secao-layer {
    position: absolute;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
}
.secao-panel {
    width: min(1100px, 100%);
    height: min(80vh, 720px);
    background: var(--m-bg-medium);
    backdrop-filter: blur(32px) saturate(160%);
    -webkit-backdrop-filter: blur(32px) saturate(160%);
    border: 1px solid var(--m-border-strong);
    border-radius: 22px;
    padding: 1.75rem;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
    display: flex;
    flex-direction: column;
}
.secao-icon {
    width: 44px;
    height: 44px;
    border-radius: 12px;
    background: var(--m-bg-soft);
    border: 1px solid var(--m-border-strong);
    display: grid;
    place-items: center;
    flex-shrink: 0;
}
.secao-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: auto;
    padding: 1rem;
}
.secao-body-icon {
    font-size: 4.5rem;
    color: var(--m-text-faint);
    margin-bottom: 1.5rem;
}

/* (Antes vivia aqui o CSS do panel inline de evento — extraído pra
   MelissaEventoPanel.vue em 2026-04-27 junto com o componente.) */

/* (removido em 2026-04-29: .page-fade-* não é mais usado. As páginas
   fullscreen perderam o <Transition> wrapper porque o leave delay
   gerava orphan placeholders nos Teleport targets compartilhados.
   Animação de entrada agora vive como @keyframes nos próprios
   componentes — ver .ma-page / .mp-page.) */

/* ─── MelissaMenu — transição "rise" no .mm-panel (não no layer) ─── */
.menu-rise-enter-active :deep(.mm-panel),
.menu-rise-leave-active :deep(.mm-panel) {
    transition: opacity 240ms ease, transform 280ms cubic-bezier(0.2, 0.7, 0.3, 1);
    transform-origin: bottom left;
}
.menu-rise-enter-from :deep(.mm-panel) {
    opacity: 0;
    transform: translateY(16px) scale(0.97);
}
.menu-rise-leave-to :deep(.mm-panel) {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
}

/* Em mobile (<lg) o painel vira drawer da esquerda — anima translateX
   em vez de translateY+scale. Backdrop do .mm-layer também anima opacity. */
@media (max-width: 1023px) {
    .menu-rise-enter-active :deep(.mm-panel),
    .menu-rise-leave-active :deep(.mm-panel) {
        transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1);
    }
    .menu-rise-enter-from :deep(.mm-panel),
    .menu-rise-leave-to :deep(.mm-panel) {
        opacity: 1;
        transform: translateX(-100%);
    }
    .menu-rise-enter-active :deep(.mm-layer),
    .menu-rise-leave-active :deep(.mm-layer) {
        transition: opacity 240ms ease;
    }
    .menu-rise-enter-from :deep(.mm-layer),
    .menu-rise-leave-to :deep(.mm-layer) {
        opacity: 0;
    }
}

/* ─── Transições ───────────────────────────────────────────── */
.lift-enter-active,
.lift-leave-active {
    transition: opacity 240ms ease, transform 280ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.lift-enter-from {
    opacity: 0;
    transform: scale(0.96) translateY(12px);
}
.lift-leave-to {
    opacity: 0;
    transform: scale(0.98) translateY(8px);
}

.settings-pop-enter-active,
.settings-pop-leave-active {
    transition: opacity 160ms ease, transform 160ms ease;
}
.settings-pop-enter-from,
.settings-pop-leave-to {
    opacity: 0;
    transform: translateY(6px) scale(0.98);
}

/* line-clamp util (caso Tailwind não tenha) */
.line-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* ── Dialog "Lançamentos da sessão" (2026-05-14) ── */
.ml-lanc-card {
    border: 1px solid var(--surface-border);
    border-radius: 8px;
    padding: 0.7rem 0.85rem;
    background: var(--surface-card);
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.ml-lanc-card--child {
    background: color-mix(in srgb, var(--p-primary-color) 4%, var(--surface-card));
    margin-left: 1.5rem;
    border-color: color-mix(in srgb, var(--p-primary-color) 25%, var(--surface-border));
}
/* Cancelled — apenas visíveis quando user expande o histórico.
   Visual atenuado pra sinalizar "audit trail, não-ativo". */
.ml-lanc-card--cancelled {
    opacity: 0.55;
    border-style: dashed;
    background: color-mix(in srgb, var(--surface-ground) 60%, transparent);
}
.ml-lanc-card--cancelled .ml-lanc-card__desc {
    text-decoration: line-through;
    text-decoration-color: currentColor;
    text-decoration-thickness: 1px;
}
.ml-lanc-card__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.ml-lanc-card__indent {
    color: var(--text-color-secondary);
    font-size: 0.7rem;
    transform: scaleY(-1);
}
.ml-lanc-card__desc {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text-color);
}
.ml-lanc-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
.ml-lanc-card__row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.78rem;
    color: var(--text-color-secondary);
}
.ml-lanc-card__row i { font-size: 0.72rem; }
.ml-lanc-card__amount {
    font-weight: 600;
    color: var(--text-color);
}
</style>

<!-- ────────────────────────────────────────────────────────────
     Variáveis de tema Melissa — bloco GLOBAL (não scoped) pra
     atravessar o scoped CSS dos componentes filhos (MelissaMenu,
     MelissaCronometro, MelissaAgenda, MelissaCard, MelissaBusca).

     Defaults (modo escuro):
       --m-bg-soft   bg sutil em superfícies internas (cards, hovers)
       --m-bg-medium bg dos painéis flutuantes (panel principal)
       --m-text      cor de texto principal
       --m-text-muted texto secundário
       --m-border    cor de bordas glass
       --m-accent    cor primária do tema (PrimeVue --p-primary-color)
       --m-accent-soft tint sutil da primária pra active/hover

     Em modo light (html sem .app-dark) os valores invertem.
──────────────────────────────────────────────────────────── -->
<style>
/* Tokens Melissa derivados dos tokens semânticos do PrimeVue (--p-*) que
   já flipam automaticamente com dark/light AND seguem o preset escolhido
   (Aura/Lara/Nora/Material têm escalas e radii próprios). */
.win11-root {
    /* Cor primária — vem do preset escolhido pelo user */
    --m-accent:        var(--p-primary-color);
    --m-accent-soft:   color-mix(in srgb, var(--p-primary-color) 18%, transparent);
    --m-accent-strong: color-mix(in srgb, var(--p-primary-color) 35%, transparent);

    /* Glass derivado do content/surface tokens com alpha pra preservar
       o efeito translúcido sobre a photo. Auto-flip dark/light. */
    --m-bg-soft:       color-mix(in srgb, var(--p-content-background) 50%, transparent);
    --m-bg-soft-hover: color-mix(in srgb, var(--p-content-hover-background, var(--p-surface-100)) 70%, transparent);
    --m-bg-medium:     color-mix(in srgb, var(--p-content-background) 88%, transparent);

    /* Texto e bordas — auto-flip via PrimeVue */
    --m-text:          var(--p-text-color);
    --m-text-muted:    var(--p-text-muted-color);
    --m-text-faint:    color-mix(in srgb, var(--p-text-color) 45%, transparent);
    --m-border:        var(--p-content-border-color);
    --m-border-strong: color-mix(in srgb, var(--p-text-color) 22%, transparent);

    /* Border-radius do preset (Aura usa 12px, Nora 6px, Lara 6px etc.) */
    --m-radius:    var(--p-border-radius, 8px);
    --m-radius-lg: calc(var(--p-border-radius, 8px) * 1.5);

    /* RGB do dim (overlay sobre a photo). Opacidade vem do slider via
       rgba(var(--m-dim-rgb), opacity) no inline style. Único token que
       precisa de override manual (PrimeVue não tem "dim" nativo). */
    --m-dim-rgb: 0, 0, 0;

    /* ─── Gradiente "Bloom" (default = dark) ────────────────────────
       Derivado da palette primária + surface do preset. Em light flipam
       pra tons claros (200/100 + surface-0/primary-50). O defaultBgStyle
       no <script> referencia essas vars; mudar aqui muda o bloom todo. */
    --bloom-c1:     var(--p-primary-400);
    --bloom-c2:     var(--p-primary-300);
    --bloom-base-1: var(--p-surface-950);
    --bloom-base-2: var(--p-primary-950);

    /* ─── Pílula de atalho de teclado (psi-kbd) ─────────────────────
       Em dark: pílula escura com texto claro. Em light: pílula clara
       com texto escuro (override no bloco light abaixo). */
    --m-kbd-bg:   rgba(0, 0, 0, 0.65);
    --m-kbd-text: rgba(255, 255, 255, 0.92);

    /* ─── Dock (taskbar transparent) ────────────────────────────────
       Altura da faixa inferior que recebe items minimizados (chip do
       cronômetro, futuros). Páginas fullscreen reservam esse espaço
       no inset bottom pra não ficar atrás do dock. */
    --m-dock-h: 76px;

    /* ─── Faixa de fundo (top + dock) ───────────────────────────────
       Cor solida com 80% de opacidade pra fazer gradiente horizontal
       e dar legibilidade aos icones sem virar barra hard. Default =
       dark (preto); light flipa pra branco (override em html:not(.app-dark)). */
    --m-band: rgba(0, 0, 0, 0.8);

    /* ─── Fundo dos textos do hero (relogio/saudacao/resumo) ─────────
       Toggle "fundo nos textos soltos" no painel Personalizar. 60% de
       opacidade pra dar legibilidade sobre wallpapers complexos sem
       sumir totalmente o look do bg. Default = dark; light flipa. */
    --m-hero-text-bg: rgba(0, 0, 0, 0.6);
    --m-hero-text-border: rgba(255, 255, 255, 0.12);
}

/* ─── Tray Melissa (system tray win11-style, canto inferior direito)
   Posiciona o grupo de icones globais (plan-DEV, notificacoes, ajuda,
   cog) alinhado verticalmente com os pins do dock. z-index acima do
   dock (65) pra ficar no mesmo plano interativo. */
.melissa-tray {
    position: fixed;
    bottom: 0;
    right: 1.25rem;
    height: var(--m-dock-h, 76px);
    z-index: 66;
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Dot vermelho no botao "More" da tray (mobile) — sinaliza que ha
   notificacoes nao-lidas escondidas dentro do popup. */
.melissa-tray__more-dot {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 9999px;
    background: rgb(239, 68, 68);
    border: 1.5px solid var(--m-bg-medium, rgba(0, 0, 0, 0.4));
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    pointer-events: none;
}

/* Popup vertical do "More" — abre acima do botao trigger, mesmo padrao
   visual do MelissaSettingsPanel mas mais compacto. */
.melissa-tray__more-panel {
    position: absolute;
    bottom: calc(var(--m-dock-h, 76px) - 8px);
    right: 0;
    min-width: 220px;
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    border-radius: 14px;
    z-index: 67;
}

.melissa-tray__more-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    background: transparent;
    border: none;
    color: var(--m-text);
    border-radius: 8px;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.88rem;
    text-align: left;
    transition: background-color 140ms ease, color 140ms ease;
}
.melissa-tray__more-item:hover:not(:disabled),
.melissa-tray__more-item:focus-visible {
    background: var(--m-bg-soft-hover);
    outline: none;
}
.melissa-tray__more-item.is-active {
    background: var(--m-bg-soft-hover);
    color: white;
}
.melissa-tray__more-item:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}
.melissa-tray__more-item > i {
    font-size: 0.95rem;
    width: 16px;
    color: var(--m-text-muted);
    flex-shrink: 0;
}
.melissa-tray__more-item:hover > i,
.melissa-tray__more-item.is-active > i {
    color: var(--m-text);
}

/* ─── Dock (global pra atravessar Teleport + evitar perda de scoped
   em static-hoisted nodes). Faixa horizontal no bottom com gradiente
   espelhado: cor solida na esquerda (onde os pins ficam) e fade
   pra transparente na direita. */
.melissa-dock {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: var(--m-dock-h, 76px);
    z-index: 65;             /* abaixo do ψ (70) e overlays modais */
    display: flex;
    align-items: center;
    /* padding-left reserva ~96px pro ψ (56px + 1.25rem left + gap) */
    padding: 0 1.5rem 0 6rem;
    gap: 12px;
    pointer-events: none;    /* só os items recebem clique */
    background: linear-gradient(
        to right,
        var(--m-band) 0%,
        var(--m-band) 35%,
        transparent 85%
    );
}
.melissa-dock > * { pointer-events: auto; }

/* ─── Dock pinned items (Agenda, WhatsApp) ──────────────────────
   Tamanho menor que o ψ (44px vs 56px) + canto arredondado em vez
   de full-circle: hierarquia visual auto-resolvida. ψ = "tudo",
   pinned = atalhos. Hover lifta + ring. Active state mostra qual
   seção tá aberta. */
.dock-pin {
    position: relative;
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border-radius: 12px;
    background: var(--m-bg-soft, rgba(255, 255, 255, 0.06));
    backdrop-filter: blur(20px) saturate(160%);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    border: 1px solid var(--m-border, rgba(255, 255, 255, 0.10));
    color: var(--m-text, white);
    cursor: pointer;
    font-family: inherit;
    transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}
.dock-pin > i {
    font-size: 1.05rem;
    opacity: 0.92;
}
.dock-pin:hover {
    transform: translateY(-3px);
    background: var(--m-bg-soft-hover, rgba(255, 255, 255, 0.10));
    border-color: var(--m-border-strong, rgba(255, 255, 255, 0.18));
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
}
.dock-pin:active {
    transform: translateY(-1px);
}
.dock-pin--active {
    background: color-mix(in srgb, var(--p-primary-color) 22%, transparent);
    border-color: color-mix(in srgb, var(--p-primary-color) 55%, transparent);
    box-shadow:
        0 6px 18px rgba(0, 0, 0, 0.25),
        0 0 0 1px color-mix(in srgb, var(--p-primary-color) 40%, transparent);
}
.dock-pin--active > i { opacity: 1; }

/* Badge de count (WhatsApp não-lidas) */
.dock-pin__badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 9999px;
    background: rgb(239, 68, 68);
    color: white;
    font-size: 0.6rem;
    font-weight: 700;
    display: grid;
    place-items: center;
    line-height: 1;
    border: 1.5px solid var(--m-bg-medium, rgba(0, 0, 0, 0.4));
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    pointer-events: none;
}

/* Light mode: dock-pin precisa de mais contraste sobre bloom claro */
html:not(.app-dark) .melissa-dock .dock-pin {
    box-shadow: 0 4px 12px rgba(15, 23, 42, 0.10);
}
html:not(.app-dark) .melissa-dock .dock-pin:hover {
    box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
}

/* ─── Pins dinâmicos do dock (híbrido fixo + recente) ───────── */
/* Divisor entre builtins e dinâmicos. Fininho, atravessa o gap. */
.dock-divider {
    width: 1px;
    height: 24px;
    align-self: center;
    background: var(--m-border, rgba(255, 255, 255, 0.18));
    opacity: 0.7;
}
html:not(.app-dark) .dock-divider {
    background: rgba(15, 23, 42, 0.18);
}

/* Pin fixado pelo user — pequena marca no canto pra diferenciar do recente. */
.dock-pin--user .dock-pin__pinned-mark {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--p-primary-color) 70%, white);
    box-shadow: 0 0 6px color-mix(in srgb, var(--p-primary-color) 50%, transparent);
    pointer-events: none;
}

/* Pin recente (MRU) — visualmente mais leve pra denotar transitoriedade.
   Fica entre opacity total (active/hover) e ~70% no estado normal. */
.dock-pin--recent {
    opacity: 0.78;
}
.dock-pin--recent:hover,
.dock-pin--recent.dock-pin--active {
    opacity: 1;
}
/* Em mobile (<md=768px) os pins MRU somem pra economizar largura
   (4 builtins + user-fixed pins ja saturam o dock). Media query no
   bloco do .dock-pin--recent ganha do utility 'hidden' do Tailwind
   por ordem de carga (mesma specificity). */
@media (max-width: 767px) {
    .dock-pin--recent { display: none; }
}

/* ─── Skeleton loading utilitário (global pra atravessar scoped CSS dos
       componentes filhos). Use a classe .melissa-skeleton em qualquer
       container — vira um placeholder com shimmer suave.
       Variantes via classes adicionais:
         .melissa-skeleton--text     altura ~12px (linha de texto)
         .melissa-skeleton--title    altura ~18px (heading)
         .melissa-skeleton--avatar   redondo 32px
         .melissa-skeleton--block    altura definida via inline style
       Shimmer respeita prefers-reduced-motion. */
.melissa-skeleton {
    background: linear-gradient(
        90deg,
        var(--m-bg-soft, rgba(255, 255, 255, 0.04)) 0%,
        var(--m-bg-soft-hover, rgba(255, 255, 255, 0.08)) 50%,
        var(--m-bg-soft, rgba(255, 255, 255, 0.04)) 100%
    );
    background-size: 200% 100%;
    animation: melissa-shimmer 1400ms linear infinite;
    border-radius: 6px;
    flex-shrink: 0;
    pointer-events: none;
}
.melissa-skeleton--text   { height: 12px; }
.melissa-skeleton--title  { height: 18px; }
.melissa-skeleton--number { height: 24px; width: 32px; }
.melissa-skeleton--avatar { width: 32px; height: 32px; border-radius: 50%; }
@keyframes melissa-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
    .melissa-skeleton { animation: none; }
}

/* ════════════════════════════════════════════════════════════════
   LIGHT MODE — aesthetic "Pippit/Linear": superfícies sólidas,
   bordas finas, sombras suaves. Sem photo nem glass blur.
   Texto SEMPRE legível porque vem do --p-text-color sobre superfície
   sólida (não depende do slider de opacidade nem da photo).
   ════════════════════════════════════════════════════════════════ */
html:not(.app-dark) .win11-root {
    /* Surfaces sólidas (sem alpha) — vem dos tokens PrimeVue */
    --m-bg-soft:       var(--p-surface-100, #f3f4f6);
    --m-bg-soft-hover: var(--p-surface-200, #e5e7eb);
    --m-bg-medium:     var(--p-content-background);  /* opaco */

    /* Bordas mais sutis em light (Pippit-style) */
    --m-border:        var(--p-content-border-color);
    --m-border-strong: color-mix(in srgb, var(--p-text-color) 14%, transparent);

    /* Sombras leves (substituem o glass blur em light) */
    --m-shadow:    0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.05);
    --m-shadow-md: 0 2px 6px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.08);

    /* Dim só aparece quando há foto custom (ver regra .win11-has-photo
       no fim do arquivo). Sem foto, o bloom claro já é leve o suficiente. */
    --m-dim-rgb: 255, 255, 255;

    /* ─── Gradiente "Bloom" — versão clara (Pippit/Linear morning) ────
       Tons 50/100/200 da primary + surface-0/50. Gradiente fica suave,
       legível, dá identidade Win11 sem competir com o conteúdo. */
    --bloom-c1:     var(--p-primary-200);
    --bloom-c2:     var(--p-primary-100);
    --bloom-base-1: var(--p-surface-0, #ffffff);
    --bloom-base-2: var(--p-primary-50);

    /* Pílula psi-kbd em light: bg escuro suave + texto branco
       (NÃO usar var(--m-text) aqui — em light vira preto e fica preto sobre preto). */
    --m-kbd-bg:   color-mix(in srgb, var(--p-text-color) 78%, transparent);
    --m-kbd-text: var(--p-content-background);

    /* Faixa de fundo do topbar/dock em light: branco 80% (vs preto 80% em dark) */
    --m-band: rgba(255, 255, 255, 0.8);

    /* Fundo dos textos do hero em light: branco 60% (vs preto 60% em dark) */
    --m-hero-text-bg: rgba(255, 255, 255, 0.6);
    --m-hero-text-border: rgba(0, 0, 0, 0.08);
}

/* Light: dim escondido por padrão (sem foto, bloom claro já é leve).
   Quando o user faz upload de foto custom, .win11-has-photo é adicionada
   e o dim volta a fazer sentido. */
html:not(.app-dark) .win11-root .win11-dim {
    display: none;
}
html:not(.app-dark) .win11-root.win11-has-photo .win11-dim {
    display: block;
}

/* Light: panels viram OPACOS com borda + shadow (sem backdrop-filter).
   Aplica em todos os "glass-*" e cards Melissa via :is(). */
html:not(.app-dark) .win11-root :is(
    .glass-panel,
    .glass-btn,
    .mc-card,
    .mc-card-add,
    .mc-card__go,
    .mc-fc-event,
    .mb-field,
    .mb-panel,
    .mb-item,
    .mc-panel,
    .mm-panel,
    .secao-panel,
    .central-panel,
    .evento-panel,
    .ma-page,
    .ma-w,
    .ma-sess,
    .ma-ev,
    .crono-icon-btn,
    .psi-btn,
    .psi-kbd,
    .crono-chip,
    .resumo-link,
    .filtro-chip,
    .module-tile,
    .settings-select,
    .settings-test-btn
) {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}

/* Sombra suave nos painéis principais em light (substitui o "depth" do blur) */
html:not(.app-dark) .win11-root :is(
    .glass-panel,
    .mc-card,
    .mc-panel,
    .mm-panel,
    .secao-panel,
    .central-panel,
    .evento-panel,
    .ma-page,
    .ma-w,
    .mb-panel
) {
    box-shadow: var(--m-shadow);
}
html:not(.app-dark) .win11-root :is(
    .mm-panel,
    .secao-panel,
    .central-panel,
    .evento-panel,
    .mc-panel,
    .ma-page
) {
    box-shadow: var(--m-shadow-md);
}

/* ─── Override de classes Tailwind hardcoded (`text-white/X`, `bg-white/X`,
   `border-white/X`) que vêm dos templates. Em light: textos viram dark,
   bgs viram surface tokens. */

html:not(.app-dark) .win11-root .text-white,
html:not(.app-dark) .win11-root [class*="text-white/9"],
html:not(.app-dark) .win11-root [class*="text-white/85"],
html:not(.app-dark) .win11-root [class*="text-white/8"] {
    color: var(--m-text);
}
html:not(.app-dark) .win11-root [class*="text-white/7"],
html:not(.app-dark) .win11-root [class*="text-white/6"],
html:not(.app-dark) .win11-root [class*="text-white/55"],
html:not(.app-dark) .win11-root [class*="text-white/5"] {
    color: var(--m-text-muted);
}
html:not(.app-dark) .win11-root [class*="text-white/4"],
html:not(.app-dark) .win11-root [class*="text-white/3"],
html:not(.app-dark) .win11-root [class*="text-white/2"] {
    color: var(--m-text-faint);
}

html:not(.app-dark) .win11-root [class*="bg-white/"] {
    background-color: var(--m-bg-soft);
}
html:not(.app-dark) .win11-root [class*="bg-white/2"],
html:not(.app-dark) .win11-root [class*="bg-white/3"] {
    background-color: var(--m-bg-soft-hover);
}
html:not(.app-dark) .win11-root [class*="hover:bg-white/"]:hover {
    background-color: var(--m-bg-soft-hover) !important;
}

html:not(.app-dark) .win11-root [class*="border-white/"] {
    border-color: var(--m-border);
}
html:not(.app-dark) .win11-root [class*="border-white/2"],
html:not(.app-dark) .win11-root [class*="border-white/3"] {
    border-color: var(--m-border-strong);
}

/* Light: clock e ψ usam color: white hardcoded (CSS scoped, fora do alcance
   dos overrides Tailwind). Sobre o bloom claro, branco vira invisível —
   forçar var(--m-text) e shadow leve. */
html:not(.app-dark) .win11-root .clock-display {
    color: var(--m-text);
    text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
html:not(.app-dark) .win11-root .psi-glyph {
    color: var(--m-text);
    text-shadow: none;
}

/* Light: psi-btn ganha sombra suave (substitui a preta agressiva 24px)
   + um leve halo da primary pra destacar o botão sobre o bloom claro. */
html:not(.app-dark) .win11-root .psi-btn {
    box-shadow:
        0 4px 14px rgba(15, 23, 42, 0.08),
        0 1px 3px rgba(15, 23, 42, 0.06),
        0 0 0 1px color-mix(in srgb, var(--p-primary-color) 12%, transparent);
}
html:not(.app-dark) .win11-root .psi-btn:hover {
    box-shadow:
        0 8px 20px rgba(15, 23, 42, 0.12),
        0 2px 6px rgba(15, 23, 42, 0.08),
        0 0 0 1px color-mix(in srgb, var(--p-primary-color) 24%, transparent);
}

/* Light + bloom: panels com leve transparência pra deixar a cor do bloom
   transparecer, mas mantendo legibilidade. 92% de opacidade em vez de 100%. */
html:not(.app-dark) .win11-root :is(
    .glass-panel,
    .mc-card,
    .mc-panel,
    .mm-panel,
    .secao-panel,
    .central-panel,
    .evento-panel,
    .ma-page,
    .ma-w,
    .mb-panel
) {
    background-color: color-mix(in srgb, var(--p-content-background) 92%, transparent);
}

/* ─── Ícones com tons claros (Tailwind text-X-300/400) ────────
   Tons claros são pensados pra fundo escuro (dark glass). Em light
   ficam invisíveis sobre bloom claro/panels brancos. Mapeia pra
   tons -600 (legível em fundo claro, mantém identidade da cor). */
html:not(.app-dark) .win11-root .text-emerald-200,
html:not(.app-dark) .win11-root .text-emerald-300,
html:not(.app-dark) .win11-root .text-emerald-400 { color: rgb(5 150 105); }     /* emerald-600 */

html:not(.app-dark) .win11-root .text-green-200,
html:not(.app-dark) .win11-root .text-green-300,
html:not(.app-dark) .win11-root .text-green-400   { color: rgb(22 163 74); }     /* green-600 */

html:not(.app-dark) .win11-root .text-amber-200,
html:not(.app-dark) .win11-root .text-amber-300,
html:not(.app-dark) .win11-root .text-amber-400   { color: rgb(217 119 6); }     /* amber-600 */

html:not(.app-dark) .win11-root .text-purple-200,
html:not(.app-dark) .win11-root .text-purple-300,
html:not(.app-dark) .win11-root .text-purple-400  { color: rgb(147 51 234); }    /* purple-600 */

html:not(.app-dark) .win11-root .text-blue-200,
html:not(.app-dark) .win11-root .text-blue-300,
html:not(.app-dark) .win11-root .text-blue-400    { color: rgb(37 99 235); }     /* blue-600 */

html:not(.app-dark) .win11-root .text-sky-200,
html:not(.app-dark) .win11-root .text-sky-300,
html:not(.app-dark) .win11-root .text-sky-400     { color: rgb(2 132 199); }     /* sky-600 */

html:not(.app-dark) .win11-root .text-cyan-200,
html:not(.app-dark) .win11-root .text-cyan-300,
html:not(.app-dark) .win11-root .text-cyan-400    { color: rgb(8 145 178); }     /* cyan-600 */

html:not(.app-dark) .win11-root .text-teal-200,
html:not(.app-dark) .win11-root .text-teal-300,
html:not(.app-dark) .win11-root .text-teal-400    { color: rgb(13 148 136); }    /* teal-600 */

html:not(.app-dark) .win11-root .text-indigo-200,
html:not(.app-dark) .win11-root .text-indigo-300,
html:not(.app-dark) .win11-root .text-indigo-400  { color: rgb(79 70 229); }     /* indigo-600 */

html:not(.app-dark) .win11-root .text-violet-200,
html:not(.app-dark) .win11-root .text-violet-300,
html:not(.app-dark) .win11-root .text-violet-400  { color: rgb(124 58 237); }    /* violet-600 */

html:not(.app-dark) .win11-root .text-pink-200,
html:not(.app-dark) .win11-root .text-pink-300,
html:not(.app-dark) .win11-root .text-pink-400    { color: rgb(219 39 119); }    /* pink-600 */

html:not(.app-dark) .win11-root .text-rose-200,
html:not(.app-dark) .win11-root .text-rose-300,
html:not(.app-dark) .win11-root .text-rose-400    { color: rgb(225 29 72); }     /* rose-600 */

html:not(.app-dark) .win11-root .text-red-200,
html:not(.app-dark) .win11-root .text-red-300,
html:not(.app-dark) .win11-root .text-red-400     { color: rgb(220 38 38); }     /* red-600 */

html:not(.app-dark) .win11-root .text-orange-200,
html:not(.app-dark) .win11-root .text-orange-300,
html:not(.app-dark) .win11-root .text-orange-400  { color: rgb(234 88 12); }     /* orange-600 */

html:not(.app-dark) .win11-root .text-yellow-200,
html:not(.app-dark) .win11-root .text-yellow-300,
html:not(.app-dark) .win11-root .text-yellow-400  { color: rgb(202 138 4); }     /* yellow-600 */

html:not(.app-dark) .win11-root .text-fuchsia-200,
html:not(.app-dark) .win11-root .text-fuchsia-300,
html:not(.app-dark) .win11-root .text-fuchsia-400 { color: rgb(192 38 211); }    /* fuchsia-600 */

html:not(.app-dark) .win11-root .text-lime-200,
html:not(.app-dark) .win11-root .text-lime-300,
html:not(.app-dark) .win11-root .text-lime-400    { color: rgb(101 163 13); }    /* lime-600 */
</style>
