===== dashboard.php =====
--- /tmp/dash-mobile-snap/dashboard.php.t1base	2026-05-19 15:08:31.469812142 +0000
+++ componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.php	2026-05-19 19:56:19.060393849 +0000
@@ -1399,6 +1399,194 @@
     #aba-visao-geral .dash-paginacao span {
         font-size: 0.9rem !important;
     }
+
+    /* =========================================================
+       MOBILE / RESPONSIVO — camada isolada (não altera desktop)
+       Tiers: ≤768px (layout mobile) · ≤480px (ajuste fino)
+       ========================================================= */
+
+    /* Desktop: wrapper invisível (não cria caixa) + controles mobile ocultos.
+       .dash-tipo-sep e .dash-daterange-wrap continuam flex-items diretos
+       da .dash-tabs-row, exatamente como antes. */
+    .dash-filtros-mobile { display: contents; }
+    .dash-btn-filtros { display: none; }
+    .dash-drawer-backdrop { display: none; }
+
+    @media (max-width: 768px) {
+        .dash-btn-filtros {
+            display: inline-flex;
+            align-items: center;
+            gap: 0.4rem;
+            height: 2.4rem;
+            padding: 0 0.85rem;
+            background: rgba(255, 255, 255, 0.04);
+            border: 1px solid rgba(255, 255, 255, 0.10);
+            color: rgba(255, 255, 255, 0.85);
+            border-radius: 0.5rem;
+            font-size: 0.82rem;
+            font-weight: 600;
+            cursor: pointer;
+        }
+        .dash-btn-filtros i { font-size: 0.95rem; }
+
+        /* wrapper deixa de ser display:contents e vira painel off-canvas */
+        .dash-filtros-mobile {
+            display: flex !important;
+            flex-direction: column;
+            gap: 1rem;
+            position: fixed;
+            top: 0;
+            right: 0;
+            height: 100dvh;
+            width: min(86vw, 360px);
+            padding: 1.25rem 1rem calc(1.25rem + env(safe-area-inset-bottom));
+            background: linear-gradient(160deg, #11132e 0%, #0d1030 100%);
+            border-left: 1px solid rgba(255, 255, 255, 0.10);
+            box-shadow: -12px 0 40px rgba(0, 0, 0, 0.5);
+            transform: translateX(100%);
+            transition: transform 0.25s ease;
+            overflow-y: auto;
+            z-index: 1050;
+        }
+        .dash-filtros-mobile.aberto { transform: translateX(0); }
+
+        /* controles empilham e ficam tocáveis dentro do drawer */
+        .dash-filtros-mobile .dash-tipo-sep,
+        .dash-filtros-mobile .dash-daterange-wrap {
+            width: 100%;
+            margin-left: 0;
+            flex-wrap: wrap;
+        }
+        .dash-filtros-mobile .dash-tipo-sep { display: flex; }
+        .dash-filtros-mobile .dash-tipo-sep button { flex: 1 1 auto; justify-content: center; }
+        .dash-filtros-mobile .dash-day-buttons { flex-wrap: wrap; gap: 0.35rem; }
+        .dash-filtros-mobile .dash-day-btn { width: 2.6rem; height: 2.6rem; }
+
+        .dash-drawer-backdrop {
+            display: none;
+            position: fixed;
+            inset: 0;
+            background: rgba(0, 0, 0, 0.55);
+            z-index: 1040;
+        }
+        .dash-drawer-backdrop.aberto { display: block; }
+        body.dash-drawer-open { overflow: hidden; }
+
+        .dash-tabs-row { flex-wrap: nowrap; align-items: center; gap: 0.5rem; margin: 0.9rem 0.5rem 1rem; }
+        /* Quase edge-to-edge: aproveita a largura da tela no celular sem cortar
+           borda/sombra/cantos arredondados dos cards (0 colaria no recorte). */
+        .dash-aba-section { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
+        .dash-tabs {
+            flex-wrap: nowrap;
+            overflow-x: auto;
+            -webkit-overflow-scrolling: touch;
+            scroll-snap-type: x proximity;
+            scrollbar-width: none;
+            min-width: 0;
+        }
+        .dash-tabs::-webkit-scrollbar { display: none; }
+        .dash-tabs .dash-tab { flex: 0 0 auto; scroll-snap-align: start; }
+
+        .dash-kpi-grade { grid-template-columns: repeat(2, 1fr) !important; gap: 0.6rem; }
+        /* KPI órfã: o último card VISÍVEL ocupa as 2 colunas quando o nº de
+           visíveis é ímpar. Conta só visíveis excluindo [data-kpi="lead-time"]
+           — ele é display:none global (dashboard.php) e como phantom child
+           quebrava a paridade do :nth-child puro. Regra geral p/ qualquer aba. */
+        .dash-kpi-grade > .g-card-kpi:nth-last-child(1 of .g-card-kpi:not([data-kpi="lead-time"])):nth-child(odd of .g-card-kpi:not([data-kpi="lead-time"])) { grid-column: 1 / -1 !important; }
+        #chart-fluxo { min-height: 220px !important; }
+        #chart-fluxo .dash-skeleton { height: 220px !important; }
+
+        /* ---- Task 6: conteúdo pesado (selectors verificados no DOM real) ---- */
+        /* grids multi-coluna com classe colapsam no mobile.
+           minmax(0,1fr) (não 1fr): permite o track encolher abaixo do
+           min-content — senão um filho de largura fixa (ex.: donut
+           flex:0 0 230px) infla a coluna e estoura a margem da aba. */
+        .dash-grid-sep-top, .dash-grid-carg-top, .dash-info-grid { grid-template-columns: minmax(0, 1fr) !important; }
+
+        /* Grids inline-styled das abas (12/6/4/2 col, ex.: aba Ocorrências)
+           colapsam para 1 coluna; filhos com grid-column:span N viram full-width.
+           Escopado a .dash-aba-section e ao atributo style → NÃO atinge
+           .dash-kpi-grade (grid vem de CSS, não inline) nem o desktop. */
+        .dash-aba-section [style*="display:grid"],
+        .dash-aba-section [style*="display: grid"] {
+            grid-template-columns: minmax(0, 1fr) !important;
+        }
+        /* min-width:0 (item) + minmax(0,1fr) (track) = item encolhe e nunca
+           ultrapassa a largura do card/aba mesmo com conteúdo fixo dentro. */
+        .dash-aba-section [style*="grid-column:"] {
+            grid-column: 1 / -1 !important;
+            min-width: 0 !important;
+        }
+
+        /* tabelas das ABAS: scroll horizontal preservando colunas.
+           NÃO afeta a tabela do modal de ocorrências (#dash-modal-corpo),
+           que tem thead sticky e scroll próprio. */
+        .dash-aba-section .dash-table {
+            display: block;
+            overflow-x: auto;
+            -webkit-overflow-scrolling: touch;
+            white-space: nowrap;
+            width: 100%;
+        }
+
+        /* timelines Gantt: card rola horizontalmente; chart recebe min-width.
+           Cards têm overflow:hidden (style embutido) → x vira auto; wrap interno
+           tem overflow:visible inline (não clipa). */
+        #card-geral-timeline, #card-sep-timeline, #card-carg-timeline {
+            overflow-x: auto !important;
+            padding-left: 0.9rem !important;
+            padding-right: 0.9rem !important;
+        }
+        #geral-timeline-chart, #sep-timeline-chart, #carg-timeline-chart {
+            min-width: 680px;
+        }
+        /* Timeline de Carregamento: modo "cabe na tela" (B) — sem scroll
+           lateral; o eixo de tempo é encaixado na janela real de atividade
+           via JS (dashboard.js, gated por matchMedia). Sobrescreve a regra
+           de scroll acima só para o carg (geral/sep continuam roláveis). */
+        #card-carg-timeline { overflow-x: hidden !important; }
+        #carg-timeline-chart { min-width: 0 !important; }
+
+        /* tooltips não estouram a viewport */
+        .apexcharts-tooltip,
+        .dash-tooltip-host .dash-tooltip-bolha {
+            max-width: calc(100vw - 1rem) !important;
+            white-space: normal !important;
+        }
+
+        /* modais em tela cheia — sistema real é .dash-modal-overlay/.dash-modal-card
+           e #dash-modal-ocorrencias (NÃO Bootstrap .modal-*). !important vence
+           os estilos inline (width:min(1000px,100%); max-height:88vh; padding). */
+        .dash-modal-overlay { padding: 0 !important; }
+        #dash-modal-ocorrencias, #dash-modal-calculos { padding: 0 !important; }
+        .dash-modal-card {
+            width: 100% !important;
+            max-width: 100% !important;
+            min-width: 0 !important;
+            max-height: 100dvh !important;
+            border-radius: 0 !important;
+            overflow-y: auto !important;
+        }
+        #dash-modal-ocorrencias > .dash-modal-card,
+        #dash-modal-calculos > .dash-modal-card { height: 100dvh !important; }
+        /* (ocorrências/cálculos mantêm o scroll interno do corpo de propósito) */
+    }
+
+    @media (max-width: 480px) {
+        .dash-kpi-grade { gap: 0.5rem !important; }
+        .dash-kpi-grade > .g-card-kpi {
+            padding: 0.6rem !important;
+            min-height: 96px !important;
+            gap: 0.35rem !important;
+        }
+        /* Donut (Detalhamento/Tipos de ocorrência): em telas bem estreitas o
+           flex:0 0 230px inline aperta o bloco de totais ao lado. Encolhe o
+           donut p/ caber donut + legenda no card (vence o inline via !important). */
+        .chart-erros-sep-pie, .chart-erros-carg-pie {
+            flex: 0 0 132px !important;
+            height: 110px !important;
+        }
+    }
 </style>
 
 <!-- Boot data — lido pelo dashboard.js -->
