SGG - Sistema de Gestão de Gabinete
Schema: v4 + Blocos 2-14 + Hotfix 10.1/10.2/10.1.1 + 11.1/11.2 + Geo 1.0/1.1
       + Storage Refactor + Geo Territorial/Completo + CEP + Bloco 12
       + Hotfix 12.1 + Bloco 13 + Bloco 14 + Hotfix UX 14.1 + 14.2
       + Importação de Cidadãos + Hotfix 14.2.1 (Demanda Rápida: câmera Android + preview)
       + Hotfix Título de Mídia (protocolo prefixado + migration 019)
       + Hotfix 14.2.2 (Câmera Android/Samsung A07: inputs estáticos especializados)
       + Bloco Registro de Evolução da Demanda (Situação inicial/final + migration 020)
       + Hotfix Complementação Evolução (resultado/status, observação visível, metadados de mídia, permissão equipe_rua, flash de erro, limite de mídias)
       + Bloco 15 — Central de Comunicação e Marketing (migration 021)
       + Bloco 16 — Consolidação da Plataforma / Auditoria Geral v1.0
       + Hotfix 16.1 — Cadastro Rápido de Cidadão na Demanda Rápida (modal AJAX)
       + Hotfix 16.1.1 — Correção definitiva do modal Novo Cidadão (cascata CSS × atributo hidden)
       + Hotfix 16.1.2 — Complementação do Cadastro Rápido de Cidadão
       + Hotfix 16.1.3 — Atualização do Modelo de Importação de Cidadãos
       + Hotfix 16.1.5 — Filtros avançados na listagem de cidadãos
       + Hotfix 16.1.6 — Busca inteligente e localização avançada de cidadão na
         Demanda Rápida
       + Hotfix 16.1.7 — Quantidade de cidadãos por página
       + Hotfix 16.1.7.1 — Aplicação automática da quantidade por página
         (este release)
Bloco 14 — Inteligência do Gabinete:
 - Nova área /intelligence (protegida por reports.view) que transforma dados
   existentes em decisão, sem criar cadastros.
 - Indicadores executivos: demandas abertas/resolvidas/atrasadas/vencendo,
   visitas, tarefas abertas/vencidas, cidadãos, por mês e por categoria.
 - Ranking de bairros (reusa Territory::byNeighborhood) e de secretarias/órgãos
   (novo: total, abertas, resolvidas, atrasadas, tempo médio de resolução).
 - Produtividade por assessor (demandas atribuídas/resolvidas/atrasadas, visitas,
   tarefas) e filtro individual por assessor.
 - Heatmap de demandas (reusa leaflet.heat + Territory::heatPoints; coordenada da
   demanda, depois do bairro, depois centro de Uberlândia).
 - Metas mensais simples (demandas resolvidas, visitas, tarefas) com realizado e
   percentual — nova tabela goals (migration 018).
 - Botão "🚗 Abrir no Waze" na tela da demanda (coordenada da demanda ou do
   bairro; some quando não há coordenada). Botão "Ver no mapa" preservado.
 - Item de menu "Inteligência do Gabinete" na seção Gestão.
 - Nenhuma tela existente removida; SLA, demanda tradicional, demanda rápida e
   mapa intactos. Total de tabelas: 53 (52 + goals).
Hotfix UX 14.1 — Bottom Navigation profissional (mobile):
 - Barra inferior redesenhada: Painel · Demandas · [Nova Demanda] · Cidadãos · Mais.
 - Botão central flutuante (FAB) circular 64px, elevado, sombra, cor do sistema,
   ícone de raio + texto "Nova Demanda", abrindo /demands/quick (rota existente).
 - Item "Demanda Rápida" removido da barra (substituído pelo FAB central).
 - Menu "Mais": bottom sheet que abre de baixo para cima com Perfil, Agenda,
   Documentos, Aniversários, Mapa, Relatórios, Configurações e Sair — cada item
   respeita as permissões existentes. Sair via POST+CSRF.
 - safe-area-inset-bottom (sem sobreposição com notch), área de toque >=44px,
   suporte a tema claro/escuro. Apenas front-end: sem mudança de banco, rotas,
   permissões ou regras de negócio.
Hotfix UX 14.2 — Refino final mobile/PWA (iPhone/Safari):
 - Safe-area da barra inferior com max(10px, env(safe-area-inset-bottom)) e
   espaço final do conteúdo calc(96px + safe-area) — botões finais não ficam
   mais escondidos atrás da barra nem colidem com os controles do Safari.
 - Botão central "Nova Demanda" refinado (alinhamento, toque >=64px, sem corte).
 - Bottom sheet "Mais": altura máxima 80vh com scroll interno, header com título
   e botão X, fecha por toque fora/ESC/X, respeita a safe-area. Itens novos:
   Cargos e Permissões e Auditoria (quando permitido). Sair segue POST+CSRF.
 - Meta tags PWA Apple (apple-mobile-web-app-capable/status-bar-style/
   mobile-web-app-capable); viewport-fit=cover preservado sem duplicar.
 - Apenas layout/CSS/JS. Sem mudança de banco, rotas, permissões ou regras.
