/**
 * Wally Monitor - IF-THEN Exclusive Rules
 * 57 conditional accessibility rules based on NBR 17.225 + WCAG 2.2.
 * Each rule follows: IF [condition detected] THEN [requirement must be met].
 * These are EXCLUSIVE checks not covered by axe-core, WAVE, or Lighthouse.
 *
 * @module WallyMonitor.IFThenRules
 * @since 1.2.0
 */
(function($) {
    'use strict';

    window.WallyMonitor = window.WallyMonitor || {};
    WallyMonitor.IFThenRules = WallyMonitor.IFThenRules || {};

    // ─── Helpers (delegated to WallyMonitor.RuleHelpers) ────────────────
    var H            = WallyMonitor.RuleHelpers;
    var getSelector  = H.getSelector;
    var outerSnippet = H.outerSnippet;
    var getComputed  = H.getComputed;
    var makeNode     = H.makeNode;
    var violation    = H.violation;
    var isHidden     = H.isHidden;
    var hasAriaLabel = H.hasAriaLabel;

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP A – ARIA & Roles (rules 1–10)
    // ═══════════════════════════════════════════════════════════════════

    /** 1. IF role="button" THEN must be keyboard-operable */
    WallyMonitor.IFThenRules.checkRoleButtonKeyboard = function(doc) {
        var nodes = [];
        var els = doc.querySelectorAll('[role="button"]:not(button):not(input)');
        for (var i = 0; i < els.length; i++) {
            var el = els[i];
            if (isHidden(el)) { continue; }
            var ti = el.getAttribute('tabindex');
            var hasKey = el.hasAttribute('onkeydown') || el.hasAttribute('onkeyup') || el.hasAttribute('onkeypress');
            if (ti === null && !hasKey) {
                nodes.push(makeNode(el, 'serious', 'SE role="button" ENTÃO o elemento deve ser focalizável (tabindex) e responder a Enter/Espaço. Este elemento não-botão possui role="button" mas não tem tabindex ou manipulador de teclado (NBR 5.10.2).'));
            }
        }
        return violation('ift-role-button-keyboard', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Elementos com role="button" devem ser operáveis por teclado (focalizáveis + Enter/Espaço).', 'Adicione tabindex="0" e manipulador keydown para Enter/Espaço em elementos de botão personalizados.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 2. IF role="tablist" THEN must contain role="tab" */
    WallyMonitor.IFThenRules.checkTablistHasTabs = function(doc) {
        var nodes = [];
        var tablists = doc.querySelectorAll('[role="tablist"]');
        for (var i = 0; i < tablists.length; i++) {
            var el = tablists[i];
            if (isHidden(el)) { continue; }
            var tabs = el.querySelectorAll('[role="tab"]');
            if (tabs.length === 0) {
                nodes.push(makeNode(el, 'serious', 'SE role="tablist" ENTÃO deve conter elementos com role="tab". Esta tablist não possui elementos tab filhos (NBR 5.10.2).'));
            }
        }
        return violation('ift-tablist-has-tabs', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Uma tablist deve conter elementos tab para o padrão ARIA correto.', 'Adicione role="tab" a cada elemento de aba dentro da tablist.', 'https://www.w3.org/WAI/ARIA/apg/patterns/tabs/', nodes);
    };

    /** 3. IF role="tab" THEN must have aria-controls → tabpanel */
    WallyMonitor.IFThenRules.checkTabHasControls = function(doc) {
        var nodes = [];
        var tabs = doc.querySelectorAll('[role="tab"]');
        for (var i = 0; i < tabs.length; i++) {
            var el = tabs[i];
            if (isHidden(el)) { continue; }
            var controls = el.getAttribute('aria-controls');
            if (!controls) {
                nodes.push(makeNode(el, 'serious', 'SE role="tab" ENTÃO deve ter aria-controls apontando para um tabpanel. Esta aba não possui aria-controls (NBR 5.10.2).'));
            } else {
                var panel = doc.getElementById(controls);
                if (panel && panel.getAttribute('role') !== 'tabpanel') {
                    nodes.push(makeNode(el, 'moderate', 'SE role="tab" possui aria-controls ENTÃO o alvo deve ser role="tabpanel". O alvo "' + controls + '" possui role="' + (panel.getAttribute('role') || 'none') + '" (NBR 5.10.2).'));
                }
            }
        }
        return violation('ift-tab-controls-panel', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Abas devem ter aria-controls apontando para o tabpanel associado.', 'Adicione aria-controls="panel-id" a cada aba e role="tabpanel" a cada painel.', 'https://www.w3.org/WAI/ARIA/apg/patterns/tabs/', nodes);
    };

    /** 4. IF role="dialog" THEN must have accessible name */
    WallyMonitor.IFThenRules.checkDialogName = function(doc) {
        var nodes = [];
        var dialogs = doc.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog');
        for (var i = 0; i < dialogs.length; i++) {
            var el = dialogs[i];
            if (isHidden(el)) { continue; }
            if (!hasAriaLabel(el)) {
                nodes.push(makeNode(el, 'serious', 'SE role="dialog" ENTÃO deve ter aria-label ou aria-labelledby. Leitores de tela não conseguem identificar este diálogo (NBR 5.10.2).'));
            }
        }
        return violation('ift-dialog-has-name', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Diálogos devem ter um nome acessível via aria-label ou aria-labelledby.', 'Adicione aria-labelledby apontando para o elemento de título do diálogo.', 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/', nodes);
    };

    /** 5. IF aria-expanded THEN must have matching controlled element */
    WallyMonitor.IFThenRules.checkExpandedHasTarget = function(doc) {
        var nodes = [];
        var els = doc.querySelectorAll('[aria-expanded]');
        for (var i = 0; i < els.length; i++) {
            var el = els[i];
            if (isHidden(el)) { continue; }
            var controls = el.getAttribute('aria-controls');
            if (!controls) {
                nodes.push(makeNode(el, 'moderate', 'SE aria-expanded é utilizado ENTÃO o elemento deve ter aria-controls vinculando à região expansível. aria-controls ausente (NBR 5.10.2).'));
            } else if (!doc.getElementById(controls)) {
                nodes.push(makeNode(el, 'moderate', 'SE aria-controls="' + controls + '" ENTÃO o elemento alvo deve existir na página. Elemento não encontrado (NBR 5.10.2).'));
            }
        }
        return violation('ift-expanded-has-target', 'moderate', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Elementos com aria-expanded devem ter aria-controls apontando para um alvo existente.', 'Adicione aria-controls="target-id" e garanta que o elemento alvo exista.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 6. IF role="slider" THEN must have valuemin/max/now */
    WallyMonitor.IFThenRules.checkSliderValues = function(doc) {
        var nodes = [];
        var sliders = doc.querySelectorAll('[role="slider"]');
        for (var i = 0; i < sliders.length; i++) {
            var el = sliders[i];
            if (isHidden(el)) { continue; }
            var missing = [];
            if (!el.hasAttribute('aria-valuemin')) { missing.push('aria-valuemin'); }
            if (!el.hasAttribute('aria-valuemax')) { missing.push('aria-valuemax'); }
            if (!el.hasAttribute('aria-valuenow')) { missing.push('aria-valuenow'); }
            if (missing.length > 0) {
                nodes.push(makeNode(el, 'critical', 'SE role="slider" ENTÃO deve ter ' + missing.join(', ') + '. Tecnologias assistivas não conseguem transmitir o estado do slider (NBR 5.10.2).'));
            }
        }
        return violation('ift-slider-values', 'critical', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Sliders devem expor seus valores mínimo, máximo e atual via ARIA.', 'Adicione aria-valuemin, aria-valuemax e aria-valuenow aos elementos slider.', 'https://www.w3.org/WAI/ARIA/apg/patterns/slider/', nodes);
    };

    /** 7. IF role="progressbar" THEN must have value or text */
    WallyMonitor.IFThenRules.checkProgressbarValue = function(doc) {
        var nodes = [];
        var bars = doc.querySelectorAll('[role="progressbar"]');
        for (var i = 0; i < bars.length; i++) {
            var el = bars[i];
            if (isHidden(el)) { continue; }
            if (!el.hasAttribute('aria-valuenow') && !el.hasAttribute('aria-valuetext')) {
                nodes.push(makeNode(el, 'serious', 'SE role="progressbar" ENTÃO deve ter aria-valuenow ou aria-valuetext. Leitores de tela não conseguem anunciar o progresso (NBR 5.10.2).'));
            }
        }
        return violation('ift-progressbar-value', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Barras de progresso devem comunicar seu valor atual para tecnologias assistivas.', 'Adicione aria-valuenow="50" (ou aria-valuetext para valores não numéricos) às barras de progresso.', 'https://www.w3.org/WAI/ARIA/apg/patterns/meter/', nodes);
    };

    /** 8. IF role="alertdialog" THEN must have aria-describedby */
    WallyMonitor.IFThenRules.checkAlertDialogDesc = function(doc) {
        var nodes = [];
        var als = doc.querySelectorAll('[role="alertdialog"]');
        for (var i = 0; i < als.length; i++) {
            var el = als[i];
            if (isHidden(el)) { continue; }
            if (!el.getAttribute('aria-describedby')) {
                nodes.push(makeNode(el, 'serious', 'SE role="alertdialog" ENTÃO deve ter aria-describedby descrevendo o alerta. Usuários precisam compreender a mensagem do alerta (NBR 5.10.2).'));
            }
        }
        return violation('ift-alertdialog-desc', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Diálogos de alerta devem descrever o conteúdo da mensagem via aria-describedby.', 'Adicione aria-describedby apontando para o elemento que contém o texto da mensagem de alerta.', 'https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/', nodes);
    };

    /** 9. IF role="menu" THEN children must be menuitem/menuitemcheckbox/menuitemradio */
    WallyMonitor.IFThenRules.checkMenuChildren = function(doc) {
        var nodes = [];
        var menus = doc.querySelectorAll('[role="menu"], [role="menubar"]');
        for (var i = 0; i < menus.length; i++) {
            var el = menus[i];
            if (isHidden(el)) { continue; }
            var items = el.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]');
            if (items.length === 0) {
                nodes.push(makeNode(el, 'serious', 'SE role="menu" ENTÃO deve conter filhos com role="menuitem". Este menu não possui itens de menu adequados (NBR 5.10.2).'));
            }
        }
        return violation('ift-menu-has-items', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Menus ARIA devem conter filhos menuitem, menuitemcheckbox ou menuitemradio.', 'Adicione role="menuitem" a cada item interativo dentro do menu.', 'https://www.w3.org/WAI/ARIA/apg/patterns/menubar/', nodes);
    };

    /** 10. IF role="tree" THEN must have treeitem children */
    WallyMonitor.IFThenRules.checkTreeChildren = function(doc) {
        var nodes = [];
        var trees = doc.querySelectorAll('[role="tree"]');
        for (var i = 0; i < trees.length; i++) {
            var el = trees[i];
            if (isHidden(el)) { continue; }
            var items = el.querySelectorAll('[role="treeitem"]');
            if (items.length === 0) {
                nodes.push(makeNode(el, 'serious', 'SE role="tree" ENTÃO deve conter filhos com role="treeitem". Este widget de árvore não possui itens de árvore (NBR 5.10.2).'));
            }
        }
        return violation('ift-tree-has-items', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Widgets de árvore ARIA devem conter filhos treeitem.', 'Adicione role="treeitem" a cada nó dentro da árvore.', 'https://www.w3.org/WAI/ARIA/apg/patterns/treeview/', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP B – Forms & Inputs (rules 11–20)
    // ═══════════════════════════════════════════════════════════════════

    /** 11. IF radio buttons THEN must be grouped in fieldset+legend */
    WallyMonitor.IFThenRules.checkRadioGrouping = function(doc) {
        var nodes = [];
        var radios = doc.querySelectorAll('input[type="radio"]');
        var names = {};
        for (var i = 0; i < radios.length; i++) {
            var name = radios[i].getAttribute('name') || '';
            if (name && !names[name]) { names[name] = radios[i]; }
        }
        for (var n in names) {
            var el = names[n];
            if (isHidden(el)) { continue; }
            var fieldset = el.closest('fieldset');
            var group = el.closest('[role="radiogroup"], [role="group"]');
            if (!fieldset && !group) {
                nodes.push(makeNode(el, 'serious', 'SE botões de rádio compartilham um name ENTÃO devem ser agrupados em <fieldset>+<legend> ou role="radiogroup". O grupo "' + n + '" não está agrupado (NBR 5.3.1).'));
            } else if (fieldset && !fieldset.querySelector('legend')) {
                nodes.push(makeNode(fieldset, 'moderate', 'SE grupo de rádio está em <fieldset> ENTÃO deve ter um <legend>. Este fieldset não possui <legend> (NBR 5.3.1).'));
            }
        }
        return violation('ift-radio-grouped', 'serious', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Grupos de botões de rádio devem estar dentro de fieldset+legend ou role="radiogroup" com rótulo.', 'Envolva grupos de rádio em <fieldset><legend>Rótulo do Grupo</legend>...</fieldset>.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 12. IF checkbox group (3+) THEN must have group label */
    WallyMonitor.IFThenRules.checkCheckboxGrouping = function(doc) {
        var nodes = [];
        var checks = doc.querySelectorAll('input[type="checkbox"]');
        var names = {};
        for (var i = 0; i < checks.length; i++) {
            var name = checks[i].getAttribute('name') || '';
            var baseName = name.replace(/\[\]$/, '');
            if (baseName) {
                names[baseName] = names[baseName] || [];
                names[baseName].push(checks[i]);
            }
        }
        for (var n in names) {
            if (names[n].length < 3) { continue; }
            var el = names[n][0];
            if (isHidden(el)) { continue; }
            var fieldset = el.closest('fieldset');
            var group = el.closest('[role="group"]');
            if (!fieldset && !group) {
                nodes.push(makeNode(el, 'moderate', 'SE 3+ checkboxes compartilham um name ENTÃO devem ser agrupados com um rótulo. O grupo "' + n + '" possui ' + names[n].length + ' checkboxes sem rótulo de grupo (NBR 5.3.1).'));
            }
        }
        return violation('ift-checkbox-grouped', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Grupos de checkboxes relacionados devem ter um rótulo de grupo via fieldset+legend ou role="group".', 'Envolva checkboxes relacionados em <fieldset><legend>Opções</legend>...</fieldset>.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 13. IF required attribute THEN should have aria-required for older AT */
    WallyMonitor.IFThenRules.checkRequiredAria = function(doc) {
        var nodes = [];
        // Note: HTML5 required is well-supported by all modern AT (NVDA, JAWS, VoiceOver).
        // aria-required is only needed for VERY old AT. This is informational only.
        var fields = doc.querySelectorAll('input[required]:not([aria-required]):not([type="hidden"]), select[required]:not([aria-required]), textarea[required]:not([aria-required])');
        for (var i = 0; i < fields.length; i++) {
            var el = fields[i];
            if (isHidden(el)) { continue; }
            nodes.push(makeNode(el, 'minor', 'SE o campo possui atributo required ENTÃO adicionar aria-required="true" melhora a compatibilidade com tecnologias assistivas mais antigas. O atributo HTML5 required sozinho é suficiente para leitores de tela modernos.'));
        }
        return violation('ift-required-aria', 'minor', ['best-practice'], 'Adicionar aria-required junto com required é uma boa prática para compatibilidade com tecnologias assistivas mais antigas.', 'Opcionalmente, adicione aria-required="true" junto ao atributo required.', 'https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html', nodes);
    };

    /** 14. IF input type="file" THEN must have accessible label */
    WallyMonitor.IFThenRules.checkFileInputLabel = function(doc) {
        var nodes = [];
        var files = doc.querySelectorAll('input[type="file"]');
        for (var i = 0; i < files.length; i++) {
            var el = files[i];
            if (isHidden(el)) { continue; }
            var id = el.getAttribute('id');
            var hasLabel = hasAriaLabel(el) || (id && doc.querySelector('label[for="' + id + '"]'));
            if (!hasLabel) {
                nodes.push(makeNode(el, 'serious', 'SE input type="file" ENTÃO deve ter um rótulo acessível. Leitores de tela não conseguem identificar a finalidade deste upload de arquivo (NBR 5.9.2).'));
            }
        }
        return violation('ift-file-input-label', 'serious', ['wcag2a', 'nbr17225-5.9.2', 'wcag332'], 'Campos de upload de arquivo devem ter um rótulo associado.', 'Adicione <label for="file-id">Enviar documento</label> ou aria-label.', 'https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html', nodes);
    };

    /** 15. IF input type="range" THEN must have visible value + label */
    WallyMonitor.IFThenRules.checkRangeAccessibility = function(doc) {
        var nodes = [];
        var ranges = doc.querySelectorAll('input[type="range"]');
        for (var i = 0; i < ranges.length; i++) {
            var el = ranges[i];
            if (isHidden(el)) { continue; }
            var id = el.getAttribute('id');
            var hasLabel = hasAriaLabel(el) || (id && doc.querySelector('label[for="' + id + '"]'));
            if (!hasLabel) {
                nodes.push(makeNode(el, 'serious', 'SE input type="range" ENTÃO deve ter um rótulo e valor visível. Leitores de tela não conseguem transmitir a finalidade do slider (NBR 5.9.2).'));
            }
            var output = id ? doc.querySelector('output[for="' + id + '"]') : null;
            if (!output && !el.getAttribute('aria-valuetext')) {
                nodes.push(makeNode(el, 'moderate', 'SE slider de intervalo existe ENTÃO seu valor atual deve ser visível. Nenhum elemento <output> ou aria-valuetext encontrado (NBR 5.10.2).'));
            }
        }
        return violation('ift-range-accessible', 'serious', ['wcag2a', 'nbr17225-5.9.2', 'wcag412'], 'Campos de intervalo (range) devem ter rótulos e comunicar seu valor.', 'Adicione <label> e <output for="range-id"> para exibir o valor atual.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 16. IF textarea THEN must have associated label */
    WallyMonitor.IFThenRules.checkTextareaLabel = function(doc) {
        var nodes = [];
        var tas = doc.querySelectorAll('textarea');
        for (var i = 0; i < tas.length; i++) {
            var el = tas[i];
            if (isHidden(el)) { continue; }
            var id = el.getAttribute('id');
            var hasLabel = hasAriaLabel(el) || (id && doc.querySelector('label[for="' + id + '"]'));
            if (!hasLabel && !el.getAttribute('placeholder')) {
                nodes.push(makeNode(el, 'serious', 'SE <textarea> ENTÃO deve ter um rótulo associado. Este textarea não possui label, aria-label ou placeholder (NBR 5.9.2).'));
            }
        }
        return violation('ift-textarea-label', 'serious', ['wcag2a', 'nbr17225-5.9.2', 'wcag332'], 'Textareas devem ter rótulos associados para usuários de leitores de tela.', 'Adicione <label for="textarea-id"> ou aria-label ao textarea.', 'https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html', nodes);
    };

    /** 17. IF select[multiple] THEN must have usage instructions */
    WallyMonitor.IFThenRules.checkSelectMultipleInstructions = function(doc) {
        var nodes = [];
        var selects = doc.querySelectorAll('select[multiple]');
        for (var i = 0; i < selects.length; i++) {
            var el = selects[i];
            if (isHidden(el)) { continue; }
            var desc = el.getAttribute('aria-describedby');
            var title = el.getAttribute('title');
            if (!desc && !title) {
                nodes.push(makeNode(el, 'moderate', 'SE <select multiple> ENTÃO deve ter instruções sobre seleção múltipla (Ctrl+clique). Nenhum aria-describedby ou title encontrado (NBR 5.9.2).'));
            }
        }
        return violation('ift-select-multiple-instructions', 'moderate', ['wcag2a', 'nbr17225-5.9.2', 'wcag332'], 'Campos de seleção múltipla devem fornecer instruções sobre como selecionar várias opções.', 'Adicione aria-describedby apontando para texto auxiliar: "Segure Ctrl para selecionar múltiplos".', 'https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html', nodes);
    };

    /** 18. IF input has pattern THEN must have title explaining format */
    WallyMonitor.IFThenRules.checkPatternTitle = function(doc) {
        var nodes = [];
        var inputs = doc.querySelectorAll('input[pattern]');
        for (var i = 0; i < inputs.length; i++) {
            var el = inputs[i];
            if (isHidden(el)) { continue; }
            var title = el.getAttribute('title');
            var desc = el.getAttribute('aria-describedby');
            if (!title && !desc) {
                var pattern = el.getAttribute('pattern') || '';
                nodes.push(makeNode(el, 'moderate', 'SE input possui pattern="' + pattern.substring(0, 30) + '" ENTÃO deve ter title ou aria-describedby explicando o formato esperado (NBR 5.9.3).'));
            }
        }
        return violation('ift-pattern-explained', 'moderate', ['wcag2aa', 'nbr17225-5.9.3', 'wcag333'], 'Campos com validação pattern devem descrever o formato esperado.', 'Adicione title="Formato: XXX-XXX" ou aria-describedby para explicar o padrão.', 'https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html', nodes);
    };

    /** 19. IF element is disabled THEN should not trap focus */
    WallyMonitor.IFThenRules.checkDisabledNotFocusable = function(doc) {
        var nodes = [];
        var disabled = doc.querySelectorAll('[disabled][tabindex]');
        for (var i = 0; i < disabled.length; i++) {
            var el = disabled[i];
            var ti = parseInt(el.getAttribute('tabindex'), 10);
            if (ti >= 0) {
                nodes.push(makeNode(el, 'moderate', 'SE o elemento está desabilitado ENTÃO não deveria estar na ordem de tabulação. Este elemento desabilitado possui tabindex="' + ti + '" tornando-o focalizável (NBR 5.5.2).'));
            }
        }
        return violation('ift-disabled-not-focusable', 'moderate', ['wcag2a', 'nbr17225-5.5.2', 'wcag212'], 'Elementos desabilitados não devem ser focalizáveis via tabindex.', 'Remova tabindex ou defina tabindex="-1" em elementos desabilitados.', 'https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html', nodes);
    };

    /** 20. IF form has action THEN submit must have clear label */
    WallyMonitor.IFThenRules.checkFormSubmitLabel = function(doc) {
        var nodes = [];
        var forms = doc.querySelectorAll('form[action]');
        for (var i = 0; i < forms.length; i++) {
            var form = forms[i];
            if (isHidden(form)) { continue; }
            var submits = form.querySelectorAll('button[type="submit"], input[type="submit"]');
            for (var j = 0; j < submits.length; j++) {
                var btn = submits[j];
                var text = (btn.textContent || btn.value || '').trim().toLowerCase();
                var label = btn.getAttribute('aria-label') || '';
                var combined = text + label;
                // Only flag truly generic labels — many short labels are valid with context
                var genericLabels = ['submit', 'enviar', 'go', 'ok', 'send', 'ir'];
                var isGeneric = false;
                for (var g = 0; g < genericLabels.length; g++) {
                    if (combined === genericLabels[g]) { isGeneric = true; break; }
                }
                if (!combined || isGeneric) {
                    // But skip if the form has a visible heading or legend providing context
                    var formLabel = form.querySelector('legend, h1, h2, h3, h4, h5, h6');
                    var formAriaLabel = form.getAttribute('aria-label') || form.getAttribute('aria-labelledby');
                    if (!formLabel && !formAriaLabel) {
                        nodes.push(makeNode(btn, 'moderate', 'SE formulário possui action ENTÃO o botão de envio deve ter um rótulo descritivo. O texto genérico "' + (text || 'empty') + '" não descreve a ação (NBR 5.9.2).'));
                    }
                }
            }
        }
        return violation('ift-form-submit-label', 'moderate', ['wcag2a', 'nbr17225-5.9.2', 'wcag332'], 'Botões de envio devem descrever claramente qual ação será executada.', 'Use rótulos descritivos como "Criar Conta" em vez de genéricos como "Enviar".', 'https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP C – Tables (rules 21–27)
    // ═══════════════════════════════════════════════════════════════════

    /** 21. IF table has th THEN th must have scope */
    WallyMonitor.IFThenRules.checkThHasScope = function(doc) {
        var nodes = [];
        var ths = doc.querySelectorAll('table:not([role="presentation"]) th');
        for (var i = 0; i < ths.length; i++) {
            var el = ths[i];
            if (isHidden(el)) { continue; }
            if (!el.getAttribute('scope') && !el.getAttribute('id')) {
                nodes.push(makeNode(el, 'moderate', 'SE <th> existe ENTÃO deve ter scope="col" ou scope="row". Sem scope, as associações entre células de dados são ambíguas (NBR 5.3.1).'));
            }
        }
        return violation('ift-th-has-scope', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Cabeçalhos de tabela devem ter scope para indicar se se aplicam a linhas ou colunas.', 'Adicione scope="col" aos cabeçalhos de coluna e scope="row" aos cabeçalhos de linha.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 22. IF table has colspan/rowspan THEN must use headers+id pattern */
    WallyMonitor.IFThenRules.checkComplexTableHeaders = function(doc) {
        var nodes = [];
        var tables = doc.querySelectorAll('table:not([role="presentation"])');
        for (var i = 0; i < tables.length; i++) {
            var table = tables[i];
            if (isHidden(table)) { continue; }
            var merged = table.querySelectorAll('[colspan], [rowspan]');
            if (merged.length === 0) { continue; }
            var tds = table.querySelectorAll('td');
            var hasHeadersAttr = false;
            for (var j = 0; j < tds.length; j++) {
                if (tds[j].getAttribute('headers')) { hasHeadersAttr = true; break; }
            }
            if (!hasHeadersAttr) {
                nodes.push(makeNode(table, 'serious', 'SE tabela possui células mescladas (colspan/rowspan) ENTÃO deve usar o padrão headers+id para associação célula-cabeçalho. Esta tabela complexa depende apenas da posição (NBR 5.3.1).'));
            }
        }
        return violation('ift-complex-table-headers', 'serious', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Tabelas complexas com células mescladas devem usar headers+id para associações inequívocas entre células.', 'Adicione id a cada <th> e headers="id1 id2" a cada <td>.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 23. IF data table THEN must have caption or aria-label */
    WallyMonitor.IFThenRules.checkTableCaption = function(doc) {
        var nodes = [];
        var tables = doc.querySelectorAll('table:not([role="presentation"]):not([role="none"])');
        for (var i = 0; i < tables.length; i++) {
            var table = tables[i];
            if (isHidden(table)) { continue; }
            var ths = table.querySelectorAll('th');
            if (ths.length === 0) { continue; } // only flag data tables
            var caption = table.querySelector('caption');
            if (!caption && !hasAriaLabel(table)) {
                nodes.push(makeNode(table, 'moderate', 'SE tabela de dados existe ENTÃO deve ter <caption> ou aria-label descrevendo sua finalidade. Esta tabela possui cabeçalhos mas não possui caption (NBR 5.3.1).'));
            }
        }
        return violation('ift-table-has-caption', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Tabelas de dados devem ter um caption ou aria-label descrevendo seu conteúdo.', 'Adicione <caption>Descrição da tabela</caption> como primeiro filho da tabela.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 24. IF table role="presentation" THEN must not have th/caption */
    WallyMonitor.IFThenRules.checkPresentationTableClean = function(doc) {
        var nodes = [];
        var tables = doc.querySelectorAll('table[role="presentation"], table[role="none"]');
        for (var i = 0; i < tables.length; i++) {
            var table = tables[i];
            if (isHidden(table)) { continue; }
            var ths = table.querySelectorAll('th');
            var caption = table.querySelector('caption');
            if (ths.length > 0 || caption) {
                nodes.push(makeNode(table, 'serious', 'SE tabela possui role="presentation" ENTÃO não deve ter <th> ou <caption>. Marcação semântica de tabela contradiz o role de apresentação (NBR 5.3.1).'));
            }
        }
        return violation('ift-presentation-table-clean', 'serious', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Tabelas de layout com role="presentation" não devem conter elementos semânticos de tabela.', 'Remova <th>, <caption>, scope e headers de tabelas de layout, ou remova role="presentation".', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 25. IF td has colspan THEN should reference headers */
    WallyMonitor.IFThenRules.checkColspanHeaders = function(doc) {
        var nodes = [];
        var cells = doc.querySelectorAll('table:not([role="presentation"]) td[colspan]');
        for (var i = 0; i < cells.length; i++) {
            var el = cells[i];
            if (isHidden(el)) { continue; }
            var span = parseInt(el.getAttribute('colspan'), 10) || 1;
            if (span > 1 && !el.getAttribute('headers')) {
                nodes.push(makeNode(el, 'moderate', 'SE <td> abrange ' + span + ' colunas ENTÃO deve ter atributo headers para manter a associação célula-cabeçalho (NBR 5.3.1).'));
            }
        }
        return violation('ift-colspan-has-headers', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Células que abrangem múltiplas colunas devem usar headers para manter as associações.', 'Adicione headers="header-id" às células que abrangem colunas.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 26. IF nested tables THEN flag complexity */
    WallyMonitor.IFThenRules.checkNestedTables = function(doc) {
        var nodes = [];
        var inner = doc.querySelectorAll('table table');
        for (var i = 0; i < inner.length; i++) {
            var el = inner[i];
            if (isHidden(el)) { continue; }
            nodes.push(makeNode(el, 'moderate', 'SE <table> está aninhada dentro de outra <table> ENTÃO cria complexidade excessiva para leitores de tela. Considere reestruturar (NBR 5.3.1).'));
        }
        return violation('ift-no-nested-tables', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Tabelas aninhadas são extremamente difíceis para leitores de tela navegarem.', 'Simplifique a estrutura de dados ou use layout CSS em vez de tabelas aninhadas.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 27. IF sortable table THEN must have aria-sort on active header */
    WallyMonitor.IFThenRules.checkSortableTable = function(doc) {
        var nodes = [];
        var sortable = doc.querySelectorAll('table th[data-sort], table th[data-sortable], table th.sortable, table.sortable th, .tablesorter th, .wp-list-table th');
        for (var i = 0; i < sortable.length; i++) {
            var el = sortable[i];
            if (isHidden(el)) { continue; }
            if (!el.getAttribute('aria-sort')) {
                nodes.push(makeNode(el, 'moderate', 'SE coluna da tabela é ordenável ENTÃO <th> deve ter aria-sort="ascending|descending|none" para anunciar o estado de ordenação (NBR 5.10.2).'));
            }
        }
        return violation('ift-sortable-aria-sort', 'moderate', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Cabeçalhos de tabela ordenáveis devem comunicar a direção da ordenação via aria-sort.', 'Adicione aria-sort="none" inicialmente e atualize para "ascending" ou "descending" ao ordenar.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP D – Links & Navigation (rules 28–35)
    // ═══════════════════════════════════════════════════════════════════

    /** 28. IF link opens new window THEN must warn user */
    WallyMonitor.IFThenRules.checkNewWindowWarning = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[target="_blank"]');
        for (var i = 0; i < links.length; i++) {
            var el = links[i];
            if (isHidden(el)) { continue; }
            var text = (el.textContent || '').toLowerCase() + (el.getAttribute('aria-label') || '').toLowerCase() + (el.getAttribute('title') || '').toLowerCase();
            var hasWarning = text.indexOf('nova janela') !== -1 || text.indexOf('new window') !== -1 || text.indexOf('new tab') !== -1 || text.indexOf('nova aba') !== -1 || text.indexOf('external') !== -1;
            var hasIcon = el.querySelector('.screen-reader-text, .sr-only, .visually-hidden, [aria-hidden="true"]');
            if (!hasWarning && !hasIcon) {
                nodes.push(makeNode(el, 'moderate', 'SE <a target="_blank"> ENTÃO deve avisar o usuário que o link abre em uma nova janela/aba. Nenhum texto de aviso encontrado (NBR 5.6.4).'));
            }
        }
        return violation('ift-new-window-warning', 'moderate', ['wcag2a', 'nbr17225-5.6.4', 'wcag244'], 'Links que abrem em novas janelas devem avisar os usuários antes da ativação.', 'Adicione o texto "(abre em nova aba)" ou um span de aviso visível apenas para leitores de tela.', 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html', nodes);
    };

    /** 29. IF a href="#" THEN flag empty destination */
    WallyMonitor.IFThenRules.checkHashLink = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[href="#"], a[href=""]');
        for (var i = 0; i < links.length; i++) {
            var el = links[i];
            // Use explicit attribute check instead of CSS-based isHidden()
            // getComputedStyle is unreliable in scan context (admin CSS conflicts
            // with injected theme CSS can falsely hide navigation links)
            var explicitlyHidden = false;
            var ancestor = el;
            while (ancestor && ancestor.nodeType === 1) {
                if (ancestor.hasAttribute('hidden') || ancestor.getAttribute('aria-hidden') === 'true') {
                    explicitlyHidden = true;
                    break;
                }
                var inlineStyle = ancestor.getAttribute('style') || '';
                if (/display\s*:\s*none/i.test(inlineStyle)) {
                    explicitlyHidden = true;
                    break;
                }
                if (ancestor.id === 'wally-scan-container') { break; }
                ancestor = ancestor.parentNode;
            }
            if (explicitlyHidden) { continue; }
            var role = el.getAttribute('role');
            if (role === 'button' || role === 'tab' || role === 'menuitem' || role === 'switch' || role === 'option' || role === 'treeitem') { continue; } // acceptable use
            // Skip SPA/framework interactive elements
            if (el.hasAttribute('data-toggle') || el.hasAttribute('data-bs-toggle') || el.hasAttribute('data-target') || el.hasAttribute('data-bs-target') || el.hasAttribute('aria-controls') || el.hasAttribute('aria-expanded') || el.hasAttribute('data-slide') || el.hasAttribute('data-bs-slide')) { continue; }
            // Skip anchors used for scroll-to-top or smooth scrolling
            if (el.hasAttribute('data-scroll') || (el.className || '').match(/\b(scroll-top|back-to-top|smooth-scroll)\b/)) { continue; }
            nodes.push(makeNode(el, 'moderate', 'SE <a href="#"> ENTÃO o link não tem destino real. Use um <button> para ações ou forneça um href válido (NBR 5.6.4).'));
        }
        return violation('ift-no-hash-links', 'moderate', ['wcag2a', 'nbr17225-5.6.4', 'wcag244'], 'Links com href="#" não possuem destino e confundem os usuários. Use botões para ações.', 'Altere para <button> para ações ou forneça um href válido para navegação.', 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html', nodes);
    };

    /** 30. IF a href="javascript:void(0)" THEN flag misuse */
    WallyMonitor.IFThenRules.checkJavascriptHref = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[href^="javascript:"]');
        for (var i = 0; i < links.length; i++) {
            var el = links[i];
            if (isHidden(el)) { continue; }
            nodes.push(makeNode(el, 'serious', 'SE <a href="javascript:..."> ENTÃO está usando incorretamente o link semântico para scripting. Use <button> para ações (NBR 5.10.2).'));
        }
        return violation('ift-no-javascript-href', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Links javascript: fazem uso indevido da semântica de links. Use botões para ações com script.', 'Substitua <a href="javascript:..."> por <button onclick="..."> ou manipuladores de eventos adequados.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 31. IF adjacent links have same href THEN flag redundancy */
    WallyMonitor.IFThenRules.checkAdjacentDuplicateLinks = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[href]');
        for (var i = 0; i < links.length - 1; i++) {
            var a = links[i], b = links[i + 1];
            if (isHidden(a) || isHidden(b)) { continue; }
            if (a.getAttribute('href') === b.getAttribute('href')) {
                // Check if they're truly adjacent (siblings or parent-sibling)
                if (a.parentNode === b.parentNode || a.parentNode.parentNode === b.parentNode || a.parentNode === b.parentNode.parentNode) {
                    nodes.push(makeNode(b, 'minor', 'SE links adjacentes possuem o mesmo href ENTÃO devem ser combinados em um único link. Navegação duplicada é confusa (NBR 5.6.4).'));
                }
            }
        }
        return violation('ift-no-adjacent-duplicate-links', 'minor', ['wcag2a', 'nbr17225-5.6.4', 'wcag244'], 'Links adjacentes para a mesma URL devem ser combinados em um único link.', 'Mescle o link da imagem e o link do texto em um único elemento <a>.', 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html', nodes);
    };

    /** 32. IF nav has links THEN active page should have aria-current */
    WallyMonitor.IFThenRules.checkAriaCurrentPage = function(doc) {
        var nodes = [];
        var navs = doc.querySelectorAll('nav');
        for (var i = 0; i < navs.length; i++) {
            var nav = navs[i];
            if (isHidden(nav)) { continue; }
            var links = nav.querySelectorAll('a[href]');
            if (links.length < 3) { continue; }
            var hasCurrent = nav.querySelector('[aria-current]');
            var hasActive = nav.querySelector('.active, .current, .current-menu-item, .current_page_item');
            if (hasActive && !hasCurrent) {
                nodes.push(makeNode(hasActive, 'moderate', 'SE nav possui um item ativo/atual ENTÃO deve usar aria-current="page". Classe CSS sozinha não é exposta para tecnologias assistivas (NBR 5.8.3).'));
            }
        }
        return violation('ift-aria-current-page', 'moderate', ['wcag2aa', 'nbr17225-5.8.3', 'wcag323'], 'Itens de navegação ativos devem usar aria-current="page" para leitores de tela.', 'Adicione aria-current="page" ao link que representa a página atual.', 'https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html', nodes);
    };

    /** 33. IF pagination exists THEN must have role="navigation" + aria-label */
    WallyMonitor.IFThenRules.checkPaginationSemantic = function(doc) {
        var nodes = [];
        var pags = doc.querySelectorAll('.pagination, .nav-links, .page-numbers, .pager, [class*="pagination"]');
        for (var i = 0; i < pags.length; i++) {
            var el = pags[i];
            if (isHidden(el)) { continue; }
            var nav = el.closest('nav');
            if (!nav) {
                nodes.push(makeNode(el, 'moderate', 'SE paginação existe ENTÃO deve estar dentro de <nav> com aria-label="Paginação". Não está envolvida em <nav> (NBR 5.3.1).'));
            } else if (!hasAriaLabel(nav)) {
                nodes.push(makeNode(nav, 'minor', 'SE paginação está em <nav> ENTÃO deve ter aria-label="Paginação" para distinguir da navegação principal (NBR 5.3.1).'));
            }
        }
        return violation('ift-pagination-semantic', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Paginação deve estar envolvida em <nav aria-label="Paginação"> para semântica adequada.', 'Envolva a paginação em um elemento <nav aria-label="Paginação">.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 34. IF link downloads file THEN must indicate file type */
    WallyMonitor.IFThenRules.checkDownloadLinkInfo = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[download], a[href$=".pdf"], a[href$=".doc"], a[href$=".docx"], a[href$=".xls"], a[href$=".xlsx"], a[href$=".zip"], a[href$=".csv"]');
        for (var i = 0; i < links.length; i++) {
            var el = links[i];
            if (isHidden(el)) { continue; }
            var text = (el.textContent || '').toLowerCase() + (el.getAttribute('aria-label') || '').toLowerCase();
            var href = (el.getAttribute('href') || '');
            var ext = href.split('.').pop().split('?')[0].toUpperCase();
            if (text.indexOf(ext.toLowerCase()) === -1 && text.indexOf('pdf') === -1 && text.indexOf('download') === -1) {
                nodes.push(makeNode(el, 'minor', 'SE link faz download de arquivo ENTÃO o texto do link deve indicar o tipo de arquivo (' + ext + ') e idealmente o tamanho. Usuários precisam saber o que estão baixando (NBR 5.6.4).'));
            }
        }
        return violation('ift-download-link-info', 'minor', ['wcag2a', 'nbr17225-5.6.4', 'wcag244'], 'Links de download devem indicar o tipo e tamanho do arquivo.', 'Adicione o tipo de arquivo ao texto do link: "Relatório Anual (PDF, 2,5 MB)".', 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html', nodes);
    };

    /** 35. IF anchor link (#section) THEN target must exist */
    WallyMonitor.IFThenRules.checkAnchorTarget = function(doc) {
        var nodes = [];
        var links = doc.querySelectorAll('a[href^="#"]');
        for (var i = 0; i < links.length; i++) {
            var el = links[i];
            if (isHidden(el)) { continue; }
            var target = (el.getAttribute('href') || '').substring(1);
            if (!target || target === 'top' || target === '!') { continue; }
            if (!doc.getElementById(target) && !doc.querySelector('[name="' + target + '"]')) {
                nodes.push(makeNode(el, 'moderate', 'SE <a href="#' + target + '"> ENTÃO o elemento alvo deve existir. Link âncora quebrado — nenhum elemento com id="' + target + '" encontrado (NBR 5.6.4).'));
            }
        }
        return violation('ift-anchor-target-exists', 'moderate', ['wcag2a', 'nbr17225-5.6.4', 'wcag244'], 'Links âncora devem apontar para elementos alvo existentes na página.', 'Garanta que o elemento alvo possua o atributo id correspondente.', 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP E – Media & Embeds (rules 36–42)
    // ═══════════════════════════════════════════════════════════════════

    /** 36. IF video THEN must have captions track */
    WallyMonitor.IFThenRules.checkVideoCaptions = function(doc) {
        var nodes = [];
        var videos = doc.querySelectorAll('video');
        for (var i = 0; i < videos.length; i++) {
            var el = videos[i];
            if (isHidden(el)) { continue; }
            var captions = el.querySelectorAll('track[kind="captions"], track[kind="subtitles"]');
            if (captions.length === 0) {
                nodes.push(makeNode(el, 'serious', 'SE <video> ENTÃO deve ter <track kind="captions"> para usuários surdos ou com deficiência auditiva. Nenhuma faixa de legendas encontrada (NBR 5.2.2).'));
            }
        }
        return violation('ift-video-has-captions', 'serious', ['wcag2a', 'nbr17225-5.2.2', 'wcag122'], 'Vídeos devem fornecer legendas sincronizadas para conteúdo de áudio.', 'Adicione <track kind="captions" src="legendas.vtt" srclang="pt" label="Português">.', 'https://www.w3.org/WAI/WCAG22/Understanding/captions-prerecorded.html', nodes);
    };

    /** 37. IF audio THEN must have transcript nearby */
    WallyMonitor.IFThenRules.checkAudioTranscript = function(doc) {
        var nodes = [];
        var audios = doc.querySelectorAll('audio');
        for (var i = 0; i < audios.length; i++) {
            var el = audios[i];
            if (isHidden(el)) { continue; }
            var parent = el.parentNode;
            var sibling = parent ? parent.querySelector('a[href*="transcript"], a[href*="transcri"], .transcript, [class*="transcript"]') : null;
            var desc = el.getAttribute('aria-describedby');
            if (!sibling && !desc) {
                nodes.push(makeNode(el, 'serious', 'SE <audio> ENTÃO deve ter um link para transcrição próximo. Nenhum link de transcrição ou aria-describedby encontrado (NBR 5.2.1).'));
            }
        }
        return violation('ift-audio-has-transcript', 'serious', ['wcag2a', 'nbr17225-5.2.1', 'wcag121'], 'Conteúdo de áudio deve ter uma transcrição em texto disponível.', 'Adicione um link "Ver Transcrição" próximo ao player de áudio.', 'https://www.w3.org/WAI/WCAG22/Understanding/audio-only-and-video-only-prerecorded.html', nodes);
    };

    /** 38. IF canvas THEN must have fallback text */
    WallyMonitor.IFThenRules.checkCanvasFallback = function(doc) {
        var nodes = [];
        var canvases = doc.querySelectorAll('canvas');
        for (var i = 0; i < canvases.length; i++) {
            var el = canvases[i];
            if (isHidden(el)) { continue; }
            var fallback = (el.textContent || '').trim();
            var label = hasAriaLabel(el);
            var role = el.getAttribute('role');
            if (!fallback && !label && role !== 'img') {
                nodes.push(makeNode(el, 'serious', 'SE <canvas> ENTÃO deve ter conteúdo de texto alternativo ou aria-label. Canvas é invisível para leitores de tela (NBR 5.1.1).'));
            }
        }
        return violation('ift-canvas-fallback', 'serious', ['wcag2a', 'nbr17225-5.1.1', 'wcag111'], 'Elementos canvas devem fornecer texto alternativo para tecnologias assistivas.', 'Adicione role="img" + aria-label, ou coloque texto alternativo dentro do elemento canvas.', 'https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html', nodes);
    };

    /** 39. IF interactive SVG THEN must have keyboard support */
    WallyMonitor.IFThenRules.checkInteractiveSvgKeyboard = function(doc) {
        var nodes = [];
        var svgs = doc.querySelectorAll('svg');
        for (var i = 0; i < svgs.length; i++) {
            var svg = svgs[i];
            if (isHidden(svg)) { continue; }
            var interactive = svg.querySelectorAll('[onclick], [onmousedown], a, [role="button"]');
            if (interactive.length > 0) {
                var focusable = svg.querySelectorAll('[tabindex], a[href], [role="button"][tabindex]');
                if (focusable.length === 0) {
                    nodes.push(makeNode(svg, 'serious', 'SE SVG possui elementos interativos ENTÃO devem ser acessíveis por teclado. Encontrados ' + interactive.length + ' elementos clicáveis sem tabindex (NBR 5.5.1).'));
                }
            }
        }
        return violation('ift-svg-interactive-keyboard', 'serious', ['wcag2a', 'nbr17225-5.5.1', 'wcag211'], 'Elementos SVG interativos devem ser operáveis por teclado.', 'Adicione tabindex="0" e role="button" aos elementos SVG interativos, com manipuladores keydown.', 'https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html', nodes);
    };

    /** 40. IF decorative iframe THEN must have aria-hidden */
    WallyMonitor.IFThenRules.checkDecorativeIframeHidden = function(doc) {
        var nodes = [];
        var iframes = doc.querySelectorAll('iframe');
        for (var i = 0; i < iframes.length; i++) {
            var el = iframes[i];
            if (isHidden(el)) { continue; }
            var cs = getComputed(el);
            if (!cs) { continue; }
            var w = parseInt(cs.width, 10) || el.width || 999;
            var h = parseInt(cs.height, 10) || el.height || 999;
            // Very small iframes are likely tracking pixels
            if ((w <= 1 || h <= 1) && el.getAttribute('aria-hidden') !== 'true') {
                nodes.push(makeNode(el, 'minor', 'SE <iframe> possui 1×1 pixel (rastreamento/decorativo) ENTÃO deve ter aria-hidden="true". Leitores de tela não devem anunciá-lo (NBR 5.10.2).'));
            }
        }
        return violation('ift-decorative-iframe-hidden', 'minor', ['wcag2a', 'nbr17225-5.10.2', 'wcag412', 'best-practice'], 'Iframes decorativos ou de rastreamento devem ser ocultados de tecnologias assistivas.', 'Adicione aria-hidden="true" e tabindex="-1" a iframes decorativos/de rastreamento.', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 41. IF decorative img THEN must have alt="" + role="presentation" */
    WallyMonitor.IFThenRules.checkDecorativeImgRole = function(doc) {
        var nodes = [];
        var imgs = doc.querySelectorAll('img[alt=""]');
        for (var i = 0; i < imgs.length; i++) {
            var el = imgs[i];
            if (isHidden(el)) { continue; }
            var role = el.getAttribute('role');
            if (role !== 'presentation' && role !== 'none') {
                // Check if it really seems decorative (spacer, decorative class, tiny)
                var src = (el.getAttribute('src') || '').toLowerCase();
                var cls = (el.className || '').toLowerCase();
                if (src.indexOf('spacer') !== -1 || src.indexOf('pixel') !== -1 || src.indexOf('decorat') !== -1 || cls.indexOf('decorat') !== -1) {
                    nodes.push(makeNode(el, 'minor', 'SE img é decorativa (alt="") ENTÃO adicionar role="presentation" reforça a intenção. Esta imagem parece decorativa mas não possui role (NBR 5.1.1).'));
                }
            }
        }
        return violation('ift-decorative-img-role', 'minor', ['wcag2a', 'nbr17225-5.1.1', 'wcag111', 'best-practice'], 'Imagens decorativas com alt="" também devem ter role="presentation" para clareza.', 'Adicione role="presentation" ou role="none" para reforçar a intenção decorativa.', 'https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html', nodes);
    };

    /** 42. IF embedded map iframe THEN must have text alternative */
    WallyMonitor.IFThenRules.checkMapIframeAlternative = function(doc) {
        var nodes = [];
        var iframes = doc.querySelectorAll('iframe[src*="google.com/maps"], iframe[src*="maps.google"], iframe[src*="openstreetmap"], iframe[src*="bing.com/maps"], iframe[src*="mapbox"]');
        for (var i = 0; i < iframes.length; i++) {
            var el = iframes[i];
            if (isHidden(el)) { continue; }
            var title = el.getAttribute('title') || '';
            var label = el.getAttribute('aria-label') || '';
            if (!title && !label) {
                nodes.push(makeNode(el, 'serious', 'SE <iframe> de mapa embutido ENTÃO deve ter title descrevendo a localização. Leitores de tela não conseguem transmitir o conteúdo do mapa (NBR 5.1.1).'));
            }
            // Also check for text alternative near the map
            var parent = el.parentNode;
            var address = parent ? parent.querySelector('address, .address, [itemtype*="PostalAddress"]') : null;
            if (!address) {
                nodes.push(makeNode(el, 'moderate', 'SE mapa embutido ENTÃO deve ter um endereço em texto próximo como alternativa. Nem todos os usuários podem usar o mapa interativo (NBR 5.1.1).'));
            }
        }
        return violation('ift-map-iframe-alternative', 'serious', ['wcag2a', 'nbr17225-5.1.1', 'wcag111'], 'Mapas embutidos devem ter um title e uma alternativa de endereço em texto.', 'Adicione title="Mapa: Localização do Escritório" e forneça o endereço escrito próximo.', 'https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP F – Structure & Landmarks (rules 43–50)
    // ═══════════════════════════════════════════════════════════════════

    /** 43. IF nested header THEN must distinguish from page header */
    WallyMonitor.IFThenRules.checkNestedHeaderDistinct = function(doc) {
        var nodes = [];
        var headers = doc.querySelectorAll('article header, section header');
        for (var i = 0; i < headers.length; i++) {
            var el = headers[i];
            if (isHidden(el)) { continue; }
            if (!hasAriaLabel(el)) {
                var parent = el.closest('article, section');
                var parentTag = parent ? parent.tagName.toLowerCase() : 'unknown';
                nodes.push(makeNode(el, 'minor', 'SE <header> está aninhado dentro de <' + parentTag + '> ENTÃO deve ter aria-label para distinguir do cabeçalho banner da página (NBR 5.3.1).'));
            }
        }
        return violation('ift-nested-header-labeled', 'minor', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Elementos header aninhados devem ter aria-label para se distinguir do cabeçalho da página.', 'Adicione aria-label="Cabeçalho do Artigo" a elementos <header> aninhados.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 44. IF nested footer THEN must have aria-label */
    WallyMonitor.IFThenRules.checkNestedFooterLabeled = function(doc) {
        var nodes = [];
        var footers = doc.querySelectorAll('article footer, section footer');
        for (var i = 0; i < footers.length; i++) {
            var el = footers[i];
            if (isHidden(el)) { continue; }
            if (!hasAriaLabel(el)) {
                nodes.push(makeNode(el, 'minor', 'SE <footer> está aninhado dentro de article/section ENTÃO deve ter aria-label. Múltiplos footers sem rótulo confundem a navegação por marcos de leitores de tela (NBR 5.3.1).'));
            }
        }
        return violation('ift-nested-footer-labeled', 'minor', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Elementos footer aninhados devem ter aria-label para desambiguação de marcos.', 'Adicione aria-label="Rodapé do Artigo" a elementos <footer> aninhados.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 45. IF aside THEN must have aria-label */
    WallyMonitor.IFThenRules.checkAsideLabeled = function(doc) {
        var nodes = [];
        var asides = doc.querySelectorAll('aside');
        var count = 0;
        for (var i = 0; i < asides.length; i++) { if (!isHidden(asides[i])) { count++; } }
        if (count > 1) {
            for (var j = 0; j < asides.length; j++) {
                var el = asides[j];
                if (isHidden(el)) { continue; }
                if (!hasAriaLabel(el)) {
                    nodes.push(makeNode(el, 'moderate', 'SE múltiplos <aside> ENTÃO cada um deve ter aria-label. ' + count + ' asides encontrados sem rótulos — leitores de tela os listam de forma idêntica (NBR 5.3.1).'));
                }
            }
        }
        return violation('ift-aside-labeled', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Quando múltiplos elementos aside existem, cada um deve ter um aria-label único.', 'Adicione aria-label="Barra Lateral" ou aria-label="Conteúdo Relacionado" a cada aside.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 46. IF multiple main THEN flag violation */
    WallyMonitor.IFThenRules.checkUniqueMain = function(doc) {
        var nodes = [];
        var mains = doc.querySelectorAll('main, [role="main"]');
        var visible = [];
        for (var i = 0; i < mains.length; i++) {
            if (!isHidden(mains[i])) { visible.push(mains[i]); }
        }
        if (visible.length > 1) {
            for (var j = 1; j < visible.length; j++) {
                nodes.push(makeNode(visible[j], 'serious', 'SE a página possui <main> ENTÃO deve haver apenas um visível. Encontrados ' + visible.length + ' marcos main — confunde a navegação de tecnologias assistivas (NBR 5.3.1).'));
            }
        } else if (visible.length === 0) {
            var body = doc.body;
            if (body) {
                nodes.push(makeNode(body, 'serious', 'SE a página existe ENTÃO deve ter um marco <main>. Nenhum <main> ou role="main" encontrado (NBR 5.3.1).'));
            }
        }
        return violation('ift-unique-main', 'serious', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Páginas devem ter exatamente um marco <main> visível.', 'Garanta que apenas um elemento <main> esteja visível por vez.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 47. IF section THEN must have heading or aria-label */
    WallyMonitor.IFThenRules.checkSectionLabeled = function(doc) {
        var nodes = [];
        var sections = doc.querySelectorAll('section');
        for (var i = 0; i < sections.length; i++) {
            var el = sections[i];
            if (isHidden(el)) { continue; }
            // Skip wrapper sections that contain child sections (structural only)
            if (el.querySelector('section')) { continue; }
            // Skip sections with known layout/framework classes
            var cls = (el.className || '').toLowerCase();
            if (/\b(container|wrapper|row|col|inner|outer|layout|grid)\b/.test(cls)) { continue; }
            var heading = el.querySelector('h1, h2, h3, h4, h5, h6');
            if (!heading && !hasAriaLabel(el)) {
                var text = (el.textContent || '').trim();
                if (text.length > 100) {
                    nodes.push(makeNode(el, 'moderate', 'SE <section> ENTÃO deve ter um heading ou aria-label. Esta seção possui conteúdo mas não possui rótulo identificador (NBR 5.3.1).'));
                }
            }
        }
        return violation('ift-section-labeled', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Elementos section devem ser rotulados com um heading ou aria-label.', 'Adicione um heading (<h2>, <h3>) ou aria-label="Finalidade da Seção" a cada seção.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 48. IF article THEN must have heading */
    WallyMonitor.IFThenRules.checkArticleHeading = function(doc) {
        var nodes = [];
        var articles = doc.querySelectorAll('article');
        for (var i = 0; i < articles.length; i++) {
            var el = articles[i];
            if (isHidden(el)) { continue; }
            // Skip comment articles (WP uses article for individual comments)
            var cls = (el.className || '').toLowerCase();
            var id = (el.id || '').toLowerCase();
            if (/\bcomment\b/.test(cls) || /\bcomment\b/.test(id)) { continue; }
            // Skip if inside a comment list
            if (el.closest('.comments, .comment-list, .comment-respond, #comments')) { continue; }
            // Skip tiny articles (product cards, social embeds, etc.)
            var text = (el.textContent || '').trim();
            if (text.length < 50) { continue; }
            var heading = el.querySelector('h1, h2, h3, h4, h5, h6');
            if (!heading && !hasAriaLabel(el)) {
                nodes.push(makeNode(el, 'moderate', 'SE <article> ENTÃO deve ter um heading identificando seu conteúdo. Nenhum heading ou aria-label encontrado (NBR 5.3.1).'));
            }
        }
        return violation('ift-article-has-heading', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Elementos article devem ter headings para identificação.', 'Adicione um heading (<h2>) dentro de cada article para identificar seu conteúdo.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 49. IF multiple search landmarks THEN must have unique labels */
    WallyMonitor.IFThenRules.checkSearchLandmarkLabeled = function(doc) {
        var nodes = [];
        var searches = doc.querySelectorAll('[role="search"], search');
        var visible = [];
        for (var i = 0; i < searches.length; i++) {
            if (!isHidden(searches[i])) { visible.push(searches[i]); }
        }
        if (visible.length > 1) {
            for (var j = 0; j < visible.length; j++) {
                if (!hasAriaLabel(visible[j])) {
                    nodes.push(makeNode(visible[j], 'moderate', 'SE múltiplos marcos de busca existem ENTÃO cada um deve ter um aria-label único. ' + visible.length + ' buscas sem rótulo encontradas (NBR 5.3.1).'));
                }
            }
        }
        return violation('ift-search-landmark-labeled', 'moderate', ['wcag2a', 'nbr17225-5.3.1', 'wcag131'], 'Múltiplos marcos de busca devem ter aria-labels únicos.', 'Adicione aria-label="Busca do Site" ou aria-label="Busca de Produtos" para distinguir.', 'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html', nodes);
    };

    /** 50. IF page has modal open THEN background must have aria-hidden */
    WallyMonitor.IFThenRules.checkModalBackgroundHidden = function(doc) {
        var nodes = [];
        var modals = doc.querySelectorAll('[role="dialog"]:not([aria-hidden="true"]), dialog[open]');
        for (var i = 0; i < modals.length; i++) {
            var modal = modals[i];
            if (isHidden(modal)) { continue; }
            // Check if modal has inert or aria-modal
            var ariaModal = modal.getAttribute('aria-modal');
            if (ariaModal !== 'true') {
                nodes.push(makeNode(modal, 'serious', 'SE diálogo modal está aberto ENTÃO deve ter aria-modal="true" para prender o foco do leitor de tela. Conteúdo de fundo permanece acessível (NBR 5.5.2).'));
            }
        }
        return violation('ift-modal-aria-modal', 'serious', ['wcag2a', 'nbr17225-5.5.2', 'wcag212'], 'Diálogos modais abertos devem usar aria-modal="true" para impedir interação com o fundo.', 'Adicione aria-modal="true" ao elemento de diálogo quando estiver aberto.', 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP G – Dynamic Content & Interaction (rules 51–57)
    // ═══════════════════════════════════════════════════════════════════

    /** 51. IF contenteditable THEN must have role and label */
    WallyMonitor.IFThenRules.checkContentEditableAccessible = function(doc) {
        var nodes = [];
        var editables = doc.querySelectorAll('[contenteditable="true"], [contenteditable=""]');
        for (var i = 0; i < editables.length; i++) {
            var el = editables[i];
            if (isHidden(el)) { continue; }
            var role = el.getAttribute('role');
            if (!role) {
                nodes.push(makeNode(el, 'serious', 'SE contenteditable="true" ENTÃO deve ter role="textbox" e aria-label. Leitores de tela não o anunciam como editável (NBR 5.10.2).'));
            }
            if (!hasAriaLabel(el)) {
                nodes.push(makeNode(el, 'serious', 'SE contenteditable="true" ENTÃO deve ter aria-label descrevendo o que editar. Nenhum nome acessível encontrado (NBR 5.10.2).'));
            }
        }
        return violation('ift-contenteditable-accessible', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Elementos contenteditable devem ter role="textbox" e aria-label.', 'Adicione role="textbox", aria-label="Editar conteúdo" e aria-multiline="true".', 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html', nodes);
    };

    /** 52. IF infinite scroll THEN must provide alternative pagination */
    WallyMonitor.IFThenRules.checkInfiniteScrollAlternative = function(doc) {
        var nodes = [];
        var indicators = doc.querySelectorAll('.infinite-scroll, [data-infinite-scroll], .load-more-container, [class*="infinite"], [data-page], [data-load-more]');
        for (var i = 0; i < indicators.length; i++) {
            var el = indicators[i];
            if (isHidden(el)) { continue; }
            // Check if there's also a traditional pagination
            var pagination = doc.querySelector('.pagination, .nav-links, .page-numbers, [role="navigation"][aria-label*="paginat"], [role="navigation"][aria-label*="Paginat"]');
            if (!pagination) {
                nodes.push(makeNode(el, 'serious', 'SE rolagem infinita é utilizada ENTÃO deve fornecer paginação alternativa. Usuários de teclado e leitores de tela podem não acionar o carregamento baseado em rolagem (NBR 5.5.1).'));
            }
        }
        return violation('ift-infinite-scroll-alternative', 'serious', ['wcag2a', 'nbr17225-5.5.1', 'wcag211'], 'Rolagem infinita deve fornecer paginação tradicional como alternativa.', 'Adicione um botão "Carregar Mais" ou links de páginas tradicionais junto à rolagem infinita.', 'https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html', nodes);
    };

    /** 53. IF accordion/collapsible THEN headers must have aria-expanded */
    WallyMonitor.IFThenRules.checkAccordionAriaExpanded = function(doc) {
        var nodes = [];
        var triggers = doc.querySelectorAll('.accordion-header, .accordion-trigger, .collapse-trigger, [data-toggle="collapse"], [data-bs-toggle="collapse"], .wp-block-details summary, details summary');
        for (var i = 0; i < triggers.length; i++) {
            var el = triggers[i];
            if (isHidden(el)) { continue; }
            if (el.tagName.toLowerCase() === 'summary') { continue; } // native HTML details/summary handles this
            if (!el.hasAttribute('aria-expanded')) {
                nodes.push(makeNode(el, 'serious', 'SE gatilho de acordeão existe ENTÃO deve ter aria-expanded="true|false". Leitores de tela não conseguem determinar se a seção está aberta ou fechada (NBR 5.10.2).'));
            }
        }
        return violation('ift-accordion-aria-expanded', 'serious', ['wcag2a', 'nbr17225-5.10.2', 'wcag412'], 'Gatilhos de acordeão devem comunicar o estado expandido/recolhido via aria-expanded.', 'Adicione aria-expanded="false" inicialmente e alterne para "true" quando expandido.', 'https://www.w3.org/WAI/ARIA/apg/patterns/accordion/', nodes);
    };

    /** 54. IF tooltip on non-focusable element THEN must make it focusable */
    WallyMonitor.IFThenRules.checkTooltipFocusable = function(doc) {
        var nodes = [];
        // Only check explicit tooltip library attributes — [title] is NOT a WCAG 1.4.13 tooltip
        var tooltips = doc.querySelectorAll('[data-tooltip]:not(a):not(button):not(input):not(select):not(textarea), [data-tippy]:not(a):not(button):not(input), [data-tippy-content]:not(a):not(button):not(input), [data-bs-toggle="tooltip"]:not(a):not(button):not(input), [data-toggle="tooltip"]:not(a):not(button):not(input)');
        for (var i = 0; i < tooltips.length; i++) {
            var el = tooltips[i];
            if (isHidden(el)) { continue; }
            var tag = el.tagName.toLowerCase();
            if (tag === 'html' || tag === 'head' || tag === 'body' || tag === 'meta' || tag === 'link' || tag === 'script' || tag === 'style') { continue; }
            var ti = el.getAttribute('tabindex');
            if (ti === null) {
                nodes.push(makeNode(el, 'moderate', 'SE tooltip em elemento não focalizável ENTÃO o elemento deve ser focalizável. Usuários de teclado não conseguem acessar este tooltip (NBR 5.4.13).'));
            }
        }
        return violation('ift-tooltip-focusable', 'moderate', ['wcag2aa', 'nbr17225-5.4.13', 'wcag1413'], 'Elementos com tooltips devem ser focalizáveis por teclado.', 'Adicione tabindex="0" a elementos com atributos data-tooltip.', 'https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html', nodes);
    };

    /** 55. IF loading spinner THEN must have live region announcement */
    WallyMonitor.IFThenRules.checkLoadingSpinnerAria = function(doc) {
        var nodes = [];
        var spinners = doc.querySelectorAll('.spinner, .loading, .loader, [class*="loading-spinner"], [class*="sk-"], .lds-ring, .lds-spinner, .wp-spinner, [class*="preloader"]');
        for (var i = 0; i < spinners.length; i++) {
            var el = spinners[i];
            var cs = getComputed(el);
            if (cs && cs.display === 'none') { continue; }
            var role = el.getAttribute('role');
            var ariaLabel = el.getAttribute('aria-label') || '';
            var ariaLive = el.getAttribute('aria-live') || '';
            var parentLive = el.parentNode ? el.parentNode.getAttribute('aria-live') : null;
            if (role !== 'status' && role !== 'alert' && role !== 'progressbar' && !ariaLive && !parentLive) {
                nodes.push(makeNode(el, 'moderate', 'SE indicador de carregamento está visível ENTÃO deve ter role="status" ou estar dentro de uma região aria-live. Leitores de tela não anunciarão o estado de carregamento (NBR 5.10.3).'));
            }
            if (!ariaLabel && !(el.textContent || '').trim()) {
                nodes.push(makeNode(el, 'moderate', 'SE indicador de carregamento ENTÃO deve ter aria-label="Carregando..." ou texto visível. Leitores de tela veem um elemento vazio (NBR 5.10.3).'));
            }
        }
        return violation('ift-loading-spinner-aria', 'moderate', ['wcag2aa', 'nbr17225-5.10.3', 'wcag413'], 'Indicadores de carregamento devem anunciar seu estado para leitores de tela.', 'Adicione role="status" e aria-label="Carregando, aguarde" aos contêineres de indicadores de carregamento.', 'https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html', nodes);
    };

    /** 56. IF cookie banner/popup THEN must be keyboard-navigable */
    WallyMonitor.IFThenRules.checkCookieBannerAccessible = function(doc) {
        var nodes = [];
        var banners = doc.querySelectorAll('[class*="cookie"], [class*="consent"], [class*="gdpr"], [class*="lgpd"], [id*="cookie"], [id*="consent"], [id*="gdpr"], [id*="lgpd"]');
        for (var i = 0; i < banners.length; i++) {
            var el = banners[i];
            var cs = getComputed(el);
            if (cs && cs.display === 'none') { continue; }
            var buttons = el.querySelectorAll('button, a, input[type="button"], input[type="submit"], [role="button"]');
            if (buttons.length === 0) {
                nodes.push(makeNode(el, 'serious', 'SE banner de cookie/consentimento ENTÃO deve ter botões operáveis por teclado. Nenhum controle interativo encontrado (NBR 5.5.1).'));
            }
            var role = el.getAttribute('role');
            if (role !== 'dialog' && role !== 'alertdialog' && role !== 'region') {
                nodes.push(makeNode(el, 'moderate', 'SE banner de cookie/consentimento ENTÃO deve ter role="dialog" ou role="region" com aria-label. Não identificado semanticamente (NBR 5.10.2).'));
            }
            // Check for close/dismiss mechanism
            var close = el.querySelector('button[aria-label*="close"], button[aria-label*="fechar"], button[aria-label*="dismiss"], .close, [class*="close"], [class*="dismiss"]');
            if (!close && buttons.length > 0) {
                var hasReject = false;
                for (var j = 0; j < buttons.length; j++) {
                    var txt = (buttons[j].textContent || '').toLowerCase();
                    if (txt.indexOf('reject') !== -1 || txt.indexOf('recusar') !== -1 || txt.indexOf('decline') !== -1 || txt.indexOf('negar') !== -1) {
                        hasReject = true; break;
                    }
                }
                if (!hasReject) {
                    nodes.push(makeNode(el, 'serious', 'SE banner de cookie ENTÃO deve ter uma opção de rejeitar/fechar. Usuários devem poder dispensar sem aceitar (NBR 5.5.1, LGPD).'));
                }
            }
        }
        return violation('ift-cookie-banner-accessible', 'serious', ['wcag2a', 'nbr17225-5.5.1', 'wcag211'], 'Banners de cookie/consentimento devem ser totalmente acessíveis por teclado com opções claras de aceitar/rejeitar.', 'Adicione role="dialog", aria-label e botões de aceitar e rejeitar aos banners de cookie.', 'https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html', nodes);
    };

    /** 57. IF countdown/timer THEN must have live region */
    WallyMonitor.IFThenRules.checkCountdownLiveRegion = function(doc) {
        var nodes = [];
        var timers = doc.querySelectorAll('[class*="countdown"], [class*="timer"], [class*="clock"], [data-countdown], time[datetime]');
        for (var i = 0; i < timers.length; i++) {
            var el = timers[i];
            if (isHidden(el)) { continue; }
            // Only flag elements that appear to be dynamic countdowns
            var text = (el.textContent || '').trim();
            if (text.length < 2 || text.length > 100) { continue; }
            var ariaLive = el.getAttribute('aria-live');
            var role = el.getAttribute('role');
            if (el.tagName.toLowerCase() === 'time') { continue; } // static <time> is fine
            if (!ariaLive && role !== 'timer' && role !== 'status') {
                nodes.push(makeNode(el, 'moderate', 'SE contador/temporizador é exibido ENTÃO deve ter aria-live="polite" ou role="timer". Leitores de tela não anunciarão mudanças de tempo (NBR 5.10.3).'));
            }
        }
        return violation('ift-countdown-live-region', 'moderate', ['wcag2aa', 'nbr17225-5.10.3', 'wcag413'], 'Elementos dinâmicos de contador/temporizador devem anunciar atualizações para leitores de tela.', 'Adicione role="timer" ou aria-live="polite" a elementos de contagem regressiva.', 'https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html', nodes);
    };

    // ═══════════════════════════════════════════════════════════════════
    //  Runner — execute all 57 IF-THEN checks
    // ═══════════════════════════════════════════════════════════════════

    WallyMonitor.IFThenRules.runAllChecks = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return []; }

        var results = [
            // Group A – ARIA & Roles
            WallyMonitor.IFThenRules.checkRoleButtonKeyboard(doc),
            WallyMonitor.IFThenRules.checkTablistHasTabs(doc),
            WallyMonitor.IFThenRules.checkTabHasControls(doc),
            WallyMonitor.IFThenRules.checkDialogName(doc),
            WallyMonitor.IFThenRules.checkExpandedHasTarget(doc),
            WallyMonitor.IFThenRules.checkSliderValues(doc),
            WallyMonitor.IFThenRules.checkProgressbarValue(doc),
            WallyMonitor.IFThenRules.checkAlertDialogDesc(doc),
            WallyMonitor.IFThenRules.checkMenuChildren(doc),
            WallyMonitor.IFThenRules.checkTreeChildren(doc),

            // Group B – Forms & Inputs
            WallyMonitor.IFThenRules.checkRadioGrouping(doc),
            WallyMonitor.IFThenRules.checkCheckboxGrouping(doc),
            WallyMonitor.IFThenRules.checkRequiredAria(doc),
            WallyMonitor.IFThenRules.checkFileInputLabel(doc),
            WallyMonitor.IFThenRules.checkRangeAccessibility(doc),
            WallyMonitor.IFThenRules.checkTextareaLabel(doc),
            WallyMonitor.IFThenRules.checkSelectMultipleInstructions(doc),
            WallyMonitor.IFThenRules.checkPatternTitle(doc),
            WallyMonitor.IFThenRules.checkDisabledNotFocusable(doc),
            WallyMonitor.IFThenRules.checkFormSubmitLabel(doc),

            // Group C – Tables
            WallyMonitor.IFThenRules.checkThHasScope(doc),
            WallyMonitor.IFThenRules.checkComplexTableHeaders(doc),
            WallyMonitor.IFThenRules.checkTableCaption(doc),
            WallyMonitor.IFThenRules.checkPresentationTableClean(doc),
            WallyMonitor.IFThenRules.checkColspanHeaders(doc),
            WallyMonitor.IFThenRules.checkNestedTables(doc),
            WallyMonitor.IFThenRules.checkSortableTable(doc),

            // Group D – Links & Navigation
            WallyMonitor.IFThenRules.checkNewWindowWarning(doc),
            WallyMonitor.IFThenRules.checkHashLink(doc),
            WallyMonitor.IFThenRules.checkJavascriptHref(doc),
            WallyMonitor.IFThenRules.checkAdjacentDuplicateLinks(doc),
            WallyMonitor.IFThenRules.checkAriaCurrentPage(doc),
            WallyMonitor.IFThenRules.checkPaginationSemantic(doc),
            WallyMonitor.IFThenRules.checkDownloadLinkInfo(doc),
            WallyMonitor.IFThenRules.checkAnchorTarget(doc),

            // Group E – Media & Embeds
            WallyMonitor.IFThenRules.checkVideoCaptions(doc),
            WallyMonitor.IFThenRules.checkAudioTranscript(doc),
            WallyMonitor.IFThenRules.checkCanvasFallback(doc),
            WallyMonitor.IFThenRules.checkInteractiveSvgKeyboard(doc),
            WallyMonitor.IFThenRules.checkDecorativeIframeHidden(doc),
            WallyMonitor.IFThenRules.checkDecorativeImgRole(doc),
            WallyMonitor.IFThenRules.checkMapIframeAlternative(doc),

            // Group F – Structure & Landmarks
            WallyMonitor.IFThenRules.checkNestedHeaderDistinct(doc),
            WallyMonitor.IFThenRules.checkNestedFooterLabeled(doc),
            WallyMonitor.IFThenRules.checkAsideLabeled(doc),
            WallyMonitor.IFThenRules.checkUniqueMain(doc),
            WallyMonitor.IFThenRules.checkSectionLabeled(doc),
            WallyMonitor.IFThenRules.checkArticleHeading(doc),
            WallyMonitor.IFThenRules.checkSearchLandmarkLabeled(doc),
            WallyMonitor.IFThenRules.checkModalBackgroundHidden(doc),

            // Group G – Dynamic Content & Interaction
            WallyMonitor.IFThenRules.checkContentEditableAccessible(doc),
            WallyMonitor.IFThenRules.checkInfiniteScrollAlternative(doc),
            WallyMonitor.IFThenRules.checkAccordionAriaExpanded(doc),
            WallyMonitor.IFThenRules.checkTooltipFocusable(doc),
            WallyMonitor.IFThenRules.checkLoadingSpinnerAria(doc),
            WallyMonitor.IFThenRules.checkCookieBannerAccessible(doc),
            WallyMonitor.IFThenRules.checkCountdownLiveRegion(doc)
        ];

        return results.filter(function(v) { return v !== null; });
    };

})(jQuery);