@@ -1435,6 +1623,71 @@
     })();
 </script>
 
+<script>
+    /* Drawer de filtros (mobile) — isolado, presentational. */
+    (function () {
+      function init() {
+        var btn = document.getElementById('dash-btn-filtros');
+        var panel = document.getElementById('dash-filtros-mobile');
+        var backdrop = document.getElementById('dash-drawer-backdrop');
+        if (!btn || !panel || !backdrop) return;
+
+        var mq = window.matchMedia('(max-width: 768px)');
+
+        function syncAria() {
+            // No mobile, drawer fechado fica fora da árvore de acessibilidade.
+            // No desktop, o wrapper é display:contents e seu conteúdo é visível.
+            if (mq.matches && !panel.classList.contains('aberto')) {
+                panel.setAttribute('aria-hidden', 'true');
+            } else {
+                panel.removeAttribute('aria-hidden');
+            }
+        }
+
+        syncAria();
+        mq.addEventListener ? mq.addEventListener('change', syncAria) : mq.addListener(syncAria);
+
+        function abrir() {
+            panel.classList.add('aberto');
+            backdrop.classList.add('aberto');
+            document.body.classList.add('dash-drawer-open');
+            btn.setAttribute('aria-expanded', 'true');
+            panel.removeAttribute('aria-hidden');
+            panel.setAttribute('tabindex', '-1');
+            panel.focus();
+        }
+
+        function fechar() {
+            panel.classList.remove('aberto');
+            backdrop.classList.remove('aberto');
+            document.body.classList.remove('dash-drawer-open');
+            btn.setAttribute('aria-expanded', 'false');
+            syncAria();
+            btn.focus();
+            window.dispatchEvent(new Event('resize'));
+        }
+
+        // Sem focus-trap: drawer de filtros é não-destrutivo e dispensável (backdrop/Esc).
+        btn.addEventListener('click', function () {
+            if (panel.classList.contains('aberto')) fechar();
+            else abrir();
+        });
+        backdrop.addEventListener('click', fechar);
+        document.addEventListener('keydown', function (e) {
+            if (e.key === 'Escape' && panel.classList.contains('aberto')) fechar();
+        });
+      }
+      // O <script> está acima do markup do drawer (#dash-op-root vem depois);
+      // sem isto, getElementById roda no parse e retorna null → listeners
+      // nunca anexados → botão "Filtros" morto. Adia até o DOM existir.
+      if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', init);
+      } else {
+        init();
+      }
+    })();
+</script>
+
 <!-- Fundo navy gradiente (sem camadas GPU pesadas) -->
 <div class="dash-stars-bg" aria-hidden="true"></div>
 