Data: 2026-06-24
Módulo Importação de Cidadãos por Planilha (CSV):
 - Nova área /citizens/import (permissão citizens.create) com botão na listagem.
 - Download de modelo CSV (UTF-8, separador ';', com linha de exemplo).
 - Upload .csv (máx 5MB, máx 5000 linhas, MIME básico, cabeçalhos obrigatórios).
 - Validação por linha: nome obrigatório, e-mail, data (DD/MM ou DD/MM/AAAA),
   bairro existente, telefone normalizado; dedup interno e contra o banco.
 - Prévia em sessão (sem tabela nova) com resumo e status por linha (OK/ERRO/
   DUPLICADO) e motivo. Confirmar importa só os válidos; cancela limpa.
 - Origem "Importação" garantida com slug='importacao' (busca por slug, insere
   slug/name/is_active/sort_order). SEM fallback: se não garantir a origem, NÃO
   importa e mostra erro claro.
 - Reaproveita Citizen::create e ConsentLog; consent_channel=importacao; auditoria.
 - CSV nativo (fgetcsv), sem Composer/XLSX/Node. instagram->notes, whatsapp->phone.

Bloco — Registro de Evolução da Demanda (este release):
 - "Mídias vinculadas" da tela da demanda passa a "Situação inicial"; mídias
   antigas sem classificação são tratadas como 'initial' (backfill migration 020).
 - Nova seção "Situação final" com Tirar foto / Gravar vídeo / Escolher da
   galeria, preview, remoção individual e observação da situação final. Reusa
   EXATAMENTE a arquitetura estática de câmera validada no Samsung A07 (inputs
   especializados + input final único name="media[]" via DataTransfer; sem
   input único com capture dinâmico). Mídias salvas com estágio 'final'.
 - Título das mídias da situação final: "PROTOCOLO — Situação final — TÍTULO"
   (reusa buildDemandMediaTitle; idempotente, não duplica protocolo).
 - Observação geral da situação final: registrada no histórico auditável da
   demanda (Demand::addComment + Audit::log). Descrição individual reusa
   media_files.description. Nenhum campo semanticamente duplicado.
 - Upload de "Anexos" movido visualmente para dentro de "Documentos" (tabela e
   arquivos de attachments preservados). Documentos oferece "Gerar documento
   desta demanda" e "Adicionar documento/arquivo". Seção isolada "Anexos" some.
 - Nova coluna media_files.demand_stage VARCHAR(20) DEFAULT 'initial' + índice
   idx_media_demand_stage (migration 020, idempotente). RBAC, storage externo,
   Demanda Rápida, câmera Android/iPhone, mapa, Waze, agenda, tarefas e
   comentários preservados e inalterados.

PHP: 8.0+    Banco: MySQL 8.0+ / MariaDB 10.4+

Hotfix 16.1.7.1 — Aplicacao automatica da quantidade por pagina (este release):
 - Ajuste minimo: UM UNICO arquivo alterado (Views/citizens/index.php). SEM
   migration, SEM banco, SEM controller, SEM model, SEM rota, SEM permissao e
   SEM biblioteca externa.
 - AUDITORIA: o listener de "change" do seletor JA EXISTIA desde o 16.1.7 e o
   auto-submit ja funcionava. O botao "Aplicar" era apenas fallback sem
   JavaScript e vinha sendo OCULTADO por JS (go.hidden = true) — no celular ele
   extrapolava a largura da tela antes de ser escondido.
 - 4 REMOCOES DE CODIGO: (1) o elemento <button id="czPerPageGo">Aplicar</button>
   foi REMOVIDO do HTML, nao ocultado por CSS; (2) a regra CSS orfa
   .cz-perpage__go; (3) a variavel "go"; (4) a linha go.hidden = true.
 - O listener de "change" NAO foi tocado nem duplicado: continua sendo o UNICO,
   garantindo 1 alteracao = 1 submissao. Sem setTimeout, sem workaround.
 - LARGURA NO MOBILE (media query <=600px): o bloco do seletor caiu de ~318px
   para ~234px. Num iPhone SE (320px, ~288px uteis) ele ESTOURAVA e agora cabe
   com folga; tambem cabe em 360px (Samsung A07), 390px e 412px. Preservados
   min-height:44px, max-width:110px e order:3 — o select segue facil de tocar e
   "Importar Planilha"/"Novo cidadao" seguem acessiveis. Nenhuma largura fixa em
   px, nenhum position:fixed/z-index/overflow-x novo: navegacao inferior e
   safe-area intactas.
 - SEM JAVASCRIPT: conforme decidido para este hotfix, o fallback com botao NAO
   foi recriado; sem JS o seletor permanece visivel, porem sem auto-submit.
 - TESTES: php -l OK, node --check OK, 95 casos / 95 PASS — ausencia do botao no
   DOM, no CSS e no HTML servido; 1 alteracao = exatamente 1 submissao (e o
   evento "input" nao submete); as seis transicoes 250->25, 25->50, 50->100,
   100->250, 250->500 e 500->25 submetendo com o per_page correto; "page"
   ausente do formulario (retorno a pagina 1 a partir de um cenario real de 187
   paginas); preservacao de busca, bairro, lideranca, consentimento e WhatsApp
   isolados e combinados; as cinco quantidades ainda renderizando 25/50/100/250/
   500 linhas; contador e intervalo preservados; larguras de 320 a 412px.
 - NAO-REGRESSAO: diff -rq recursivo acusa 1 arquivo alterado (+ VERSION/
   CHANGELOG), 0 novos e 0 removidos — 264 arquivos antes e 264 depois.
   Citizen.php, CitizenController.php, routes.php, app.css, demands/quick.php,
   citizens/form.php, citizens/import.php, config.php, .htaccess, database.sql,
   sw.js e manifest.json byte a byte. Whitelist, padrao 250, filtros, busca,
   paginacao, contador, chips e toggle de filtros preservados. Demanda Rapida,
   autocomplete, modal Procurar, "+ Novo", camera, importacao, Central de
   Comunicacao e PWA intactos.

Hotfix 16.1.7 — Quantidade de cidadaos por pagina:
 - SEM migration, SEM alteracao de banco, SEM rota nova, SEM permissao nova,
   SEM indice novo e SEM biblioteca externa. Apenas 3 arquivos alterados:
   Models/Citizen.php, Controllers/CitizenController.php e
   Views/citizens/index.php.
 - LIMITE ANTERIOR: 20 registros por pagina. O controller chamava
   Citizen::paginate($filters, $page) SEM o 3o argumento, herdando o default do
   model. Com 1.600+ cidadaos reais isso dava ~81 paginas.
 - PARAMETRO GET: per_page (nao havia convencao previa no sistema).
   Whitelist explicita Citizen::PER_PAGE_OPTIONS = [25, 50, 100, 250, 500] e
   padrao Citizen::PER_PAGE_DEFAULT = 250.
 - VALIDACAO NO SERVIDOR: normalizePerPage() so aceita inteiro ou string de
   digitos puros e confronta com in_array estrito. Qualquer outro valor — 0, -1,
   1, 20, 999, 999999, abc, 1e3, 25.5, 0x19, 250abc, "25 OR 1=1",
   "25' OR '1'='1", "25;DROP TABLE citizens", array, bool — cai em 250. O
   <select> do HTML NAO e fonte confiavel.
 - TETO NO MODEL (defesa em profundidade): LIMIT/OFFSET sao interpolados, entao
   paginate() aplica max(1, min(500, $perPage)) e max(1, $page). Nenhuma chamada
   consegue pedir mais de 500 linhas. A assinatura mantem $perPage = 20 por
   retrocompatibilidade.
 - PRESERVACAO DE PARAMETROS: a view monta um $baseParams unico (busca, bairro,
   lideranca, consentimento, WhatsApp e per_page), reaproveitado nos links de
   paginacao, nos links de remocao de chip e nos campos ocultos do seletor. O
   formulario de busca/filtros ganhou hidden per_page.
 - VOLTA A PAGINA 1: o formulario do seletor deliberadamente NAO envia "page".
   Trocar 25 -> 500 recomeca na pagina 1 (com 25 havia 209 paginas; com 500,
   apenas 11), evitando cair numa pagina que deixou de existir.
 - INTERFACE: na toolbar ja existente, ao lado do contador —
   "5208 cidadao(s) encontrado(s) · Exibindo 1-250   Exibir [250] por pagina".
   O total continua sendo o total do filtro, nao o que cabe na tela.
 - SEM JAVASCRIPT: o seletor e um formulario GET nativo com botao "Aplicar" que
   funciona sozinho. O JS apenas aplica a troca no evento change e, por isso,
   esconde o botao.
 - MOBILE: select e botao com min-height 44px; abaixo de 600px o seletor ocupa a
   linha inteira com order:3, indo para baixo em vez de empurrar "Importar
   Planilha" e "Novo cidadao" para fora da tela; select com max-width 110px.
   Nenhuma largura fixa em px — sem risco de scroll horizontal.
 - PERFORMANCE (5.208 cidadaos, conexao aquecida): 25 -> 1,5ms; 50 -> 1,5ms;
   100 -> 8,9ms; 250 -> 9,1ms; 500 -> 10,8ms. SEMPRE 2 queries (COUNT + SELECT),
   independentemente da quantidade e dos filtros: nenhuma query N+1 nova.
   Ultima pagina (OFFSET maximo): 11,8ms. Nenhum cache, nenhum indice novo.
 - TESTES (PHP 8.3.6 + MariaDB 10.11, banco real): php -l 3/3, node --check OK,
   201 casos / 201 PASS — padrao 250 sem parametro; 25/50/100/250/500 conferidos
   contra o banco; ultima pagina com o resto; busca, bairro, lideranca,
   consentimento e WhatsApp isolados e combinados com totais conferidos por
   COUNT independente; pagina 2 sem sobreposicao; preservacao de per_page em
   paginacao, chips e filtros; retorno a pagina 1 ao trocar a quantidade; todos
   os payloads maliciosos; regra de WhatsApp (consent_contact=1 AND
   whatsapp_opt_out=0) nao invertida; isolamento por tenant e deleted_at com
   per_page=500; mobile.
 - NAO-REGRESSAO: diff -rq recursivo acusa 3 arquivos alterados (+ VERSION/
   CHANGELOG), 0 novos e 0 removidos — 264 arquivos antes e 264 depois. 13
   metodos do model e 10 do CitizenController byte a byte; routes.php,
   demands/quick.php, citizens/form.php, citizens/import.php e os controllers de
   importacao, Demanda Rapida, demandas e comunicacao inalterados. Autocomplete,
   modal Procurar, "+ Novo", camera, importacao, aniversariantes, Central de
   Comunicacao e PWA intactos.