@@ -1468,6 +1721,12 @@
                 <i class="bi-trophy"></i><?= show('Ranking') ?>
             </button>
         </div>
+        <button type="button" class="dash-btn-filtros" id="dash-btn-filtros"
+            aria-expanded="false" aria-controls="dash-filtros-mobile"
+            title="<?= show('Filtros') ?>" aria-label="<?= show('Filtros') ?>">
+            <i class="bi-sliders"></i><span><?= show('Filtros') ?></span>
+        </button>
+        <div class="dash-filtros-mobile" id="dash-filtros-mobile">
         <div class="dash-tipo-sep" id="dash-tipo-sep" role="group" aria-label="<?= show('Filtro de tipo de separação') ?>">
             <button type="button" data-tipo-sep="entrega" title="<?= show('Separações com entrega') ?>">
                 <i class="bi-truck"></i><?= show('Entrega') ?>
@@ -1496,7 +1755,9 @@
                 <i class="bi-info-circle"></i>
             </button>
         </div>
+        </div><!-- /.dash-filtros-mobile -->
     </div>
+    <div class="dash-drawer-backdrop" id="dash-drawer-backdrop"></div>
 
     <?php
     // Textos de ajuda dos KPIs do topo + cards customizados. Usados nas 3 abas

===== dashboard.js =====
--- /tmp/dash-mobile-snap/dashboard.js.t1base	2026-05-19 15:08:31.471744181 +0000
+++ componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.js	2026-05-19 19:55:49.353367466 +0000
@@ -20,6 +20,22 @@
 'use strict';
 
 // ===========================================================================