Hotfix 16.1.6 — Busca inteligente e localizacao avancada de cidadao na Demanda Rapida:
 - SEM alteracao de banco e SEM migration: nickname, phone, email, city,
   neighborhood_id e a cadeia neighborhoods.region_id -> regions ja existiam; os
   indices idx_citizens_name/_phone/_neigh atendem. Nenhum indice novo.
   NAO existe coluna whatsapp separada — phone E o campo de WhatsApp (a
   importacao mapeia whatsapp->phone), portanto pesquisar phone ja cobre o
   requisito de busca por WhatsApp.
 - PROBLEMA ELIMINADO: citizenOptions() carregava ate 500 cidadaos por abertura
   da tela (LIMIT 500) e os materializava como <option>. Medido com 5.208
   cidadaos: 4.708 (90,4%) eram INALCANCAVEIS — quem estivesse alem do 500o nome
   nao existia para a equipe de rua, que recadastrava duplicata pelo "+ Novo".
 - ENDPOINT NOVO: GET /citizens/lookup -> CitizenController@lookup. Auditoria
   confirmou que nao havia equivalente (/map/search e /transparencia/pesquisa
   consultam outros dominios). Registrado ANTES das rotas com parametro; nao e
   sombreado por /citizens/{id}/edit (comprovado com o Router real).
   Protecao: autenticado + citizens.view + tenant + deleted_at IS NULL. A
   permissao e checada no corpo do metodo para responder JSON (nao HTML 403).
   Modos: suggest (12 resultados, SEM COUNT — pede perPage+1 e detecta excesso
   pela linha sobrando) e search (20/pagina COM total, para paginar).
 - SQL 100% parametrizado: pesquisa name, nickname, phone e email; com 2+
   digitos compara o telefone SO por digitos (3432324545 encontra
   (34) 3232-4545, independente da mascara). Territorio por LEFT JOIN
   neighborhoods + regions; cidade efetiva = COALESCE(NULLIF(c.city,''),n.city).
   Curingas do LIKE escapados: o termo e literal ("%" nao varre a base).
   LIMIT/OFFSET sao os unicos interpolados e sao inteiros validados (teto 50).
 - INTERFACE: o <select> pre-carregado deu lugar a input de busca + hidden
   citizen_id (o backend da demanda recebe citizen_id exatamente como antes):
   [ Digite nome, apelido ou telefone... ] [ Procurar ] [ + Novo ]
   * Autocomplete: 2+ caracteres, debounce 300ms, numero de sequencia
     descartando respostas fora de ordem (race condition), teclado ↑/↓/Enter/ESC.
     Cada opcao mostra nome (apelido), telefone e bairro • cidade • regiao.
     Com excesso: "Muitos resultados encontrados. Continue digitando ou use
     Procurar."
   * Modal "Procurar": filtros por nome/apelido/telefone, bairro, cidade e
     regiao (listas oficiais, sem texto livre), 20 por pagina com "Carregar
     mais" acumulativo e contador "Exibindo X de Y".
   * Cidadao escolhido em destaque com telefone e territorio + botao "Trocar".
 - LOCALIZACAO AVANCADA = BUSCAR por informacao territorial. A localizacao da
   OCORRENCIA nunca e tocada: coordenada do cidadao NAO e copiada para a demanda
   e o bairro do cidadao NAO e vinculado ao bairro da ocorrencia (comprovado).
 - "+ NOVO" PRESERVADO (16.1/16.1.1/16.1.2 intactos). Apenas pick() foi
   reapontado para a funcao compartilhada window.sggCitizenPick; quickStore
   passou a devolver tambem "meta" (telefone • bairro • cidade • regiao) via
   Citizen::lookupOne, para exibir o recem-cadastrado ja identificado. Ele ja
   esta no banco, logo e encontravel pelo autocomplete sem recarregar a pagina.
 - ERRO DE VALIDACAO: viewData() nao carrega mais lista alguma;
   selectedCitizen() busca UMA linha por ID (Citizen::lookupOne, escopada por
   tenant e deleted_at IS NULL) e a tela reexibe o cidadao escolhido.
 - PERMISSOES: sem citizens.create some o "+ Novo" e a busca segue funcionando
   (o CSS compartilhado do overlay, incluindo o fix 16.1.1 .qc-overlay[hidden],
   e declarado no bloco alternativo); sem citizens.view o campo fica desabilitado
   com aviso e o "+ Novo" permanece. Nenhuma permissao nova.
 - PERFORMANCE (5.208 cidadaos): abertura da tela passa de 5,5ms/500 linhas/
   23,3KB de <option> para ZERO consulta, ZERO linha, ZERO KB. Autocomplete
   4,7ms; modal 1a pagina + total 8,3ms; reexibir apos erro 0,4ms (1 linha).
   EXPLAIN usa idx_citizens_name. Escala sem aumentar LIMIT.
 - TESTES (PHP 8.3.6 + MariaDB 10.11, banco real): php -l 5/5, node --check OK,
   231 casos / 231 PASS — nome, apelido, telefone (com e sem mascara), exibicao
   completa, homonimos, vazio, debounce, race condition, falha de rede, modal e
   filtros de bairro/cidade/regiao, combinacoes, paginacao sem sobreposicao,
   selecao/remocao/troca, "+ Novo" com selecao automatica e duplicidade, erro de
   validacao preservando o cidadao, preservacao de fotos/videos/localizacao e
   demais campos, 5.208 cidadaos, citizens.view/create, isolamento por tenant,
   SQL parametrizado, mobile (alvos >=44px, safe-area, sem scroll horizontal).
 - CAMERA byte a byte identica: HTML md5 dbaf9634c9a664acee62b517b80d4f0c
   (3.180 bytes) e JS md5 43b74ccfb6d9997c87193f9b8c725abb (7.300 bytes); os 4
   inputs e o DataTransfer inalterados. Demanda tradicional, importacao,
   listagem de cidadaos, aniversariantes, Central de Comunicacao, mapa, PWA e
   LGPD intactos.

Hotfix 16.1.5 — Filtros avançados na listagem de cidadãos:
 - SEM alteracao de banco e SEM migration: as colunas ja existiam
   (neighborhood_id, is_community_leader, consent_contact, whatsapp_opt_out) e os
   indices necessarios tambem (idx_citizens_tenant/_neigh/_leader/_optout/_name).
   Nenhum indice novo: booleano de baixa seletividade nao justifica.
 - Listagem /citizens ganha filtros combinaveis com a busca atual (nome/telefone/
   email), todos aplicados no BACKEND com consulta parametrizada (nenhum valor de
   GET entra cru no SQL), escopados por tenant e deleted_at IS NULL:
     * Bairro (neighborhood_id): inteiro validado no controller
       (Citizen::neighborhoodExists confirma existencia + tenant; ID forjado e
       descartado). Lista oficial de neighborhoods, sem texto livre.
     * Lideranca comunitaria (community_leader): ''/1/0 => is_community_leader.
     * Consentimento de contato (contact_consent): ''/1/0 => consent_contact.
     * WhatsApp (whatsapp_status): allowed => consent_contact=1 AND
       whatsapp_opt_out=0 (regra segura, NAO invertida); optout =>
       whatsapp_opt_out=1.
 - Consentimento x opt-out permanecem INDEPENDENTES: whatsapp_opt_out=0 NAO
   autoriza WhatsApp por si; "Pode receber WhatsApp" exige consentimento E
   ausencia de opt-out.
 - Interface: busca mantida no topo; botao "Filtros" (aria-expanded/aria-controls)
   abre area recolhivel (desktop 4 colunas; mobile 1 coluna, campos/botoes >=44px,
   sem rolagem horizontal); botoes Filtrar e Limpar tudo; chips de filtros ativos
   com remocao individual (preservando os demais parametros) e link Limpar filtros.
 - Contador passa a exibir o total FILTRADO ("N cidadao(s) encontrado(s)") e a
   paginacao usa esse total; parametros preservados na paginacao via
   http_build_query; trocar filtro volta a pagina 1; botao Voltar restaura estado.
 - Coluna "Contato" deixa de exibir "Autoriza" como se fosse WhatsApp: badges
   separados "Contato autorizado"/"Sem consentimento" e "WhatsApp permitido"
   (consent + sem opt-out) / "Nao enviar WhatsApp" (opt-out).
 - Permissoes inalteradas (citizens.view na listagem; update/delete nas acoes);
   nenhuma permissao nova. Cadastro, importacao, Demanda Rapida, camera, banco de
   bairros, aniversariantes, Central de Comunicacao, PWA e LGPD intactos.
 - Retrocompat: Citizen::paginate() aceita array de filtros OU string (tratada
   como q); unico chamador (CitizenController) atualizado.
 - Testes (MariaDB 10.11 + PHP 8.3): php -l OK (3 arquivos); 21/21 casos de
   consulta PASS (busca, cada filtro, combinacoes, busca+filtros, combinacao
   vazia, isolamento por tenant, parametros invalidos ignorados); paginacao com
   filtro preservada entre paginas; EXPLAIN usa indice existente.