+// ApexCharts — defaults globais de responsividade (mesclados em todo gráfico).
+// Não edita gráficos individuais; opções locais sempre vencem.
+// ===========================================================================
+window.Apex = window.Apex || {};
+window.Apex.responsive = (window.Apex.responsive || []).concat([{
+    breakpoint: 768,
+    options: {
+        chart: { toolbar: { show: false } },
+        dataLabels: { enabled: false },
+        legend: { position: 'bottom', fontSize: '11px', itemMargin: { horizontal: 6, vertical: 2 } },
+        xaxis: { tickAmount: 4, labels: { rotate: -45, style: { fontSize: '10px' } } },
+        yaxis: { labels: { style: { fontSize: '10px' } } }
+    }
+}]);
+
+// ===========================================================================
 // Constantes semafóricas (spec §9)
 // ===========================================================================
 const DASH_SEMAFORO = {
@@ -1225,8 +1241,14 @@
 
         // Tabs visuais
         document.querySelectorAll('.dash-tab').forEach(btn => {
-            btn.classList.toggle('ativo', btn.dataset.tab === novaAba);
+            const ativo = btn.dataset.tab === novaAba;
+            btn.classList.toggle('ativo', ativo);
+            if (ativo && typeof btn.scrollIntoView === 'function') {
+                btn.scrollIntoView({ inline: 'center', block: 'nearest' });
+            }
         });
+        // Charts em aba antes oculta renderizam com width 0 — força recálculo.
+        window.dispatchEvent(new Event('resize'));
 
         // Seções
         document.querySelectorAll('.dash-aba-section').forEach(sec => {
@@ -4534,6 +4556,26 @@
                 theme: { mode: 'dark' },
             };
 
+            // Mobile (B): encaixa o Gantt na largura da tela — fixa o eixo de
+            // tempo na janela real de atividade (1º início → último fim) em vez
+            // do dia inteiro, evitando scroll horizontal. Hora exata no toque.
+            if (window.matchMedia && window.matchMedia('(max-width: 768px)').matches) {
+                const _pts = (series[0] && series[0].data) || [];
+                let _ini = Infinity, _fim = -Infinity;
+                _pts.forEach(p => {
+                    const a = p.y && p.y[0], b = p.y && p.y[1];
+                    if (typeof a === 'number' && !isNaN(a) && a < _ini) _ini = a;
+                    if (typeof b === 'number' && !isNaN(b) && b > _fim) _fim = b;
+                });
+                if (isFinite(_ini) && isFinite(_fim) && _fim > _ini) {
+                    const _pad = Math.max((_fim - _ini) * 0.03, 60000); // ≥ 1 min
+                    opts.xaxis = Object.assign({}, opts.xaxis, {
+                        min: _ini - _pad,
+                        max: _fim + _pad,
+                    });
+                }
+            }
+
             if (this._chartCargTimeline) {
                 try { this._chartCargTimeline.destroy(); } catch (_) {}
                 this._chartCargTimeline = null;
@@ -5838,6 +5880,91 @@
         btn.addEventListener('click', () => DashboardAbas.trocarAba(btn.dataset.tab));
     });
 
+    // Aba ativa (renderizada server-side) entra na viewport da faixa rolável
+    // no load — no mobile a barra de abas rola horizontalmente e a ativa pode
+    // ficar fora de vista (trocarAba só roda no clique, não no load inicial).
+    (function () {
+        var tabAtiva = document.querySelector('.dash-tab.ativo');
+        if (tabAtiva && typeof tabAtiva.scrollIntoView === 'function') {
+            requestAnimationFrame(function () {
+                tabAtiva.scrollIntoView({ inline: 'center', block: 'nearest' });
+            });
+        }
+    })();
+
+    // Mudança de orientação: ApexCharts recalcula largura.
+    window.addEventListener('orientationchange', function () {
+        setTimeout(function () { window.dispatchEvent(new Event('resize')); }, 200);
+    });
+
+    // ------------------------------------------------------------------
+    // Largura full-screen no mobile (tema = genezes).
+    // Ground truth do device: body/html = 375px, MAS o viewport de layout
+    // (onde o cabeçalho fixo se estica em 100vw, = onde fica a data no canto
+    // direito) = window.innerWidth = 512px. O usuário confirmou: forçar a
+    // 512 (innerWidth) cobre a tela; 100%/375 fica estreito.
+    // → Alvo = window.innerWidth (largura do cabeçalho/visível real).
+    // Forçamos #dash-op-root + ancestrais via inline !important. O tema
+    // reescreve a largura após o bootstrap → em vez de reapplies cegos por
+    // tempo, observamos style/class dos alvos (MutationObserver) e o tamanho
+    // (ResizeObserver) e reaplicamos sempre que o tema mexer — converge sem
+    // janela de tempo. Guarda `aplicando` (liberada no rAF) evita loop com as
+    // próprias escritas. Só no mobile, reversível no desktop. Escopo: a página.
+    (function () {
+        var root = document.getElementById('dash-op-root');
+        if (!root) return;
+        var mq = window.matchMedia('(max-width: 768px)');
+        var alvos = [];
+        for (var el = root; el && el !== document.body; el = el.parentElement) {
+            alvos.push(el);
+        }
+        var PROPS = ['width', 'max-width', 'min-width', 'margin-left', 'margin-right', 'flex', 'box-sizing'];
+        var aplicando = false;
+        function aplicar() {
+            if (aplicando) return;
+            aplicando = true;
+            try {
+                if (mq.matches) {
+                    var w = Math.max(window.innerWidth || 0, document.documentElement.clientWidth || 0);
+                    var wpx = w + 'px';
+                    alvos.forEach(function (el) {
+                        el.style.setProperty('width', wpx, 'important');
+                        el.style.setProperty('max-width', 'none', 'important');
+                        el.style.setProperty('min-width', '0', 'important');
+                        el.style.setProperty('margin-left', '0', 'important');
+                        el.style.setProperty('margin-right', '0', 'important');
+                        el.style.setProperty('flex', '1 1 ' + wpx, 'important');
+                        el.style.setProperty('box-sizing', 'border-box', 'important');
+                    });
+                } else {
+                    alvos.forEach(function (el) {
+                        PROPS.forEach(function (p) { el.style.removeProperty(p); });
+                    });
+                }
+            } finally {
+                // libera no próximo frame → ignora as mutações que nós mesmos
+                // geramos (MutationObserver dispara em microtask, antes do rAF).
+                (window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(function () { aplicando = false; });
+            }
+        }
+        aplicar();
+        (mq.addEventListener ? mq.addEventListener('change', aplicar) : mq.addListener(aplicar));
+        window.addEventListener('orientationchange', function () { setTimeout(aplicar, 250); });
+        // Reaplica sempre que o tema reescrever style/class ou o tamanho mudar.
+        if (window.MutationObserver) {
+            var mo = new MutationObserver(function () { if (!aplicando) aplicar(); });
+            alvos.forEach(function (el) {
+                mo.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
+            });
+        }
+        if (window.ResizeObserver && alvos.length > 1) {
+            var ro = new ResizeObserver(function () { if (!aplicando) aplicar(); });
+            ro.observe(alvos[1]); // #genezes_app_root
+        }
+        // Backstop leve (caso o tema mexa fora de attr/size observáveis).
+        [400, 1500].forEach(function (t) { setTimeout(aplicar, t); });
+    })();
+
     // Filtro Retira/Entrega/Todos — destaca o ativo e religa ao trocar
     function _aplicarHighlightTipoSep() {
         document.querySelectorAll('#dash-tipo-sep button[data-tipo-sep]').forEach(b => {