Hotfix 16.1.3 — Atualização do Modelo de Importação de Cidadãos:
 - SEM alteração de banco e SEM migration: latitude, longitude e whatsapp_opt_out
   ja existiam na tabela citizens. Faltava apenas expo-las no modelo e trata-las
   no importador (whatsapp_opt_out estava fixo em 0; coordenadas nao eram lidas).
 - Modelo CSV passa de 17 para 20 colunas, na ordem: nome, apelido, telefone,
   whatsapp, email, instagram, data_nascimento, cep, endereco, numero,
   complemento, bairro, cidade, latitude, longitude, origem,
   lideranca_comunitaria, consentimento_contato, whatsapp_opt_out, observacoes.
 - Linha de exemplo agora usa data SEM ano (15/03), conforme a arquitetura do
   SGG (birth_day/birth_month). Instrucoes objetivas incluidas no proprio CSV
   em linhas iniciadas por '#', que o importador ignora automaticamente.
 - COMPATIBILIDADE: a validacao do arquivo enviado passa a exigir apenas as 17
   colunas antigas (REQUIRED_HEADERS). Planilhas anteriores continuam sendo
   importadas: coordenadas ficam NULL e opt-out assume o padrao seguro 0.
 - latitude/longitude: opcionais, aceitam negativo, ponto ou virgula decimal,
   espacos removidos; limites -90..90 e -180..180; exigem o par completo
   (coordenada isolada e rejeitada com mensagem clara); texto/data invalidos sao
   recusados; vazio => NULL, sem impedir a importacao.
 - whatsapp_opt_out: sim/1/true/yes => 1 (nao deseja WhatsApp); nao/0/false/no
   => 0; vazio => 0; valor desconhecido gera erro por linha. E independente de
   consentimento_contato (LGPD) — consentir contato nao autoriza WhatsApp.
 - Aniversario: combinacoes impossiveis passam a ser rejeitadas (31/02, 31/04,
   00/10, 15/13) via checkdate(); 29/02 aceito; arquivos antigos com ano
   (15/03/1980) seguem aceitos, com o ano descartado.
 - consent_logs: alem do consentimento LGPD (granted), o opt-out importado passa
   a ser registrado como 'updated', ambos com canal 'importacao'.
 - Seguranca: neutralizacao de CSV/Formula injection (=, +, -, @) nos campos de
   texto livre importados. Bairro continua sendo apenas associado a lista
   oficial, nunca criado automaticamente.

Hotfix 16.1.2 — Complementação do Cadastro Rápido de Cidadão (este release):
 - SEM alteração de banco e SEM migration: auditoria comprovou que TODAS as
   colunas já existiam (cep, city, birth_day, birth_month, consent_contact,
   whatsapp_opt_out, latitude, longitude, email) e que collect() já coletava
   os 21 campos. Faltavam apenas os inputs no HTML do modal.
 - Modal ampliado com paridade TOTAL de campos com a ficha oficial (19 campos,
   mesmos name=, mesmas colunas, mesmo model/controller/validações), em seções:
   Dados do cidadão / Contato / Endereço e território / Aniversário /
   Consentimentos / Localização do cidadão / Observações. Duas colunas no
   desktop, uma no mobile (Samsung A07), inputs >=44px, scroll interno,
   safe-area preservada.
 - CEP: reutiliza o módulo global sggCep() (mesma busca do cadastro completo),
   apenas com IDs do modal. Cidade permanece "Uberlândia" readonly, como na
   ficha oficial. Bairro usa a lista oficial (select) e só é selecionado quando
   há correspondência — nunca cria bairro novo.
 - Aniversário: apenas dia e mês (sem ano), selects, preenchimento parcial
   permitido como já era. Validação de combinação impossível (31/02, 31/04)
   adicionada ao validate() COMPARTILHADO, beneficiando também o cadastro
   completo, via checkdate() do core (aceita 29/02).
 - Localização do cidadão: botão "Usar minha localização" reutilizando
   sggGeolocate() com IDs próprios (qc_latitude/qc_longitude). A localização da
   DEMANDA (occur_latitude/occur_longitude) nunca é tocada — comprovado: zero
   colisão de IDs entre modal e formulário da demanda.
 - Validações de servidor acrescentadas (não apenas JS): limites de latitude
   (-90..90) e longitude (-180..180) e bairro existente no tenant (bloqueia ID
   forjado). LGPD e opt-out de WhatsApp permanecem campos independentes, com
   registro em consent_logs pelo fluxo normal.
 - Preservados: fix 16.1.1 (.qc-overlay[hidden]{display:none}), duplicidade
   (telefone normalizado / nome+bairro, Selecionar / Cadastrar mesmo assim),
   endpoint POST /citizens/quick-store, RBAC citizens.create + CSRF, e a
   arquitetura da câmera (JS e inputs byte a byte inalterados).

Hotfix 16.1.1 — Correção definitiva do modal "Novo Cidadão" (este release):
 - CAUSA RAIZ: o modal era ocultado pelo atributo HTML hidden, que funciona via
   folha de estilo do NAVEGADOR ([hidden]{display:none}). Como na cascata CSS
   toda declaração de autor vence a do navegador, o display:flex declarado em
   .qc-overlay sobrepunha o hidden — o modal aparecia sozinho ao abrir a tela e
   não fechava (X/Cancelar/ESC/clique fora setavam hidden=true corretamente,
   mas o CSS mantinha o overlay visível).
 - CORREÇÃO (1 linha de CSS, sem tocar JavaScript): .qc-overlay[hidden]
   {display:none} — origem autor e especificidade maior (0,2,0 > 0,1,0),
   sem !important, sem workaround, sem setTimeout. O atributo hidden volta a
   comandar o estado; o JS existente (que já alternava hidden) passa a
   funcionar integralmente: abre no "+ Novo", fecha por X, Cancelar, ESC,
   clique no overlay, após cadastrar e após selecionar existente; scroll
   restaurado; nenhum elemento invisível bloqueando a tela.
 - Prova empírica (DOM real via jsdom, com o CSS extraído do próprio arquivo):
   carga=none (oculto), abrir=flex, fechar=none, reabrir/fechar=none.
 - JS da câmera e JS do modal byte-idênticos à base; nenhum outro módulo tocado.

Hotfix 16.1 — Cadastro Rápido de Cidadão na Demanda Rápida (este release):
 - Botão "＋ Novo" ao lado do select Cidadão (visível apenas com permissão
   citizens.create). Abre modal SEM sair da tela, sem redirect e sem reload —
   nenhum dado da demanda (categoria, prioridade, descrição, fotos, vídeos,
   localização) é tocado: o modal fica FORA do #quickForm, com form próprio.
 - Cadastro via AJAX (POST /citizens/quick-store, CSRF + RBAC). REUSA
   collect()/validate()/Citizen::create/Audit/ConsentLog do cadastro normal —
   nenhuma estrutura duplicada. Campos: os MESMOS existentes no Cadastro de
   Cidadãos (nome*, telefone/WhatsApp*, bairro, endereço, número, complemento,
   origem, apelido, liderança comunitária, observações). Obs.: a ficha de
   cidadão não possui campos Instagram/Facebook — não foram criados, seguindo
   a regra de não criar segunda estrutura nem alterar o cadastro completo.
 - Telefone obrigatório no fluxo rápido. Duplicidade verificada antes de criar:
   telefone (comparado só por dígitos, independente de máscara) OU nome+bairro.
   Ao encontrar semelhantes: lista com [Selecionar] (usa o cadastro existente)
   ou [Cadastrar mesmo assim] (force).
 - Após salvar: mensagem "✓ Cidadão cadastrado com sucesso", option inserida no
   select, cidadão selecionado automaticamente, modal fecha; usuário continua
   exatamente onde estava.
 - Mobile (Samsung A07): modal em tela cheia (100dvh), inputs/botões grandes,
   safe-area. Câmera e fluxo da demanda 100% preservados (JS e inputs da câmera
   byte-idênticos à base).

Bloco 16 — Consolidação / Auditoria Geral da Plataforma (este release):
 - Auditoria técnica completa (arquitetura, banco, segurança, performance, RBAC,
   auditoria, uploads, CSS/JS, mobile, UX, acessibilidade). NÃO cria módulos nem
   altera regras de negócio. Apenas melhorias de ganho técnico real, sem regressão.
 - Segurança (hardening, defesa em profundidade): MediaController::download e
   ::raw passam a confinar o arquivo servido à raiz de storage via realpath +
   Storage::isSafe() (mesma proteção já usada em downloadAttachment), prevenindo
   path traversal caso storage_path venha corrompido. Sem mudança de comportamento
   para arquivos legítimos.
 - Auditoria: tentativas de login malsucedidas passam a gerar log
   ('login_failed', só o e-mail tentado, nunca a senha) para observabilidade de
   segurança / detecção de brute-force.
 - Performance: eliminado N+1 na listagem da Central de Comunicação. As
   miniaturas Antes/Depois de todos os itens da página passam a ser buscadas em
   2 queries (uma por estágio) via Communication::beforeAfterThumbs(), no lugar
   de 2×N queries. Além de mais rápido, prioriza imagem para a miniatura.
 - Verificações sem alteração (já conformes): CSRF em 100% dos POST (81/81),
   escape de saída via View::e(), validação de upload por finfo + whitelist de
   MIME/extensão + limite de tamanho, headers de segurança e cookies de sessão
   (httponly/samesite/secure), RBAC em todas as rotas protegidas, idempotência
   de todas as 21 migrations, instalação limpa (database.sql → migrations → OK).
 - Câmera Samsung A07, Demanda Rápida, storage, mídias, uploads e CSS/identidade
   visual preservados byte a byte.

Bloco 15 — Central de Comunicação e Marketing (este release):
 - Novo módulo /communication: fluxo de trabalho de comunicação institucional,
   integrado às demandas mas com estrutura, status, permissões e auditoria
   PRÓPRIOS. Não altera a demanda nem duplica dados (bairro/região/solicitante/
   secretaria vêm por JOIN; UNIQUE por demand_id impede duplicação).
 - Encaminhamento: checkbox "Encaminhar para Comunicação" na Situação final da
   demanda cria automaticamente um communication_item (registra usuário, data,
   protocolo, vínculo). Aditivo; nunca duplica a demanda.
 - Tela principal: cards com Antes/Depois (miniaturas), protocolo, título,
   bairro, região, solicitante, secretaria, responsável, data, status próprio.
 - Status próprios configuráveis (Recebido…Publicado…Cancelado); NÃO alteram o
   status da demanda. Checklist de produção (11 itens) com progresso.
   Responsável atribuível. Publicações por canal (Instagram Feed/Story/Reels,
   Facebook, WhatsApp, Portal, YouTube, TikTok) com status/data/usuário/link.
   Observações internas (só Comunicação). Histórico e auditoria completos.
 - Dashboard: encaminhadas/recebidas/em produção/prontas/publicadas/arquivadas,
   Antes/Depois completos, com/sem vídeo, tempo médio até publicação. Alertas
   automáticos. Filtros: bairro, região, secretaria, responsável, status,
   período, publicado/não, antes/depois completo, com/sem vídeo, pendente mídia.
 - 6 permissões próprias (communication.view/create/update/assign/publish/
   archive), NÃO reutiliza permissões de demanda. Menu lateral "Comunicação" +
   item no menu mobile "Mais". Layout responsivo.
 - 6 tabelas novas (communication_items, _statuses, _status_history, _checklists,
   _publications, _assignments) via migration 021 (idempotente). Não implementa
   publicação automática/integrações/IA — só a arquitetura escalável.
 - Câmera Samsung A07, Demanda Rápida, mídia, storage e demais módulos intactos.

Hotfix Complementação da Evolução da Demanda (este release):
 - Sem mudança de banco: nenhuma migration nova, demand_stage inalterado.
 - Resultado da demanda: seletor "Resultado da demanda" na Situação final usando
   os status configuráveis existentes (demand_statuses), exibindo só desfechos
   relevantes (finais + Aguardando retorno + Parcialmente/Não resolvida quando
   cadastrados). Opcional: "Manter status atual" ou atualizar; ao atualizar,
   registra histórico e auditoria. Sem campo duplicado.
 - Observação final agora visível na própria seção (texto, data e usuário),
   sem obrigar a procurar no histórico (Demand::latestFinalSituation).
 - Cada mídia final exibe título, data/hora, usuário e descrição (quando houver),
   em layout compacto no mobile.
 - "Editar observação" por mídia: link para a tela de edição de mídia já
   existente (/media/{id}/edit), que edita media_files.description. Sem coluna
   nova.
 - Permissões corrigidas: registrar situação final exige media.upload +
   media.link (que a Equipe de Rua já possui) — NÃO exige mais demands.update.
   A alteração de status só é oferecida/aplicada a quem tem demands.update.
   A Equipe de Rua volta a conseguir registrar evolução sem acesso de edição
   total da demanda. Nenhuma permissão nova criada.
 - Flash de permissão negada agora usa Response::forbidden (403) e mensagens de
   falha usam Session::flash('error', ...) — tipo já existente — em vez de
   'success'. A tela da demanda passa a renderizar alert--error.
 - Limite de mídias: carrega até 200 por estágio e, se houver mais, exibe aviso
   "Exibindo X de Y — Ver todas na galeria" (sem esconder mídia silenciosamente).
 - Câmera Samsung A07, galeria, preview, DataTransfer, storage e Demanda Rápida
   inalterados (JS e inputs byte-idênticos à base; quick.php idêntico).
