/**
 * Wally Monitor - NBR 17.225 Custom Rules
 * Brazilian Accessibility Standard rules that axe-core doesn't cover.
 * Competitive advantage: +25-40pp coverage over axe-core/WAVE/Lighthouse alone.
 *
 * @module WallyMonitor.NBRRules
 * @since 1.1.0
 */
(function($) {
    'use strict';

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

    // ─── 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 1 – WCAG 2.2 New Criteria
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 1. nbr-focus-not-obscured (WCAG 2.4.11 / NBR 5.6.11)
     * Fixed/sticky elements with high z-index may obscure focused elements.
     */
    WallyMonitor.NBRRules.checkFocusNotObscured = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        // Targeted selector: only elements likely to be fixed/sticky overlays
        var candidates = doc.querySelectorAll('header, footer, nav, div, aside, [style*="position"], [class*="sticky"], [class*="fixed"], [class*="toolbar"], [class*="overlay"], [class*="modal"], [class*="popup"], [class*="banner"], [class*="toast"], [class*="notification"]');

        for (var i = 0; i < candidates.length; i++) {
            var cs = getComputed(candidates[i]);
            if (!cs) { continue; }

            var pos = cs.getPropertyValue('position');
            if (pos !== 'fixed' && pos !== 'sticky') { continue; }

            var zi = parseInt(cs.getPropertyValue('z-index'), 10);
            if (isNaN(zi) || zi <= 100) { continue; }

            var rect = candidates[i].getBoundingClientRect();
            if (rect.width < 50 || rect.height < 30) { continue; }

            nodes.push(makeNode(
                candidates[i],
                'serious',
                'Este elemento fixo/sticky (z-index: ' + zi + ') pode ocultar elementos focados atrás dele. Garanta que o conteúdo focado não seja totalmente escondido por camadas sobrepostas (WCAG 2.4.11).'
            ));
        }

        return violation(
            'nbr-focus-not-obscured',
            'serious',
            ['wcag22aa', 'nbr17225-5.6.11', 'wcag2411'],
            'Elementos focados não devem ser totalmente obscurecidos por conteúdo criado pelo autor.',
            'Garanta que sobreposições fixas/sticky não escondam completamente o elemento com foco do teclado.',
            'https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html',
            nodes
        );
    };

    /**
     * 2. nbr-focus-appearance (WCAG 2.4.13 / NBR 5.6.13)
     * Focusable elements must have a visible focus indicator.
     */
    WallyMonitor.NBRRules.checkFocusAppearance = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Check stylesheets for outline:none / outline:0 on :focus
        try {
            var sheets = doc.styleSheets;
            for (var s = 0; s < sheets.length; s++) {
                var rules;
                try { rules = sheets[s].cssRules || sheets[s].rules; } catch (e) { continue; }
                if (!rules) { continue; }

                for (var r = 0; r < rules.length; r++) {
                    var rule = rules[r];
                    if (!rule.selectorText) { continue; }

                    var sel = rule.selectorText.toLowerCase();
                    if (sel.indexOf(':focus') === -1) { continue; }

                    var css = rule.cssText || '';
                    if (/outline\s*:\s*(none|0)\b/.test(css)) {
                        // Skip if rule provides alternative focus style
                        var hasAltFocus = /box-shadow\s*:/.test(css) || /border\s*:/.test(css) || /border-color\s*:/.test(css) || /background(-color)?\s*:/.test(css) || /outline-color\s*:/.test(css);
                        // Skip :focus-visible paired rules (progressive enhancement)
                        var isFocusVisible = sel.indexOf(':focus-visible') !== -1;
                        if (!hasAltFocus && !isFocusVisible) {
                            nodes.push({
                                target: [rule.selectorText],
                                html: '<style>' + rule.selectorText + ' { ' + rule.style.cssText + ' }</style>',
                                impact: 'serious',
                                message: 'A regra CSS "' + rule.selectorText + '" remove o contorno de foco sem fornecer um indicador alternativo (box-shadow, border, etc). Elementos focados devem ter um indicador visível (WCAG 2.4.13).'
                            });
                        }
                    }
                }
            }
        } catch (e) { /* cross-origin sheets – skip */ }

        // Check elements with tabindex that have inline styles suppressing outline
        var focusable = doc.querySelectorAll('[tabindex]');
        for (var i = 0; i < focusable.length; i++) {
            var style = focusable[i].getAttribute('style') || '';
            if (/outline\s*:\s*(none|0)\b/i.test(style)) {
                nodes.push(makeNode(
                    focusable[i],
                    'serious',
                    'Este elemento possui tabindex, mas seu estilo inline remove o contorno de foco. Forneça um indicador de foco visível.'
                ));
            }
        }

        return violation(
            'nbr-focus-appearance',
            'serious',
            ['wcag22aaa', 'nbr17225-5.6.13', 'wcag2413'],
            'Os indicadores de foco devem ser claramente visíveis quando o foco do teclado é aplicado.',
            'Garanta que elementos interativos tenham um indicador de foco visível com área e contraste suficientes.',
            'https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html',
            nodes
        );
    };

    /**
     * 3. nbr-dragging-alternative (WCAG 2.5.7 / NBR 5.7.7)
     * Drag-and-drop must have a single-pointer alternative.
     */
    WallyMonitor.NBRRules.checkDraggingAlternative = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var selectors = [
            '[draggable="true"]',
            '[data-sortable]',
            '.ui-sortable',
            '.ui-draggable',
            '.drag',
            '.draggable',
            '.sortable-list',
            '[data-drag]',
            '.cdk-drag'
        ];
        var els = doc.querySelectorAll(selectors.join(','));

        for (var i = 0; i < els.length; i++) {
            // Heuristic: check for nearby buttons or an aria-grabbed pattern
            var hasAlt = els[i].querySelector('button, [role="button"], .move-up, .move-down, [aria-grabbed]');
            if (hasAlt) { continue; }

            nodes.push(makeNode(
                els[i],
                'serious',
                'Este elemento de arrastar e soltar não possui uma alternativa aparente de ponteiro único. Forneça botões ou outro mecanismo para usuários que não podem realizar gestos de arrastar (WCAG 2.5.7).'
            ));
        }

        return violation(
            'nbr-dragging-alternative',
            'serious',
            ['wcag22aa', 'nbr17225-5.7.7', 'wcag257'],
            'Toda funcionalidade que utiliza arrastar deve ter uma alternativa de ponteiro único.',
            'Forneça um método de entrada alternativo (ex.: botões mover-cima/mover-baixo) para interações de arrastar.',
            'https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html',
            nodes
        );
    };

    /**
     * 4. nbr-target-size (WCAG 2.5.8 / NBR 5.7.8)
     * Clickable elements should have at least 24×24 px target size.
     */
    WallyMonitor.NBRRules.checkTargetSize = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var interactive = doc.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [onclick]');

        for (var i = 0; i < interactive.length; i++) {
            var el = interactive[i];

            // Exclude inline links inside paragraphs
            if (el.tagName === 'A' && el.parentNode && el.parentNode.tagName === 'P') { continue; }
            // Exclude hidden elements
            var cs = getComputed(el);
            if (!cs || cs.display === 'none' || cs.visibility === 'hidden') { continue; }

            var rect = el.getBoundingClientRect();
            if (rect.width === 0 && rect.height === 0) { continue; }

            if (rect.width < 24 || rect.height < 24) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este elemento interativo tem ' + Math.round(rect.width) + '×' + Math.round(rect.height) + 'px, abaixo do tamanho mínimo de área de toque de 24×24px (WCAG 2.5.8).'
                ));
            }
        }

        return violation(
            'nbr-target-size',
            'moderate',
            ['wcag22aa', 'nbr17225-5.7.8', 'wcag258'],
            'Áreas de toque interativas devem ter pelo menos 24×24 pixels CSS.',
            'Garanta que elementos clicáveis/tocáveis tenham pelo menos 24×24px de área de toque.',
            'https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html',
            nodes
        );
    };

    /**
     * 5. nbr-consistent-help (WCAG 3.2.6 / NBR 5.8.6)
     * Help mechanisms should appear consistently across pages.
     */
    WallyMonitor.NBRRules.checkConsistentHelp = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var helpKeywords = /\b(ajuda|help|suporte|support|faqs?|contato|contact|atendimento|chat|dúvida|duvida)\b/i;

        var links = doc.querySelectorAll('a, button, [role="button"]');
        var helpElements = [];

        for (var i = 0; i < links.length; i++) {
            var text = (links[i].textContent || '').trim();
            var ariaLabel = links[i].getAttribute('aria-label') || '';
            if (helpKeywords.test(text) || helpKeywords.test(ariaLabel)) {
                helpElements.push(links[i]);
            }
        }

        // Also check for chat widgets
        var chatWidgets = doc.querySelectorAll('#hubspot-messages-iframe-container, .intercom-lightweight-app, #drift-widget, .tawk-min-container, [id*="chat-widget"], [class*="chat-widget"], .crisp-client');
        for (var j = 0; j < chatWidgets.length; j++) {
            helpElements.push(chatWidgets[j]);
        }

        // Rule: if there are no help mechanisms at all, flag
        if (helpElements.length === 0) {
            return violation(
                'nbr-consistent-help',
                'minor',
                ['wcag22a', 'nbr17225-5.8.6', 'wcag326'],
                'Mecanismos de ajuda devem estar disponíveis e posicionados de forma consistente.',
                'Forneça um mecanismo de ajuda (link de contato, FAQ, chat) em uma localização consistente entre as páginas.',
                'https://www.w3.org/WAI/WCAG22/Understanding/consistent-help.html',
                [{
                    target: ['html'],
                    html: '<html>',
                    impact: 'minor',
                    message: 'Nenhum mecanismo de ajuda (link de ajuda, FAQ, contato, chat) foi detectado nesta página. A WCAG 3.2.6 exige disponibilidade consistente de ajuda.'
                }]
            );
        }

        return null;
    };

    /**
     * 6. nbr-redundant-entry (WCAG 3.3.7 / NBR 5.9.7)
     * Avoid making users re-enter information already provided.
     */
    WallyMonitor.NBRRules.checkRedundantEntry = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var confirmPatterns = doc.querySelectorAll('input[name*="confirm"], input[name*="repeat"], input[name*="retype"], input[name*="confirma"], input[name*="repet"], input[name*="re_"], input[name*="re-"], input[id*="confirm"], input[id*="repeat"], input[id*="retype"]');

        for (var i = 0; i < confirmPatterns.length; i++) {
            var el = confirmPatterns[i];
            if (el.getAttribute('autocomplete')) { continue; }

            nodes.push(makeNode(
                el,
                'moderate',
                'Este campo de confirmação/repetição não possui atributo autocomplete. Considere o preenchimento automático ou a remoção da entrada redundante (WCAG 3.3.7).'
            ));
        }

        return violation(
            'nbr-redundant-entry',
            'moderate',
            ['wcag22a', 'nbr17225-5.9.7', 'wcag337'],
            'Informações previamente inseridas devem ser preenchidas automaticamente ou selecionáveis.',
            'Evite exigir que os usuários reinsiram informações já fornecidas no mesmo processo.',
            'https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html',
            nodes
        );
    };

    /**
     * 7. nbr-accessible-auth (WCAG 3.3.8 / NBR 5.9.8)
     * CAPTCHAs must have an accessible alternative.
     */
    WallyMonitor.NBRRules.checkAccessibleAuth = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var captchaSelectors = [
            '.g-recaptcha',
            'iframe[src*="recaptcha"]',
            '.h-captcha',
            'iframe[src*="hcaptcha"]',
            'img[src*="captcha"]',
            '[class*="captcha"]',
            '#captcha',
            '.cf-turnstile'
        ];
        var captchas = doc.querySelectorAll(captchaSelectors.join(','));

        for (var i = 0; i < captchas.length; i++) {
            var el = captchas[i];
            // Check if there's an audio alternative or accessible fallback nearby
            var parent = el.parentNode;
            var hasAlt = false;
            if (parent) {
                var altElements = parent.querySelectorAll('[aria-label*="audio"], [aria-label*="áudio"], button[class*="audio"], .captcha-audio, .captcha-alternative');
                if (altElements.length > 0) { hasAlt = true; }
            }

            if (!hasAlt) {
                nodes.push(makeNode(
                    el,
                    'critical',
                    'CAPTCHA detectado sem uma alternativa acessível aparente. Forneça CAPTCHA de áudio, quebra-cabeças lógicos ou outra autenticação independente de função cognitiva (WCAG 3.3.8).'
                ));
            }
        }

        return violation(
            'nbr-accessible-auth',
            'critical',
            ['wcag22aa', 'nbr17225-5.9.8', 'wcag338'],
            'A autenticação não deve depender de testes de função cognitiva, a menos que uma alternativa seja fornecida.',
            'Garanta que elementos CAPTCHA tenham alternativas acessíveis (áudio, copiar-colar, biometria).',
            'https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 2 – Language & Text (NBR 5.8.x)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 8. nbr-lang-attribute (WCAG 3.1.1 / NBR 5.8.1)
     * The <html> element must have a valid lang attribute.
     */
    WallyMonitor.NBRRules.checkLangAttribute = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var html = doc.documentElement || null;
        var lang = html ? html.getAttribute('lang') : (doc.getAttribute ? doc.getAttribute('lang') : null);

        if (!lang || lang.trim() === '') {
            nodes.push({
                target: ['html'],
                html: outerSnippet(html, 120),
                impact: 'critical',
                message: 'O elemento <html> não possui o atributo lang. Leitores de tela dependem deste atributo para usar a pronúncia correta (WCAG 3.1.1).'
            });
        } else {
            // Validate it's a real BCP 47 tag
            var validLang = /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/;
            if (!validLang.test(lang.trim())) {
                nodes.push({
                    target: ['html'],
                    html: outerSnippet(html, 120),
                    impact: 'serious',
                    message: 'O valor do atributo lang "' + lang + '" não parece ser uma tag de idioma BCP 47 válida.'
                });
            }
        }

        return violation(
            'nbr-lang-attribute',
            'critical',
            ['wcag2a', 'nbr17225-5.8.1', 'wcag311'],
            'A página deve ter um atributo lang válido no elemento html.',
            'Garanta que o elemento <html> possua um atributo lang com um código de idioma BCP 47 válido.',
            'https://www.w3.org/WAI/WCAG22/Understanding/language-of-page.html',
            nodes
        );
    };

    /**
     * 9. nbr-lang-parts (WCAG 3.1.2 / NBR 5.8.2)
     * Foreign-language passages should be marked with the lang attribute.
     */
    WallyMonitor.NBRRules.checkLangParts = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var htmlEl = doc.documentElement || doc;
        var pageLang = ((htmlEl.getAttribute ? htmlEl.getAttribute('lang') : '') || '').substring(0, 2).toLowerCase();
        if (pageLang !== 'pt') { return null; } // Only runs on Portuguese pages

        var nodes = [];
        // Common English phrases that should be marked as lang="en"
        var englishPattern = /\b(click here|read more|learn more|sign up|log in|submit|newsletter|download|copyright|all rights reserved|privacy policy|terms of service|cookies? policy|powered by|designed by|built with|follow us|share this|subscribe|checkout|add to cart|buy now|free shipping|best seller|on sale)\b/gi;

        var textElements = doc.querySelectorAll('p, span, li, td, th, h1, h2, h3, h4, h5, h6, div, a, label');
        var checked = 0;

        for (var i = 0; i < textElements.length && checked < 200; i++) {
            var el = textElements[i];
            if (el.getAttribute('lang')) { continue; }

            // Only check direct text, not child elements' text
            var directText = '';
            for (var c = 0; c < el.childNodes.length; c++) {
                if (el.childNodes[c].nodeType === 3) {
                    directText += el.childNodes[c].nodeValue;
                }
            }
            directText = directText.trim();
            if (directText.length < 10) { continue; }

            checked++;
            var matches = directText.match(englishPattern);
            if (matches && matches.length >= 1) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este elemento contém texto em inglês ("' + matches[0] + '") dentro de uma página em português, mas não possui o atributo lang="en" (WCAG 3.1.2).'
                ));
            }
        }

        return violation(
            'nbr-lang-parts',
            'moderate',
            ['wcag2aa', 'nbr17225-5.8.2', 'wcag312'],
            'Partes do conteúdo em um idioma diferente devem ser identificadas com o atributo lang.',
            'Marque trechos de texto em idioma estrangeiro com o atributo lang apropriado.',
            'https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html',
            nodes
        );
    };

    /**
     * 10. nbr-abbreviations (WCAG 3.1.4 / NBR 5.8.4)
     * Common abbreviations should use the <abbr> tag.
     */
    WallyMonitor.NBRRules.checkAbbreviations = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var abbrs = /\b(HTML|CSS|API|URL|SEO|WCAG|PDF|FAQ|HTTP|HTTPS|JSON|XML|SQL|PHP|REST|AJAX|DOM|CDN|CMS|CTA|UX|UI|SVG|RSS|FTP|SMTP|IMAP|DNS|SSL|TLS|LGPD|IBGE|INSS|CPF|CNPJ|RG|CEP|ABNT|NBR)\b/g;

        var textElements = doc.querySelectorAll('p, li, td, th, span, div, h1, h2, h3, h4, h5, h6, label, figcaption');
        var reported = {};
        var checked = 0;

        for (var i = 0; i < textElements.length && checked < 150; i++) {
            var el = textElements[i];
            // Skip if inside <abbr>
            if (el.closest && el.closest('abbr')) { continue; }

            var directText = '';
            for (var c = 0; c < el.childNodes.length; c++) {
                if (el.childNodes[c].nodeType === 3) {
                    directText += el.childNodes[c].nodeValue;
                }
            }
            if (!directText.trim()) { continue; }

            checked++;
            var matches = directText.match(abbrs);
            if (matches) {
                for (var m = 0; m < matches.length; m++) {
                    if (reported[matches[m]]) { continue; }
                    // Check if already wrapped in <abbr> within this element
                    var abbrChildren = el.querySelectorAll('abbr');
                    var isWrapped = false;
                    for (var a = 0; a < abbrChildren.length; a++) {
                        if ((abbrChildren[a].textContent || '').indexOf(matches[m]) !== -1) {
                            isWrapped = true;
                            break;
                        }
                    }
                    if (isWrapped) { continue; }

                    reported[matches[m]] = true;
                    nodes.push(makeNode(
                        el,
                        'minor',
                        'A abreviatura "' + matches[m] + '" é usada sem uma tag <abbr>. Envolva-a em <abbr title="..."> para fornecer a forma expandida (WCAG 3.1.4).'
                    ));
                }
            }
        }

        return violation(
            'nbr-abbreviations',
            'minor',
            ['wcag2aaa', 'nbr17225-5.8.4', 'wcag314'],
            'Abreviaturas devem ser expandidas ou definidas usando o elemento <abbr>.',
            'Envolva abreviaturas em <abbr title="Forma Expandida"> para ajudar os usuários a entender acrônimos.',
            'https://www.w3.org/WAI/WCAG22/Understanding/abbreviations.html',
            nodes
        );
    };

    /**
     * 11. nbr-reading-level (WCAG 3.1.5 / NBR 5.8.5)
     * Text should not be excessively complex (average > 30 words per sentence).
     */
    WallyMonitor.NBRRules.checkReadingLevel = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var paragraphs = doc.querySelectorAll('p');

        for (var i = 0; i < paragraphs.length && i < 50; i++) {
            // Skip code blocks, preformatted text, and short paragraphs
            if (paragraphs[i].closest('pre, code, .code-block, .wp-block-code, .wp-block-preformatted')) { continue; }
            var text = (paragraphs[i].textContent || '').trim();
            if (text.length < 80) { continue; }

            // Normalize abbreviations to avoid false sentence splits
            var normalized = text
                .replace(/\b(Dr|Sr|Sra|Jr|Prof|Eng|Arq|Adv|Ex|etc|vs|ed|vol|fig|cap|art|inc|ltda|cia|nº|tel|fax|aprox|pág|págs|obs|ref|resp)\./gi, '$1§')
                .replace(/\b(Mr|Mrs|Ms|Dr|Jr|Sr|St|Inc|Ltd|Corp|Co|vs|etc|approx|dept|govt|est|assn|avg|e\.g|i\.e)\./gi, '$1§')
                .replace(/\d+\.\d+/g, 'NUM')
                .replace(/\.{2,}/g, '…');

            // Split sentences by period, exclamation, question mark
            var sentences = normalized.split(/[.!?]+/).filter(function(s) { return s.trim().length > 0; });
            if (sentences.length < 2) { continue; }

            var totalWords = 0;
            for (var s = 0; s < sentences.length; s++) {
                totalWords += sentences[s].trim().split(/\s+/).length;
            }
            var avg = totalWords / sentences.length;

            if (avg > 35) {
                nodes.push(makeNode(
                    paragraphs[i],
                    'minor',
                    'Este parágrafo tem uma média de ' + Math.round(avg) + ' palavras por frase (recomendado: ≤ 30). Considere simplificar o texto (WCAG 3.1.5).'
                ));
            }
        }

        return violation(
            'nbr-reading-level',
            'minor',
            ['wcag2aaa', 'nbr17225-5.8.5', 'wcag315'],
            'O conteúdo deve ser escrito da forma mais clara e simples possível.',
            'Mantenha frases concisas (idealmente ≤ 30 palavras) e forneça resumos para conteúdo complexo.',
            'https://www.w3.org/WAI/WCAG22/Understanding/reading-level.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 3 – Keyboard & Focus (NBR 5.5.x, 5.6.x)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 12. nbr-keyboard-trap (WCAG 2.1.2 / NBR 5.5.2)
     * Modals/dialogs must have a close button or Escape key handler.
     */
    WallyMonitor.NBRRules.checkKeyboardTrap = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var dialogs = doc.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog');

        for (var i = 0; i < dialogs.length; i++) {
            var el = dialogs[i];
            var cs = getComputed(el);
            // Only check visible dialogs
            if (cs && cs.display === 'none') { continue; }

            var hasCloseBtn = el.querySelector('[aria-label*="close"], [aria-label*="fechar"], [title*="close"], [title*="fechar"], .close, .modal-close, button.close, [data-dismiss="modal"]');
            var hasAriaModal = el.getAttribute('aria-modal') === 'true';

            if (!hasCloseBtn) {
                nodes.push(makeNode(
                    el,
                    'critical',
                    'Este dialog/modal não possui um botão de fechar aparente. Os usuários devem poder fechá-lo por meio de um botão ou pela tecla Escape (WCAG 2.1.2).'
                ));
            }

            if (!hasAriaModal && el.getAttribute('role') === 'dialog') {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este dialog possui role="dialog" mas não possui aria-modal="true". Isso pode impedir o aprisionamento adequado de foco e a navegação por teclado.'
                ));
            }
        }

        return violation(
            'nbr-keyboard-trap',
            'critical',
            ['wcag2a', 'nbr17225-5.5.2', 'wcag212'],
            'Sem armadilha de teclado: os usuários devem poder navegar para fora de qualquer componente usando o teclado.',
            'Garanta que todos os modais/dialogs tenham um botão de fechar e suportem a tecla Escape para dismissão.',
            'https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html',
            nodes
        );
    };

    /**
     * 13. nbr-skip-navigation (WCAG 2.4.1 / NBR 5.6.1)
     * Page must have a skip-to-content link.
     */
    WallyMonitor.NBRRules.checkSkipNavigation = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var skipSelectors = [
            'a[href="#content"]',
            'a[href="#main"]',
            'a[href="#main-content"]',
            'a[href="#conteudo"]',
            'a[href="#conteudo-principal"]',
            '.skip-link',
            '.skip-to-content',
            '.skip-nav',
            'a.screen-reader-text',
            'a.sr-only'
        ];

        var skipLinks = doc.querySelectorAll(skipSelectors.join(','));

        if (skipLinks.length === 0) {
            // Also check first few links
            var firstLinks = doc.querySelectorAll('a');
            var found = false;
            for (var i = 0; i < Math.min(firstLinks.length, 5); i++) {
                var href = firstLinks[i].getAttribute('href') || '';
                var text = (firstLinks[i].textContent || '').toLowerCase();
                if (href.charAt(0) === '#' && (text.indexOf('skip') !== -1 || text.indexOf('pular') !== -1 || text.indexOf('ir para') !== -1 || text.indexOf('ir ao conteúdo') !== -1)) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                // Check if a skip link is injected via JavaScript (e.g. Twenty Twenty-Five theme)
                // Method 1: The scanner sets this flag when the raw HTML contains skip-link code
                var container = doc;
                while (container && container.nodeType === 1) {
                    if (container.getAttribute('data-wally-has-skip-link-script') === 'true') {
                        found = true;
                        break;
                    }
                    container = container.parentNode;
                }
            }

            if (!found) {
                // Method 2: Check if skip-link CSS exists in the document's stylesheets
                // Twenty Twenty-Five and similar themes include .skip-link CSS even when
                // the actual <a> is created via JS. The presence of this CSS is strong
                // evidence that a skip link mechanism exists.
                try {
                    var ownerDoc = doc.ownerDocument || doc;
                    // Check for <style> with skip-link id (WordPress block template pattern)
                    var skipStyle = ownerDoc.getElementById('wp-block-template-skip-link-inline-css');
                    if (skipStyle) {
                        found = true;
                    }
                    // Also check all inline styles for .skip-link rules
                    if (!found) {
                        var allStyles = ownerDoc.querySelectorAll('style');
                        for (var s = 0; s < allStyles.length; s++) {
                            var cssText = allStyles[s].textContent || '';
                            if (/\.skip-link[.\s{:,]/.test(cssText)) {
                                found = true;
                                break;
                            }
                        }
                    }
                } catch (e) { /* cross-origin or access error, ignore */ }
            }

            if (!found) {
                return violation(
                    'nbr-skip-navigation',
                    'serious',
                    ['wcag2a', 'nbr17225-5.6.1', 'wcag241'],
                    'Um mecanismo deve estar disponível para ignorar blocos de conteúdo que se repetem em múltiplas páginas.',
                    'Forneça um link "Pular para o conteúdo" como o primeiro elemento focável na página.',
                    'https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html',
                    [{
                        target: ['body'],
                        html: '<body>',
                        impact: 'serious',
                        message: 'Nenhum link de pular navegação foi encontrado. Adicione um <a href="#main-content"> como o primeiro elemento focável.'
                    }]
                );
            }
        }

        return null;
    };

    /**
     * 14. nbr-focus-order (WCAG 2.4.3 / NBR 5.6.3)
     * Elements with tabindex > 0 disrupt natural focus order.
     */
    WallyMonitor.NBRRules.checkFocusOrder = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var all = doc.querySelectorAll('[tabindex]');

        for (var i = 0; i < all.length; i++) {
            var val = parseInt(all[i].getAttribute('tabindex'), 10);
            if (!isNaN(val) && val > 0) {
                nodes.push(makeNode(
                    all[i],
                    'serious',
                    'Este elemento possui tabindex="' + val + '". Valores positivos de tabindex interrompem a ordem natural de foco. Use tabindex="0" ou tabindex="-1" em vez disso (WCAG 2.4.3).'
                ));
            }
        }

        return violation(
            'nbr-focus-order',
            'serious',
            ['wcag2a', 'nbr17225-5.6.3', 'wcag243'],
            'A ordem de foco deve ser lógica e intuitiva.',
            'Evite usar valores positivos de tabindex; use a ordem do DOM ou tabindex="0" para definir a ordem de foco.',
            'https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html',
            nodes
        );
    };

    /**
     * 15. nbr-focus-visible (WCAG 2.4.7 / NBR 5.6.7)
     * Interactive elements must have visible focus styles.
     */
    WallyMonitor.NBRRules.checkFocusVisible = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Scan for global *, *:focus, a:focus, etc. rules that suppress outline
        try {
            var sheets = doc.styleSheets;
            for (var s = 0; s < sheets.length; s++) {
                var rules;
                try { rules = sheets[s].cssRules || sheets[s].rules; } catch (e) { continue; }
                if (!rules) { continue; }

                for (var r = 0; r < rules.length; r++) {
                    var rule = rules[r];
                    if (!rule.selectorText) { continue; }

                    var sel = rule.selectorText;
                    // Target global focus resets
                    if (/(\*|a|button|input|select|textarea):focus\b/.test(sel) || /^:focus\b/.test(sel)) {
                        var css = rule.cssText || '';
                        if (/outline\s*:\s*(none|0)\b/.test(css) && !/outline-offset|box-shadow|border/.test(css)) {
                            nodes.push({
                                target: [sel],
                                html: '<style>' + sel + ' { ' + (rule.style ? rule.style.cssText : '') + ' }</style>',
                                impact: 'serious',
                                message: 'A regra CSS "' + sel + '" remove os contornos de foco sem fornecer um indicador alternativo. O foco deve ser visível (WCAG 2.4.7).'
                            });
                        }
                    }
                }
            }
        } catch (e) { /* cross-origin */ }

        return violation(
            'nbr-focus-visible',
            'serious',
            ['wcag2aa', 'nbr17225-5.6.7', 'wcag247'],
            'Qualquer interface operável por teclado deve ter um indicador de foco visível.',
            'Não remova os contornos de foco a menos que forneça uma alternativa claramente visível.',
            'https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html',
            nodes
        );
    };

    /**
     * 16. nbr-pointer-cancellation (WCAG 2.5.2 / NBR 5.7.2)
     * Activation should occur on the up-event, not on mousedown alone.
     */
    WallyMonitor.NBRRules.checkPointerCancellation = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var allElements = doc.querySelectorAll('[onmousedown]');

        for (var i = 0; i < allElements.length; i++) {
            var el = allElements[i];
            var hasMouseUp = el.getAttribute('onmouseup') || el.getAttribute('onclick');
            if (!hasMouseUp) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este elemento usa onmousedown para ativação sem um onclick ou onmouseup correspondente. Os usuários não podem cancelar a ação movendo o ponteiro para fora (WCAG 2.5.2).'
                ));
            }
        }

        return violation(
            'nbr-pointer-cancellation',
            'serious',
            ['wcag2a', 'nbr17225-5.7.2', 'wcag252'],
            'Para funcionalidades operadas por ponteiro único, pelo menos um dos seguintes deve ser verdadeiro: o evento de pressionar não é usado, ou o evento de soltar reverte a ação, ou a ação pode ser abortada/desfeita.',
            'Use eventos click ou mouseup em vez de mousedown para ativação de elementos.',
            'https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 4 – Visual & Timing (NBR 5.3.x, 5.4.x)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 17. nbr-audio-autoplay (WCAG 1.4.2 / NBR 5.4.2)
     * Audio/video with autoplay must be muted or have controls.
     */
    WallyMonitor.NBRRules.checkAudioAutoplay = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var media = doc.querySelectorAll('audio[autoplay], video[autoplay]');

        for (var i = 0; i < media.length; i++) {
            var el = media[i];
            var isMuted = el.hasAttribute('muted');
            var hasControls = el.hasAttribute('controls');

            if (!isMuted) {
                nodes.push(makeNode(
                    el,
                    'critical',
                    'Este elemento ' + el.tagName.toLowerCase() + ' reproduz automaticamente sem estar mudo. Áudio em reprodução automática pode desorientar usuários e interferir com leitores de tela (WCAG 1.4.2).' + (!hasControls ? ' Também não possui controles de reprodução.' : '')
                ));
            }
        }

        return violation(
            'nbr-audio-autoplay',
            'critical',
            ['wcag2a', 'nbr17225-5.4.2', 'wcag142'],
            'Se o áudio for reproduzido automaticamente por mais de 3 segundos, forneça um mecanismo para pausar/parar ou controlar o volume.',
            'Adicione o atributo "muted" à mídia com reprodução automática, ou forneça controles visíveis.',
            'https://www.w3.org/WAI/WCAG22/Understanding/audio-control.html',
            nodes
        );
    };

    /**
     * 18. nbr-text-resize (WCAG 1.4.4 / NBR 5.4.4)
     * Font sizes in px may prevent text resizing. Prefer rem/em.
     */
    WallyMonitor.NBRRules.checkTextResize = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var textElements = doc.querySelectorAll('p, span, a, li, td, th, h1, h2, h3, h4, h5, h6, label, button, input, select, textarea, div');
        var checked = 0;

        for (var i = 0; i < textElements.length && checked < 50; i++) {
            var cs = getComputed(textElements[i]);
            if (!cs) { continue; }

            // Check inline style for px font-size
            var inlineStyle = textElements[i].getAttribute('style') || '';
            if (/font-size\s*:\s*\d+px/i.test(inlineStyle)) {
                checked++;
                nodes.push(makeNode(
                    textElements[i],
                    'moderate',
                    'Este elemento possui font-size definido em px via estilo inline. Use unidades relativas (rem, em, %) para permitir o redimensionamento do texto (WCAG 1.4.4).'
                ));
            }
        }

        return violation(
            'nbr-text-resize',
            'moderate',
            ['wcag2aa', 'nbr17225-5.4.4', 'wcag144'],
            'O texto deve ser redimensionável até 200% sem perda de conteúdo ou funcionalidade.',
            'Use unidades relativas (rem, em, %) em vez de px para tamanhos de fonte.',
            'https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html',
            nodes
        );
    };

    /**
     * 19. nbr-images-of-text (WCAG 1.4.5 / NBR 5.4.5)
     * Images whose filenames suggest they contain text.
     */
    WallyMonitor.NBRRules.checkImagesOfText = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        // Stricter pattern: filename must clearly indicate text content
        var textImagePattern = /^(text[-_]|texto[-_]|cta[-_]|slogan[-_]|headline[-_]|promo[-_]text|banner[-_]text|price[-_]tag|preço|oferta[-_]text)/i;
        // Also flag images with very explicit text indicators
        var strongTextPattern = /\b(image[-_]?of[-_]?text|texto[-_]?em[-_]?imagem|screenshot[-_]?text|text[-_]?image)\b/i;
        var images = doc.querySelectorAll('img[src]');

        for (var i = 0; i < images.length; i++) {
            var src = images[i].getAttribute('src') || '';
            var filename = src.split('/').pop().split('?')[0].toLowerCase();

            // Skip SVGs and tiny images (icons)
            if (/\.svg$/i.test(filename)) { continue; }
            var alt = images[i].getAttribute('alt') || '';

            if (textImagePattern.test(filename) || strongTextPattern.test(filename)) {
                nodes.push(makeNode(
                    images[i],
                    'moderate',
                    'O nome do arquivo da imagem "' + filename + '" sugere que ela pode conter texto. Use texto real estilizado com CSS em vez de imagens de texto (WCAG 1.4.5).'
                ));
            }
        }

        return violation(
            'nbr-images-of-text',
            'moderate',
            ['wcag2aa', 'nbr17225-5.4.5', 'wcag145'],
            'Texto deve ser usado em vez de imagens de texto, a menos que seja essencial.',
            'Substitua imagens que contêm texto por texto HTML estilizado.',
            'https://www.w3.org/WAI/WCAG22/Understanding/images-of-text.html',
            nodes
        );
    };

    /**
     * 20. nbr-reflow (WCAG 1.4.10 / NBR 5.4.10)
     * Elements with overflow:hidden + fixed width may clip content at narrow viewports.
     */
    WallyMonitor.NBRRules.checkReflow = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var all = doc.querySelectorAll('div, section, article, main, aside, nav, header, footer, form, table');

        for (var i = 0; i < all.length && i < 100; i++) {
            var cs = getComputed(all[i]);
            if (!cs) { continue; }

            var overflow = cs.getPropertyValue('overflow');
            var overflowX = cs.getPropertyValue('overflow-x');
            var width = cs.getPropertyValue('width');

            var isOverflowHidden = (overflow === 'hidden' || overflowX === 'hidden');
            var hasFixedWidth = width && /^\d+px$/.test(width) && parseInt(width, 10) > 320;

            if (isOverflowHidden && hasFixedWidth) {
                // Skip carousels/sliders (intentional overflow:hidden)
                var cls = (all[i].className || '').toLowerCase();
                if (/\b(carousel|slider|swiper|slick|splide|glide|owl)\b/.test(cls)) { continue; }
                // Skip if parent has overflow-x: auto/scroll (scrollable wrapper)
                var parentCs = getComputed(all[i].parentNode);
                if (parentCs) {
                    var parentOvX = parentCs.getPropertyValue('overflow-x');
                    if (parentOvX === 'auto' || parentOvX === 'scroll') { continue; }
                }
                // Skip if element also has max-width set (responsive)
                var maxW = cs.getPropertyValue('max-width');
                if (maxW && maxW !== 'none' && /^\d+/.test(maxW) && parseInt(maxW, 10) <= 320) { continue; }
                nodes.push(makeNode(
                    all[i],
                    'serious',
                    'Este elemento possui overflow:hidden com largura fixa de ' + width + '. O conteúdo pode ser cortado quando o viewport é de 320px de largura (WCAG 1.4.10).'
                ));
            }
        }

        return violation(
            'nbr-reflow',
            'serious',
            ['wcag2aa', 'nbr17225-5.4.10', 'wcag1410'],
            'O conteúdo deve se reorganizar em uma única coluna em 320px de largura sem rolagem horizontal.',
            'Evite combinar overflow:hidden com larguras fixas. Use layouts responsivos.',
            'https://www.w3.org/WAI/WCAG22/Understanding/reflow.html',
            nodes
        );
    };

    /**
     * 21. nbr-text-spacing (WCAG 1.4.12 / NBR 5.4.12)
     * Text must tolerate custom spacing overrides.
     */
    WallyMonitor.NBRRules.checkTextSpacing = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var textElements = doc.querySelectorAll('p, span, a, li, td, th, h1, h2, h3, h4, h5, h6, label, div, blockquote');

        for (var i = 0; i < textElements.length; i++) {
            if (nodes.length >= 80) { break; }
            var el = textElements[i];
            var cs = getComputed(el);
            if (!cs) { continue; }

            // For div elements, only check if they have direct text node content.
            // Layout wrapper divs (e.g. .wp-site-blocks, .wp-block-group) inherit
            // textContent from children but have no text of their own — flagging
            // them inflates the count with non-text elements.
            if (el.tagName === 'DIV') {
                var hasDirectText = false;
                for (var c = 0; c < el.childNodes.length; c++) {
                    if (el.childNodes[c].nodeType === 3 && el.childNodes[c].textContent.trim().length > 0) {
                        hasDirectText = true;
                        break;
                    }
                }
                if (!hasDirectText) { continue; }
            }

            // Skip inline elements (<a>, <span>) whose parent is also a text
            // element in our check list. They inherit spacing from the parent,
            // so flagging both parent AND child is redundant double-counting.
            var tag = el.tagName;
            if (tag === 'A' || tag === 'SPAN') {
                var parent = el.parentNode;
                if (parent && /^(P|LI|TD|TH|H[1-6]|LABEL|DIV|BLOCKQUOTE|A)$/i.test(parent.tagName)) {
                    continue;
                }
            }

            // Only check elements with visible text
            var text = (el.textContent || '').trim();
            if (text.length < 5) { continue; }

            var lineHeight = cs.getPropertyValue('line-height');
            var fontSize = parseFloat(cs.getPropertyValue('font-size'));
            var letterSpacing = cs.getPropertyValue('letter-spacing');
            var wordSpacing = cs.getPropertyValue('word-spacing');

            var issues = [];

            // Check line-height < 1.5 × font-size
            if (lineHeight !== 'normal') {
                var lhValue = parseFloat(lineHeight);
                if (!isNaN(lhValue) && !isNaN(fontSize) && fontSize > 0 && lhValue < fontSize * 1.5) {
                    issues.push('line-height (' + lineHeight + ') é menor que 1.5× font-size (' + fontSize + 'px)');
                }
            }

            // Check negative letter-spacing
            if (letterSpacing && letterSpacing !== 'normal') {
                var ls = parseFloat(letterSpacing);
                if (!isNaN(ls) && ls < 0) {
                    issues.push('letter-spacing é negativo (' + letterSpacing + ')');
                }
            }

            // Check negative word-spacing
            if (wordSpacing && wordSpacing !== 'normal') {
                var ws = parseFloat(wordSpacing);
                if (!isNaN(ws) && ws < 0) {
                    issues.push('word-spacing é negativo (' + wordSpacing + ')');
                }
            }

            if (issues.length > 0) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Problema de espaçamento de texto: ' + issues.join('; ') + '. O conteúdo não deve ser cortado quando o espaçamento de texto é sobrescrito (WCAG 1.4.12).'
                ));
            }
        }

        return violation(
            'nbr-text-spacing',
            'moderate',
            ['wcag2aa', 'nbr17225-5.4.12', 'wcag1412'],
            'Não deve haver perda de conteúdo ou funcionalidade quando o usuário sobrescreve o espaçamento do texto.',
            'Garanta line-height ≥ 1.5, evite letter/word-spacing negativos e teste com sobrescrita de espaçamento.',
            'https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html',
            nodes
        );
    };

    /**
     * 22. nbr-content-on-hover (WCAG 1.4.13 / NBR 5.4.13)
     * Tooltip-like content triggered on hover must be dismissible and hoverable.
     */
    WallyMonitor.NBRRules.checkContentOnHover = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        // Check for title attributes on non-link, non-image elements (custom tooltips)
        var titled = doc.querySelectorAll('[title]:not(a):not(img):not(iframe):not(abbr)');

        for (var i = 0; i < titled.length; i++) {
            var title = (titled[i].getAttribute('title') || '').trim();
            if (title.length < 3) { continue; }

            nodes.push(makeNode(
                titled[i],
                'moderate',
                'Este elemento usa o atributo title ("' + title.substring(0, 60) + '…") para conteúdo de tooltip. Tooltips nativos do title não podem ser dispensados ou mantidos em hover — use tooltips ARIA ou popups personalizados (WCAG 1.4.13).'
            ));
        }

        // Check for CSS-only tooltips (common pattern: [data-tooltip]:hover::after)
        var tooltipElements = doc.querySelectorAll('[data-tooltip], [data-tip], [data-original-title]');
        for (var j = 0; j < tooltipElements.length; j++) {
            var el = tooltipElements[j];
            if (!el.getAttribute('aria-describedby') && el.getAttribute('role') !== 'tooltip') {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este elemento parece usar um tooltip CSS/data-attribute. Garanta que o tooltip seja dispensável (tecla Escape), hoverável e persistente (WCAG 1.4.13).'
                ));
            }
        }

        return violation(
            'nbr-content-on-hover',
            'moderate',
            ['wcag2aa', 'nbr17225-5.4.13', 'wcag1413'],
            'Conteúdo adicional que aparece ao passar o mouse/foco deve ser dispensável, hoverável e persistente.',
            'Substitua tooltips de atributo title por tooltips ARIA que sejam dispensáveis e hoveráveis.',
            'https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html',
            nodes
        );
    };

    /**
     * 23. nbr-timing-adjustable (WCAG 2.2.1 / NBR 5.5.4)
     * <meta http-equiv="refresh"> with timeout should be avoided.
     */
    WallyMonitor.NBRRules.checkTimingAdjustable = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var metaRefresh = doc.querySelectorAll('meta[http-equiv="refresh"]');

        for (var i = 0; i < metaRefresh.length; i++) {
            var content = metaRefresh[i].getAttribute('content') || '';
            var seconds = parseInt(content, 10);

            if (!isNaN(seconds) && seconds > 0) {
                nodes.push(makeNode(
                    metaRefresh[i],
                    'critical',
                    'A página é atualizada automaticamente após ' + seconds + ' segundo(s) via <meta http-equiv="refresh">. Limites de tempo devem ser ajustáveis, desativáveis ou extensíveis (WCAG 2.2.1).'
                ));
            }
        }

        return violation(
            'nbr-timing-adjustable',
            'critical',
            ['wcag2a', 'nbr17225-5.5.4', 'wcag221'],
            'Para cada limite de tempo definido pelo conteúdo, o usuário deve poder desativar, ajustar ou estender o tempo.',
            'Remova <meta http-equiv="refresh"> ou forneça controles para o usuário estender/desativar o timeout.',
            'https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html',
            nodes
        );
    };

    /**
     * 24. nbr-pause-stop (WCAG 2.2.2 / NBR 5.5.5)
     * Auto-moving/blinking content must be pausable.
     */
    WallyMonitor.NBRRules.checkPauseStop = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Check for <marquee> elements
        var marquees = doc.querySelectorAll('marquee, .marquee');
        for (var i = 0; i < marquees.length; i++) {
            nodes.push(makeNode(
                marquees[i],
                'critical',
                'O elemento <marquee> cria conteúdo em movimento automático que não pode ser pausado. Use animações CSS com controles ou conteúdo estático (WCAG 2.2.2).'
            ));
        }

        // Check for common carousel/slider libraries without pause controls
        var carouselSelectors = [
            '.slick-slider',
            '.swiper',
            '.swiper-container',
            '.owl-carousel',
            '.carousel',
            '.flickity-slider',
            '[data-ride="carousel"]',
            '.glide',
            '.splide'
        ];
        var carousels = doc.querySelectorAll(carouselSelectors.join(','));

        for (var c = 0; c < carousels.length; c++) {
            var el = carousels[c];
            // Check if it auto-plays — look for data attributes
            var autoplay = el.getAttribute('data-autoplay') ||
                           el.getAttribute('data-slick') ||
                           el.getAttribute('data-ride');

            // Check for pause button
            var pauseBtn = el.querySelector('[aria-label*="pause"], [aria-label*="pausar"], .pause, .stop, button[data-action="stop"]');
            if (!pauseBtn) {
                // Also check nearby siblings
                var parent = el.parentNode;
                if (parent) {
                    pauseBtn = parent.querySelector('[aria-label*="pause"], [aria-label*="pausar"], .pause, .stop');
                }
            }

            if (!pauseBtn) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este carrossel/slider com reprodução automática não possui um botão de pausa aparente. Forneça um mecanismo para pausar, parar ou ocultar conteúdo em movimento automático (WCAG 2.2.2).'
                ));
            }
        }

        // Check for CSS animations that don't respect prefers-reduced-motion
        try {
            var sheets = doc.styleSheets;
            var hasReducedMotionQuery = false;

            for (var s = 0; s < sheets.length; s++) {
                var rules;
                try { rules = sheets[s].cssRules || sheets[s].rules; } catch (e) { continue; }
                if (!rules) { continue; }

                for (var r = 0; r < rules.length; r++) {
                    if (rules[r].type === CSSRule.MEDIA_RULE) {
                        var media = rules[r].conditionText || rules[r].media.mediaText || '';
                        if (media.indexOf('prefers-reduced-motion') !== -1) {
                            hasReducedMotionQuery = true;
                            break;
                        }
                    }
                }
                if (hasReducedMotionQuery) { break; }
            }

            if (!hasReducedMotionQuery && (carousels.length > 0 || marquees.length > 0)) {
                nodes.push({
                    target: ['html'],
                    html: '<html>',
                    impact: 'moderate',
                    message: 'Nenhuma consulta @media (prefers-reduced-motion) foi detectada. Conteúdo animado deve respeitar as preferências de movimento do usuário.'
                });
            }
        } catch (e) { /* cross-origin */ }

        return violation(
            'nbr-pause-stop',
            'serious',
            ['wcag2a', 'nbr17225-5.5.5', 'wcag222'],
            'Conteúdo em movimento, piscante, com rolagem ou atualização automática deve poder ser pausado, parado ou ocultado.',
            'Adicione controles de pausar/parar para carrosséis e conteúdo em movimento automático, e respeite prefers-reduced-motion.',
            'https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 5 – Semantics & Structure (NBR 5.3.x, 5.6.x)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 25. nbr-meaningful-sequence (WCAG 1.3.2 / NBR 5.3.2)
     * Visual order must match DOM order (flex/grid order property, floats).
     */
    WallyMonitor.NBRRules.checkMeaningfulSequence = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        // Targeted: find flex/grid containers, then check their direct children for CSS order
        var containers = doc.querySelectorAll('[style*="display"], [class*="flex"], [class*="grid"], [class*="row"], [class*="col"], [class*="order-"], .d-flex, .d-grid, .flex, .grid, main, section, article, aside, nav, header, footer, div');
        var flexGridContainers = [];
        for (var c = 0; c < containers.length; c++) {
            var cCs = getComputed(containers[c]);
            if (!cCs) { continue; }
            var disp = cCs.getPropertyValue('display');
            if (disp === 'flex' || disp === 'inline-flex' || disp === 'grid' || disp === 'inline-grid') {
                flexGridContainers.push(containers[c]);
            }
        }

        // Only check direct children of flex/grid containers
        for (var fg = 0; fg < flexGridContainers.length; fg++) {
            var children = flexGridContainers[fg].children;
            for (var i = 0; i < children.length; i++) {
                var cs = getComputed(children[i]);
                if (!cs) { continue; }

                var order = cs.getPropertyValue('order');
                var orderNum = parseInt(order, 10);
                // Only flag significant reordering (order > 1 or negative), skip minor CSS order: 1
                if (!isNaN(orderNum) && orderNum !== 0 && (orderNum > 1 || orderNum < 0)) {
                    // Skip elements that are decorative or purely visual
                    var role = children[i].getAttribute('role');
                    if (role === 'presentation' || role === 'none') { continue; }
                    // Skip images and icons (decorative reordering)
                    if (children[i].tagName === 'IMG' || children[i].tagName === 'SVG') { continue; }
                    nodes.push(makeNode(
                        children[i],
                        'moderate',
                        'Este elemento usa CSS order: ' + order + ' dentro de um container flex/grid. A reordenação visual pode diferir da ordem do DOM, confundindo usuários de teclado e leitores de tela (WCAG 1.3.2).'
                    ));
                }
            }
        }

        return violation(
            'nbr-meaningful-sequence',
            'moderate',
            ['wcag2a', 'nbr17225-5.3.2', 'wcag132'],
            'Quando a sequência em que o conteúdo é apresentado afeta seu significado, uma sequência de leitura correta deve ser programaticamente determinável.',
            'Evite a propriedade CSS order para reordenação de conteúdo significativo. Garanta que a ordem do DOM corresponda à ordem visual.',
            'https://www.w3.org/WAI/WCAG22/Understanding/meaningful-sequence.html',
            nodes
        );
    };

    /**
     * 26. nbr-sensory-characteristics (WCAG 1.3.3 / NBR 5.3.3)
     * Instructions must not rely solely on sensory characteristics.
     */
    WallyMonitor.NBRRules.checkSensoryCharacteristics = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var sensoryPatterns = [
            // Portuguese
            /clique\s+(n[oa]s?\s+)?(bot[aã]o|link|ícone|imagem)\s+([àa]\s+)?(direita|esquerda|acima|abaixo)/i,
            /veja\s+(acima|abaixo|à\s+direita|à\s+esquerda)/i,
            /(o|a|os|as)\s+(campo|botão|link|seção|item)\s+(vermelho|verde|azul|amarelo|redondo|quadrado|grande|pequeno)/i,
            /item\s+(circular|redondo|quadrado|triangular)/i,
            // English
            /click\s+the\s+(red|green|blue|yellow|round|square|left|right)\s+(button|link|icon|image)/i,
            /see\s+the\s+(image|section|box)\s+on\s+the\s+(left|right)/i,
            /the\s+(red|green|blue|yellow|round|large|small)\s+(field|button|box|section|area)/i,
            /look\s+(above|below|to\s+the\s+(left|right))/i
        ];

        // Only check direct instructional elements, not generic divs (avoid duplicate parent/child matches)
        var textElements = doc.querySelectorAll('p, li, td, label, figcaption, dt, dd');

        for (var i = 0; i < textElements.length && i < 150; i++) {
            // Skip code blocks, technical content, and very short text
            if (textElements[i].closest('pre, code, .code-block, .wp-block-code, blockquote')) { continue; }
            var text = (textElements[i].textContent || '').trim();
            if (text.length < 20 || text.length > 500) { continue; }
            // Only match if the element's OWN direct text (not child elements) contains the pattern
            var directText = '';
            for (var c = 0; c < textElements[i].childNodes.length; c++) {
                if (textElements[i].childNodes[c].nodeType === 3) { directText += textElements[i].childNodes[c].textContent; }
            }
            if (directText.trim().length < 15) { continue; }

            for (var p = 0; p < sensoryPatterns.length; p++) {
                if (sensoryPatterns[p].test(directText)) {
                    nodes.push(makeNode(
                        textElements[i],
                        'moderate',
                        'Este texto depende de características sensoriais (forma, tamanho, localização visual, cor) para transmitir instruções. Forneça contexto adicional além de pistas sensoriais (WCAG 1.3.3).'
                    ));
                    break; // Only flag once per element
                }
            }
        }

        return violation(
            'nbr-sensory-characteristics',
            'moderate',
            ['wcag2a', 'nbr17225-5.3.3', 'wcag133'],
            'Instruções não devem depender exclusivamente de características sensoriais como forma, cor, tamanho ou localização visual.',
            'Complemente referências visuais/espaciais com rótulos de texto ou outras descrições não sensoriais.',
            'https://www.w3.org/WAI/WCAG22/Understanding/sensory-characteristics.html',
            nodes
        );
    };

    /**
     * 27. nbr-orientation (WCAG 1.3.4 / NBR 5.3.4)
     * Content must not restrict display orientation.
     */
    WallyMonitor.NBRRules.checkOrientation = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Check viewport meta for orientation restrictions
        var viewportMeta = doc.querySelector('meta[name="viewport"]');
        if (viewportMeta) {
            var content = (viewportMeta.getAttribute('content') || '').toLowerCase();
            // Some sites still try to lock orientation via viewport tricks or use non-standard attributes
            // But the real check is CSS @media orientation locks
        }

        // Check stylesheets for forced orientation
        try {
            var sheets = doc.styleSheets;
            for (var s = 0; s < sheets.length; s++) {
                var rules;
                try { rules = sheets[s].cssRules || sheets[s].rules; } catch (e) { continue; }
                if (!rules) { continue; }

                for (var r = 0; r < rules.length; r++) {
                    if (rules[r].type === CSSRule.MEDIA_RULE) {
                        var media = rules[r].conditionText || rules[r].media.mediaText || '';
                        if (/orientation\s*:\s*(portrait|landscape)/.test(media)) {
                            // Check if it contains display:none or transform:rotate that forces orientation
                            var innerRules = rules[r].cssRules || [];
                            for (var ir = 0; ir < innerRules.length; ir++) {
                                var css = innerRules[ir].cssText || '';
                                var sel = (innerRules[ir].selectorText || '').toLowerCase();
                                if (/transform\s*:\s*rotate/.test(css)) {
                                    // Rotation to force orientation is always a violation
                                    nodes.push({
                                        target: [innerRules[ir].selectorText || '@media ' + media],
                                        html: '<style>@media (' + media + ') { ... transform:rotate ... }</style>',
                                        impact: 'serious',
                                        message: 'Consulta de mídia CSS usa transform:rotate para forçar uma orientação. O conteúdo deve ser visualizável tanto em retrato quanto em paisagem (WCAG 1.3.4).'
                                    });
                                } else if (/display\s*:\s*none/.test(css)) {
                                    // Only flag display:none if it targets a major layout container
                                    var isCritical = /^(html|body|main|#page|#content|#wrapper|#app|#root|\[role="main"\])/.test(sel);
                                    if (isCritical) {
                                        nodes.push({
                                            target: [innerRules[ir].selectorText || '@media ' + media],
                                            html: '<style>@media (' + media + ') { ' + sel + ' { display:none } }</style>',
                                            impact: 'serious',
                                            message: 'O CSS oculta o container principal de conteúdo em uma orientação. O conteúdo deve ser visualizável tanto em retrato quanto em paisagem (WCAG 1.3.4).'
                                        });
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (e) { /* cross-origin */ }

        return violation(
            'nbr-orientation',
            'serious',
            ['wcag2aa', 'nbr17225-5.3.4', 'wcag134'],
            'O conteúdo não deve restringir sua visualização e operação a uma única orientação de tela.',
            'Garanta que o conteúdo funcione tanto na orientação retrato quanto paisagem.',
            'https://www.w3.org/WAI/WCAG22/Understanding/orientation.html',
            nodes
        );
    };

    /**
     * 28. nbr-identify-input-purpose (WCAG 1.3.5 / NBR 5.3.5)
     * Form inputs should have appropriate autocomplete attribute.
     */
    WallyMonitor.NBRRules.checkIdentifyInputPurpose = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var inputMappings = {
            'email': ['email'],
            'e-mail': ['email'],
            'nome': ['name', 'given-name', 'family-name'],
            'name': ['name', 'given-name', 'family-name'],
            'first.?name': ['given-name'],
            'last.?name': ['family-name'],
            'sobrenome': ['family-name'],
            'telefone': ['tel'],
            'phone': ['tel'],
            'tel': ['tel'],
            'celular': ['tel'],
            'mobile': ['tel'],
            'endereco|endereço': ['street-address', 'address-line1'],
            'address': ['street-address', 'address-line1'],
            'cep': ['postal-code'],
            'zip': ['postal-code'],
            'postal': ['postal-code'],
            'cidade': ['address-level2'],
            'city': ['address-level2'],
            'estado': ['address-level1'],
            'state': ['address-level1'],
            'pais|país': ['country', 'country-name'],
            'country': ['country', 'country-name'],
            'nascimento|birth': ['bday'],
            'cpf': ['off'], // CPF should have autocomplete but off is acceptable
            'username|usuario|usuário': ['username'],
            'senha|password': ['current-password', 'new-password'],
            'cc.?num|card.?number|cartao|cartão': ['cc-number'],
            'organization|empresa|organiza': ['organization']
        };

        var inputs = doc.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="image"]), select, textarea');

        for (var i = 0; i < inputs.length; i++) {
            var el = inputs[i];
            if (el.getAttribute('autocomplete')) { continue; }

            var name = (el.getAttribute('name') || '').toLowerCase();
            var id = (el.getAttribute('id') || '').toLowerCase();
            var placeholder = (el.getAttribute('placeholder') || '').toLowerCase();
            var type = (el.getAttribute('type') || '').toLowerCase();
            var label = '';

            // Try to find associated label
            var labelEl = el.closest('label') || (el.id ? doc.querySelector('label[for="' + el.id + '"]') : null);
            if (labelEl) { label = (labelEl.textContent || '').toLowerCase(); }

            var combined = name + ' ' + id + ' ' + placeholder + ' ' + label + ' ' + type;

            for (var pattern in inputMappings) {
                if (inputMappings.hasOwnProperty(pattern)) {
                    var regex = new RegExp(pattern, 'i');
                    if (regex.test(combined)) {
                        nodes.push(makeNode(
                            el,
                            'moderate',
                            'Este campo de entrada parece coletar dados de "' + pattern.replace(/\|/g, '/') + '" mas não possui atributo autocomplete. Valor(es) sugerido(s): ' + inputMappings[pattern].join(', ') + ' (WCAG 1.3.5).'
                        ));
                        break;
                    }
                }
            }
        }

        return violation(
            'nbr-identify-input-purpose',
            'moderate',
            ['wcag2aa', 'nbr17225-5.3.5', 'wcag135'],
            'O propósito dos campos de entrada que coletam informações do usuário deve ser programaticamente determinável.',
            'Adicione o atributo autocomplete com o valor apropriado aos campos de formulário.',
            'https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html',
            nodes
        );
    };

    /**
     * 29. nbr-page-titled (WCAG 2.4.2 / NBR 5.6.2)
     * Page title must be descriptive, not generic.
     */
    WallyMonitor.NBRRules.checkPageTitled = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }
        // Skip title check when doc is an Element (div fallback), not a Document
        if (!doc.documentElement && doc.nodeType === 1) { return null; }

        var nodes = [];
        var title = doc.title ? doc.title.trim() : '';
        var genericTitles = /^(home|page|untitled|início|inicio|index|document|sem título|sem titulo|welcome|bemvindo|bem-vindo|new page|nova página|nova pagina|test|teste|\s*)$/i;

        if (!title) {
            nodes.push({
                target: ['head > title'],
                html: '<title></title>',
                impact: 'critical',
                message: 'A página não possui elemento <title> ou ele está vazio. Forneça um título de página descritivo (WCAG 2.4.2).'
            });
        } else if (genericTitles.test(title)) {
            nodes.push({
                target: ['head > title'],
                html: '<title>' + title + '</title>',
                impact: 'serious',
                message: 'O título da página "' + title + '" é genérico e não descreve o propósito da página. Use um título descritivo (WCAG 2.4.2).'
            });
        }

        return violation(
            'nbr-page-titled',
            'serious',
            ['wcag2a', 'nbr17225-5.6.2', 'wcag242'],
            'Páginas web devem ter títulos que descrevam seu tópico ou propósito.',
            'Forneça um <title> único e descritivo para cada página.',
            'https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html',
            nodes
        );
    };

    /**
     * 30. nbr-link-purpose (WCAG 2.4.4 / NBR 5.6.4)
     * Links must have descriptive text, not generic "click here".
     */
    WallyMonitor.NBRRules.checkLinkPurpose = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var genericLinkText = /^(clique aqui|clique|saiba mais|leia mais|veja mais|mais|aqui|link|click here|click|read more|learn more|more|here|see more|details|detalhes|ver|ver mais|acesse|acessar|confira|veja)\.?$/i;

        var links = doc.querySelectorAll('a[href]');

        for (var i = 0; i < links.length; i++) {
            var text = (links[i].textContent || '').trim();
            var ariaLabel = links[i].getAttribute('aria-label');
            var ariaLabelledBy = links[i].getAttribute('aria-labelledby');

            // If aria-label or aria-labelledby provides context, skip
            if (ariaLabel || ariaLabelledBy) { continue; }

            if (genericLinkText.test(text)) {
                nodes.push(makeNode(
                    links[i],
                    'serious',
                    'O texto do link "' + text + '" é genérico e não descreve o destino do link. Use texto descritivo ou adicione aria-label (WCAG 2.4.4).'
                ));
            }
        }

        return violation(
            'nbr-link-purpose',
            'serious',
            ['wcag2a', 'nbr17225-5.6.4', 'wcag244'],
            'O propósito de cada link deve ser determinável apenas pelo texto do link ou por seu contexto.',
            'Substitua textos de link genéricos ("clique aqui", "saiba mais") por texto descritivo.',
            'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html',
            nodes
        );
    };

    /**
     * 31. nbr-multiple-ways (WCAG 2.4.5 / NBR 5.6.5)
     * Page should have at least 2 navigation mechanisms.
     */
    WallyMonitor.NBRRules.checkMultipleWays = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var mechanisms = 0;

        // Check for navigation menu
        var nav = doc.querySelectorAll('nav, [role="navigation"]');
        if (nav.length > 0) { mechanisms++; }

        // Check for search
        var search = doc.querySelectorAll('input[type="search"], [role="search"], form[action*="search"], form[action*="busca"], input[name*="search"], input[name*="busca"], input[name*="pesquis"], input[placeholder*="search" i], input[placeholder*="busca" i], input[placeholder*="pesquis" i]');
        if (search.length > 0) { mechanisms++; }

        // Check for sitemap link
        var sitemapLinks = doc.querySelectorAll('a[href*="sitemap"], a[href*="mapa-do-site"], a[href*="mapa-site"]');
        if (sitemapLinks.length > 0) { mechanisms++; }

        // Check for breadcrumb
        var breadcrumb = doc.querySelectorAll('[aria-label*="breadcrumb"], [aria-label*="migalha"], .breadcrumb, .breadcrumbs, [role="navigation"][aria-label*="breadcrumb"], nav.breadcrumb, ol.breadcrumb');
        if (breadcrumb.length > 0) { mechanisms++; }

        // Check for table of contents
        var toc = doc.querySelectorAll('.toc, .table-of-contents, #toc, [aria-label*="table of contents"], [aria-label*="índice"], [aria-label*="sumário"]');
        if (toc.length > 0) { mechanisms++; }

        if (mechanisms < 2) {
            return violation(
                'nbr-multiple-ways',
                'moderate',
                ['wcag2aa', 'nbr17225-5.6.5', 'wcag245'],
                'Mais de uma forma deve estar disponível para localizar uma página web dentro de um conjunto de páginas.',
                'Forneça pelo menos dois mecanismos: menu de navegação, busca, mapa do site, breadcrumb ou sumário.',
                'https://www.w3.org/WAI/WCAG22/Understanding/multiple-ways.html',
                [{
                    target: ['body'],
                    html: '<body>',
                    impact: 'moderate',
                    message: 'Apenas ' + mechanisms + ' mecanismo(s) de navegação detectado(s). Forneça pelo menos 2 formas de navegar (ex.: menu de navegação + busca, ou navegação + breadcrumb) (WCAG 2.4.5).'
                }]
            );
        }

        return null;
    };

    /**
     * 32. nbr-section-headings (WCAG 2.4.10 / NBR 5.6.10)
     * Major content sections should have headings.
     */
    WallyMonitor.NBRRules.checkSectionHeadings = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var sections = doc.querySelectorAll('section, article, main, [role="main"], [role="region"], [role="complementary"]');

        for (var i = 0; i < sections.length; i++) {
            var el = sections[i];
            var heading = el.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
            var ariaLabel = el.getAttribute('aria-label');
            var ariaLabelledBy = el.getAttribute('aria-labelledby');

            if (!heading && !ariaLabel && !ariaLabelledBy) {
                var tag = el.tagName.toLowerCase();
                var role = el.getAttribute('role') || '';
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este <' + tag + '>' + (role ? ' (role="' + role + '")' : '') + ' não possui cabeçalho (h1-h6) ou aria-label/aria-labelledby. Cabeçalhos de seção ajudam os usuários a navegar pelo conteúdo (WCAG 2.4.10).'
                ));
            }
        }

        return violation(
            'nbr-section-headings',
            'moderate',
            ['wcag2aaa', 'nbr17225-5.6.10', 'wcag2410'],
            'Cabeçalhos de seção devem ser usados para organizar o conteúdo.',
            'Adicione cabeçalhos (h1-h6) ou aria-label aos elementos <section>, <article> e <main>.',
            'https://www.w3.org/WAI/WCAG22/Understanding/section-headings.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 6 – Error Handling & Forms (NBR 5.9.x)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 33. nbr-error-identification (WCAG 3.3.1 / NBR 5.9.1)
     * Required fields should have error messaging mechanisms.
     */
    WallyMonitor.NBRRules.checkErrorIdentification = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var requiredFields = doc.querySelectorAll('[required], [aria-required="true"]');

        for (var i = 0; i < requiredFields.length; i++) {
            var el = requiredFields[i];
            var ariaDescribedBy = el.getAttribute('aria-describedby');
            var ariaErrorMessage = el.getAttribute('aria-errormessage');
            var ariaInvalid = el.getAttribute('aria-invalid');

            // Check if there's an error messaging mechanism
            var hasErrorMechanism = false;

            if (ariaErrorMessage) {
                var errorEl = doc.getElementById(ariaErrorMessage);
                if (errorEl) { hasErrorMechanism = true; }
            }

            if (ariaDescribedBy) {
                var ids = ariaDescribedBy.split(/\s+/);
                for (var d = 0; d < ids.length; d++) {
                    var descEl = doc.getElementById(ids[d]);
                    if (descEl) {
                        var descText = (descEl.textContent || '').toLowerCase();
                        if (descText.indexOf('erro') !== -1 || descText.indexOf('error') !== -1 || descText.indexOf('obrigatório') !== -1 || descText.indexOf('required') !== -1 || descText.indexOf('inválid') !== -1 || descText.indexOf('invalid') !== -1) {
                            hasErrorMechanism = true;
                            break;
                        }
                    }
                }
            }

            // Also check for a sibling error message element
            if (!hasErrorMechanism && el.parentNode) {
                var siblings = el.parentNode.querySelectorAll('.error, .error-message, .field-error, .invalid-feedback, .form-error, [role="alert"]');
                if (siblings.length > 0) { hasErrorMechanism = true; }
            }

            if (!hasErrorMechanism && !ariaErrorMessage) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este campo obrigatório não possui aria-errormessage ou um container de mensagem de erro visível. Forneça identificação clara de erros quando a validação falhar (WCAG 3.3.1).'
                ));
            }
        }

        return violation(
            'nbr-error-identification',
            'serious',
            ['wcag2a', 'nbr17225-5.9.1', 'wcag331'],
            'Se um erro de entrada for detectado automaticamente, o item com erro deve ser identificado e o erro descrito ao usuário em texto.',
            'Adicione aria-errormessage ou aria-describedby apontando para containers de mensagem de erro nos campos obrigatórios.',
            'https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html',
            nodes
        );
    };

    /**
     * 34. nbr-error-suggestion (WCAG 3.3.3 / NBR 5.9.3)
     * Validated inputs should describe the expected format.
     */
    WallyMonitor.NBRRules.checkErrorSuggestion = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var validatedInputs = doc.querySelectorAll('input[pattern], input[type="email"], input[type="url"], input[type="tel"], input[type="number"], input[min], input[max], input[minlength], input[maxlength]');

        for (var i = 0; i < validatedInputs.length; i++) {
            var el = validatedInputs[i];
            var title = el.getAttribute('title');
            var ariaDescribedBy = el.getAttribute('aria-describedby');
            var placeholder = el.getAttribute('placeholder');

            // Check if there's a description of the expected format
            var hasDescription = false;

            if (title && title.length > 5) { hasDescription = true; }
            if (ariaDescribedBy) {
                var descEl = doc.getElementById(ariaDescribedBy);
                if (descEl && (descEl.textContent || '').trim().length > 5) {
                    hasDescription = true;
                }
            }

            // Placeholder alone is not sufficient but is a partial indicator
            if (!hasDescription) {
                var type = el.getAttribute('type') || 'text';
                var pattern = el.getAttribute('pattern') || '';
                var detail = type !== 'text' ? 'type="' + type + '"' : '';
                if (pattern) { detail += (detail ? ', ' : '') + 'pattern="' + pattern.substring(0, 30) + '"'; }

                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este campo possui restrições de validação (' + detail + ') mas não possui title ou aria-describedby explicando o formato esperado. Forneça sugestões para correção de erros (WCAG 3.3.3).'
                ));
            }
        }

        return violation(
            'nbr-error-suggestion',
            'moderate',
            ['wcag2aa', 'nbr17225-5.9.3', 'wcag333'],
            'Se um erro de entrada for detectado e sugestões de correção forem conhecidas, elas devem ser fornecidas ao usuário.',
            'Adicione title ou aria-describedby aos campos validados descrevendo o formato esperado.',
            'https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html',
            nodes
        );
    };

    /**
     * 35. nbr-status-messages (WCAG 4.1.3 / NBR 5.10.3)
     * Status/alert containers must use ARIA live regions.
     */
    WallyMonitor.NBRRules.checkStatusMessages = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var statusSelectors = [
            '.alert:not([role])',
            '.notice:not([role])',
            '.message:not([role]):not(p)',
            '.toast:not([role])',
            '.notification:not([role])',
            '.flash:not([role])',
            '.flash-message:not([role])',
            '.success:not([role]):not(input)',
            '.error-message:not([role])',
            '.warning:not([role]):not(input)',
            '.info-message:not([role])',
            '.status-message:not([role])',
            '.feedback:not([role])',
            '.snackbar:not([role])'
        ];

        var statusContainers = doc.querySelectorAll(statusSelectors.join(','));

        for (var i = 0; i < statusContainers.length; i++) {
            var el = statusContainers[i];
            var cs = getComputed(el);
            // Skip hidden elements
            if (cs && cs.display === 'none') { continue; }

            var role = el.getAttribute('role');
            var ariaLive = el.getAttribute('aria-live');

            if (!role && !ariaLive) {
                // Verify it looks like a status container (has some text content)
                var text = (el.textContent || '').trim();
                if (text.length < 3) { continue; }

                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este elemento parece ser um container de status/notificação (classe: "' + (el.className || '').substring(0, 50) + '") mas não possui role="alert", role="status" ou aria-live. Leitores de tela não anunciarão atualizações (WCAG 4.1.3).'
                ));
            }
        }

        return violation(
            'nbr-status-messages',
            'serious',
            ['wcag2aa', 'nbr17225-5.10.3', 'wcag413'],
            'Mensagens de status devem ser programaticamente determináveis por meio de role ou propriedades, para que possam ser apresentadas ao usuário por tecnologias assistivas sem receber foco.',
            'Adicione role="alert", role="status" ou aria-live aos containers de notificação/status.',
            'https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  GROUP 7 – Expanded Coverage (v1.2.0)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * 36. nbr-video-audiodesc (WCAG 1.2.5 / NBR 5.2.5)
     * Videos must provide audio description for visual-only content.
     */
    WallyMonitor.NBRRules.checkVideoAudioDesc = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var videos = doc.querySelectorAll('video');

        for (var i = 0; i < videos.length; i++) {
            var video = videos[i];
            var tracks = video.querySelectorAll('track[kind="descriptions"]');
            if (tracks.length === 0) {
                nodes.push(makeNode(
                    video,
                    'moderate',
                    'Este <video> não possui faixa de audiodescrição (<track kind="descriptions">). Usuários que não podem ver o conteúdo do vídeo perderão informações exclusivamente visuais (WCAG 1.2.5).'
                ));
            }
        }

        return violation(
            'nbr-video-audiodesc',
            'moderate',
            ['wcag2aa', 'nbr17225-5.2.5', 'wcag125'],
            'Conteúdo de vídeo pré-gravado deve ter audiodescrições para informações exclusivamente visuais.',
            'Adicione <track kind="descriptions" src="audiodesc.vtt"> aos vídeos com conteúdo visual importante.',
            'https://www.w3.org/WAI/WCAG22/Understanding/audio-description-prerecorded.html',
            nodes
        );
    };

    /**
     * 37. nbr-non-text-contrast (WCAG 1.4.11 / NBR 5.4.11)
     * UI components and graphical objects must have ≥ 3:1 contrast ratio.
     */
    WallyMonitor.NBRRules.checkNonTextContrast = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var inputs = doc.querySelectorAll('input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="url"], input[type="number"], textarea, select');

        for (var i = 0; i < inputs.length; i++) {
            var el = inputs[i];
            var cs = getComputed(el);
            if (!cs || cs.display === 'none') { continue; }

            var borderColor = cs.borderColor || cs.borderBottomColor || '';
            var bgColor = cs.backgroundColor || '';

            // Flag inputs with very light or invisible borders
            if (borderColor === 'transparent' || borderColor === bgColor) {
                var outline = cs.outlineStyle || 'none';
                var boxShadow = cs.boxShadow || 'none';
                if (outline === 'none' && boxShadow === 'none') {
                    nodes.push(makeNode(
                        el,
                        'moderate',
                        'Este campo de formulário não possui borda, contorno ou box-shadow visível. Seu limite não é distinguível, falhando no requisito de contraste não textual de 3:1 (WCAG 1.4.11).'
                    ));
                }
            }
        }

        return violation(
            'nbr-non-text-contrast',
            'moderate',
            ['wcag2aa', 'nbr17225-5.4.11', 'wcag1411'],
            'Componentes de interface e objetos gráficos devem ter uma relação de contraste de pelo menos 3:1 em relação às cores adjacentes.',
            'Garanta que os limites dos campos de formulário (bordas, contornos) tenham relação de contraste de pelo menos 3:1.',
            'https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html',
            nodes
        );
    };

    /**
     * 38. nbr-three-flashes (WCAG 2.3.1 / NBR 5.5.6)
     * Content must not flash more than 3 times per second.
     */
    WallyMonitor.NBRRules.checkThreeFlashes = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Targeted: only check elements likely to have animations
        var allEls = doc.querySelectorAll('[class*="animate"], [class*="blink"], [class*="flash"], [class*="pulse"], [class*="fade"], [class*="shake"], [class*="spin"], [class*="bounce"], [style*="animation"], .animated, .motion-safe, marquee, blink');
        for (var i = 0; i < allEls.length; i++) {
            var el = allEls[i];
            var cs = getComputed(el);
            if (!cs || cs.display === 'none') { continue; }

            var animDuration = cs.animationDuration || '0s';
            var animName = cs.animationName || 'none';

            if (animName !== 'none') {
                var dur = parseFloat(animDuration);
                // Animations shorter than 333ms could flash > 3 times/sec if repeating
                if (dur > 0 && dur < 0.334) {
                    var iterCount = cs.animationIterationCount || '1';
                    if (iterCount === 'infinite' || parseInt(iterCount, 10) > 3) {
                        nodes.push(makeNode(
                            el,
                            'critical',
                            'Este elemento possui uma animação repetitiva muito rápida ("' + animName + '", ' + animDuration + '). Conteúdo com flashes rápidos pode causar convulsões (WCAG 2.3.1).'
                        ));
                    }
                }
            }
        }

        return violation(
            'nbr-three-flashes',
            'critical',
            ['wcag2a', 'nbr17225-5.5.6', 'wcag231'],
            'Páginas web não devem conter nada que pisque mais de três vezes em qualquer período de um segundo.',
            'Remova ou desacelere animações com flashes rápidos. Use a media query prefers-reduced-motion.',
            'https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html',
            nodes
        );
    };

    /**
     * 39. nbr-motion-animation (WCAG 2.3.3 / NBR 5.5.8)
     * Motion animation must be disableable via prefers-reduced-motion.
     */
    WallyMonitor.NBRRules.checkMotionAnimation = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Step 1: Check if ANY stylesheet includes a @media (prefers-reduced-motion) rule
        var hasReducedMotionQuery = false;
        try {
            var sheets = doc.styleSheets;
            for (var s = 0; s < sheets.length; s++) {
                var rules;
                try { rules = sheets[s].cssRules || sheets[s].rules; } catch (e) { continue; }
                if (!rules) { continue; }
                for (var r = 0; r < rules.length; r++) {
                    if (rules[r].type === CSSRule.MEDIA_RULE) {
                        var mediaText = rules[r].conditionText || rules[r].media.mediaText || '';
                        if (/prefers-reduced-motion/.test(mediaText)) {
                            hasReducedMotionQuery = true;
                            break;
                        }
                    }
                }
                if (hasReducedMotionQuery) { break; }
            }
        } catch (e) { /* cross-origin */ }

        // Step 2: Only scan for animated elements if there's no reduced-motion media query
        if (!hasReducedMotionQuery) {
            var animated = doc.querySelectorAll('[class*="animate"], [class*="slide"], [class*="bounce"], [class*="fade"], [class*="zoom"], [style*="animation"]');
            var motionAnimPattern = /\b(slide|bounce|shake|spin|pulse|wobble|swing|flip|roll|zoom|fade-in|fade-out)\b/i;

            for (var i = 0; i < animated.length && i < 30; i++) {
                var el = animated[i];
                var cs = getComputed(el);
                if (!cs || cs.display === 'none') { continue; }

                var anim = cs.animationName || 'none';
                if (anim !== 'none' && motionAnimPattern.test(anim)) {
                    nodes.push(makeNode(
                        el,
                        'moderate',
                        'Este elemento possui uma animação de movimento ("' + anim + '") mas nenhuma consulta @media (prefers-reduced-motion: reduce) foi encontrada nas folhas de estilo. Usuários que precisam de movimento reduzido não podem desativá-la (WCAG 2.3.3).'
                    ));
                }
            }

            // Also flag if there are CSS animations/transitions but no reduced-motion query
            if (animated.length === 0) {
                // Check inline <style> blocks for animation definitions
                var styles = doc.querySelectorAll('style');
                for (var st = 0; st < styles.length; st++) {
                    var content = styles[st].textContent || '';
                    if (/@keyframes/.test(content) && !/prefers-reduced-motion/.test(content)) {
                        nodes.push({
                            target: ['style'],
                            html: '<style>/* @keyframes found without prefers-reduced-motion */</style>',
                            impact: 'moderate',
                            message: 'Esta folha de estilo define animações @keyframes mas não possui consulta @media (prefers-reduced-motion). Usuários que precisam de movimento reduzido não podem desativar as animações (WCAG 2.3.3).'
                        });
                        break;
                    }
                }
            }
        }

        return violation(
            'nbr-motion-animation',
            'moderate',
            ['wcag2aaa', 'nbr17225-5.5.8', 'wcag233'],
            'Animações de movimento disparadas por interação devem poder ser desativadas, a menos que a animação seja essencial.',
            'Use @media (prefers-reduced-motion: reduce) para desativar animações não essenciais.',
            'https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html',
            nodes
        );
    };

    /**
     * 40. nbr-on-focus (WCAG 3.2.1 / NBR 5.8.1)
     * Receiving focus must not trigger a change of context (e.g., onfocus submit, onfocus navigate).
     */
    WallyMonitor.NBRRules.checkOnFocus = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var focusable = doc.querySelectorAll('[onfocus]');

        for (var i = 0; i < focusable.length; i++) {
            var el = focusable[i];
            var handler = (el.getAttribute('onfocus') || '').toLowerCase();

            // Dangerous patterns: context-changing calls (use word boundaries to reduce false positives)
            var dangerousFocus = /\b(this\.form\.submit|form\.submit|document\.location|window\.location|location\.href|location\.replace|window\.open|window\.navigate)\b/i;
            if (dangerousFocus.test(handler)) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este elemento possui um handler onfocus que parece mudar o contexto ("' + handler.substring(0, 60) + '"). Receber foco não deve disparar uma mudança de contexto (WCAG 3.2.1).'
                ));
            }
        }

        return violation(
            'nbr-on-focus',
            'serious',
            ['wcag2a', 'nbr17225-5.8.1', 'wcag321'],
            'Quando qualquer componente de interface recebe foco, não deve iniciar uma mudança de contexto.',
            'Remova handlers onfocus que mudam contexto. Use ações explícitas do usuário (clique, envio) em vez disso.',
            'https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html',
            nodes
        );
    };

    /**
     * 41. nbr-on-input (WCAG 3.2.2 / NBR 5.8.2)
     * Changing a form field value must not automatically change context without warning.
     */
    WallyMonitor.NBRRules.checkOnInput = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var fields = doc.querySelectorAll('select[onchange], input[onchange]');

        for (var i = 0; i < fields.length; i++) {
            var el = fields[i];
            var handler = (el.getAttribute('onchange') || '').toLowerCase();

            // Use word-boundary regex to avoid false matches on function names like 'validateSubmission'
            var dangerousInput = /\b(this\.form\.submit|form\.submit|document\.location|window\.location|location\.href|location\.replace|window\.open)\b/i;
            if (dangerousInput.test(handler)) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este campo possui um handler onchange que muda o contexto ("' + handler.substring(0, 60) + '"). Os usuários devem ser avisados antes de mudanças de contexto (WCAG 3.2.2).'
                ));
            }
        }

        return violation(
            'nbr-on-input',
            'serious',
            ['wcag2a', 'nbr17225-5.8.2', 'wcag322'],
            'Alterar a configuração de qualquer componente de interface não deve causar automaticamente uma mudança de contexto, a menos que o usuário tenha sido avisado do comportamento antes de usar o componente.',
            'Remova handlers onchange que fazem envio automático. Adicione um botão de envio ou avise os usuários.',
            'https://www.w3.org/WAI/WCAG22/Understanding/on-input.html',
            nodes
        );
    };

    /**
     * 42. nbr-consistent-navigation (WCAG 3.2.3 / NBR 5.8.3)
     * Navigation mechanisms repeated on multiple pages must have consistent order.
     * Checks: multiple <nav> elements should have aria-label for identification.
     */
    WallyMonitor.NBRRules.checkConsistentNavigation = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var navs = doc.querySelectorAll('nav');

        if (navs.length > 1) {
            for (var i = 0; i < navs.length; i++) {
                var nav = navs[i];
                var label = nav.getAttribute('aria-label') || nav.getAttribute('aria-labelledby') || '';
                if (!label.trim()) {
                    nodes.push(makeNode(
                        nav,
                        'moderate',
                        'Esta página possui múltiplos elementos <nav>, mas este não possui aria-label. Os usuários não conseguem distinguir entre as regiões de navegação (WCAG 3.2.3).'
                    ));
                }
            }
        }

        return violation(
            'nbr-consistent-navigation',
            'moderate',
            ['wcag2aa', 'nbr17225-5.8.3', 'wcag323'],
            'Mecanismos de navegação que se repetem em múltiplas páginas web devem ocorrer na mesma ordem relativa cada vez que são repetidos.',
            'Adicione aria-label único a cada <nav> para identificar seu propósito (ex.: "Menu Principal", "Links do Rodapé").',
            'https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html',
            nodes
        );
    };

    /**
     * 43. nbr-consistent-identification (WCAG 3.2.4 / NBR 5.8.4)
     * Components with the same function must be identified consistently.
     * Checks: search inputs should have consistent labeling.
     */
    WallyMonitor.NBRRules.checkConsistentIdentification = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var searchInputs = doc.querySelectorAll('input[type="search"], input[name*="search"], input[name*="busca"], input[role="searchbox"]');

        var labels = [];
        for (var i = 0; i < searchInputs.length; i++) {
            var el = searchInputs[i];
            var label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('title') || '';
            labels.push({ el: el, label: label.trim().toLowerCase() });
        }

        // If multiple search inputs exist with different labels, flag inconsistency
        if (labels.length > 1) {
            var first = labels[0].label;
            for (var j = 1; j < labels.length; j++) {
                if (labels[j].label !== first && labels[j].label !== '') {
                    nodes.push(makeNode(
                        labels[j].el,
                        'moderate',
                        'Este campo de busca possui um rótulo diferente ("' + labels[j].label + '") de outro campo de busca ("' + first + '"). Componentes com a mesma função devem ser identificados de forma consistente (WCAG 3.2.4).'
                    ));
                }
            }
        }

        return violation(
            'nbr-consistent-identification',
            'moderate',
            ['wcag2aa', 'nbr17225-5.8.4', 'wcag324'],
            'Componentes que possuem a mesma funcionalidade dentro de um conjunto de páginas web devem ser identificados de forma consistente.',
            'Use os mesmos rótulos, nomes e alternativas de texto para componentes com a mesma função.',
            'https://www.w3.org/WAI/WCAG22/Understanding/consistent-identification.html',
            nodes
        );
    };

    /**
     * 44. nbr-character-key-shortcuts (WCAG 2.1.4 / NBR 5.5.4)
     * Single-character keyboard shortcuts must be remappable or disableable.
     */
    WallyMonitor.NBRRules.checkCharacterKeyShortcuts = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];

        // Note: HTML accesskey always requires a modifier (Alt/Ctrl+Alt) so it is
        // NOT a "character key shortcut" per WCAG 2.1.4. Only flag custom JS shortcuts.
        var shortcutEls = doc.querySelectorAll('[data-shortcut], [data-hotkey], [data-key]');
        for (var i = 0; i < shortcutEls.length; i++) {
            var el = shortcutEls[i];
            var key = el.getAttribute('data-shortcut') || el.getAttribute('data-hotkey') || el.getAttribute('data-key') || '';
            // Only flag single-character shortcuts without modifier prefix
            if (key.length === 1 || (/^[a-z0-9]$/i.test(key))) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este elemento possui um atalho de caractere único ("' + key + '"). Atalhos de tecla única podem conflitar com tecnologia assistiva e entrada por voz. O atalho deve ser remapeável ou desativável (WCAG 2.1.4).'
                ));
            }
        }

        // Also check for known single-key shortcut libraries (Mousetrap, hotkeys.js)
        try {
            var scripts = doc.querySelectorAll('script[src]');
            for (var j = 0; j < scripts.length; j++) {
                var scriptSrc = (scripts[j].getAttribute('src') || '').toLowerCase();
                if (/mousetrap|hotkeys/.test(scriptSrc)) {
                    nodes.push({
                        target: ['script[src*="' + scriptSrc.split('/').pop() + '"]'],
                        html: scripts[j].outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: 'A página inclui uma biblioteca de atalhos de teclado ("' + scriptSrc.split('/').pop() + '"). Verifique se todos os atalhos incluem teclas modificadoras ou podem ser remapeados (WCAG 2.1.4).'
                    });
                }
            }
        } catch (e) {}

        return violation(
            'nbr-character-key-shortcuts',
            'moderate',
            ['wcag2a', 'nbr17225-5.5.4', 'wcag214'],
            'Se um atalho de teclado usar apenas caracteres de letra, pontuação, número ou símbolo, deve ser possível desativar ou remapear o atalho.',
            'Use teclas modificadoras (Ctrl/Alt) com atalhos, ou forneça um mecanismo para remapeá-los.',
            'https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html',
            nodes
        );
    };

    /**
     * 45. nbr-error-prevention (WCAG 3.3.4 / NBR 5.9.4)
     * For legal/financial data, submissions must be reversible, verifiable, or confirmable.
     */
    WallyMonitor.NBRRules.checkErrorPrevention = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var forms = doc.querySelectorAll('form');

        for (var i = 0; i < forms.length; i++) {
            var form = forms[i];
            // Detect payment/financial forms
            var hasPayment = form.querySelector('input[name*="card"], input[name*="cartao"], input[name*="payment"], input[name*="pagamento"], input[autocomplete*="cc-"], input[name*="cpf"], input[name*="cnpj"]');
            var hasDelete = form.querySelector('input[name*="delete"], input[name*="remove"], button[name*="delete"]');

            if (hasPayment || hasDelete) {
                // Check for confirmation mechanism
                var hasConfirm = form.querySelector('input[name*="confirm"], input[type="checkbox"][required], [role="dialog"], .confirmation');
                if (!hasConfirm) {
                    var type = hasPayment ? 'financeiro/pagamento' : 'exclusão';
                    nodes.push(makeNode(
                        form,
                        'serious',
                        'Este formulário parece lidar com dados de ' + type + ' mas não possui mecanismo de confirmação. Envios com consequências legais/financeiras devem ser reversíveis, verificados ou confirmados (WCAG 3.3.4).'
                    ));
                }
            }
        }

        return violation(
            'nbr-error-prevention',
            'serious',
            ['wcag2aa', 'nbr17225-5.9.4', 'wcag334'],
            'Para páginas que causam compromissos legais ou transações financeiras, os envios devem ser reversíveis, verificados ou confirmados.',
            'Adicione uma etapa de confirmação, página de revisão ou mecanismo de desfazer para envios críticos de formulários.',
            'https://www.w3.org/WAI/WCAG22/Understanding/error-prevention-legal-financial-data.html',
            nodes
        );
    };

    /**
     * 46. nbr-breadcrumb-semantic (WCAG 1.3.1 / NBR 5.3.1)
     * Breadcrumbs must use <nav> with aria-label="Breadcrumb".
     */
    WallyMonitor.NBRRules.checkBreadcrumbSemantic = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var breadcrumbs = doc.querySelectorAll('.breadcrumb, .breadcrumbs, [class*="breadcrumb"], [class*="migalha"], [class*="trilha"]');
        var seen = []; // Avoid duplicates from parent/child matches

        for (var i = 0; i < breadcrumbs.length; i++) {
            var el = breadcrumbs[i];
            var cs = getComputed(el);
            if (cs && cs.display === 'none') { continue; }

            // Skip child items — only check the breadcrumb container itself
            var cls = (el.className || '').toLowerCase();
            if (/breadcrumb[-_]?(item|link|separator|divider|arrow|text|current|active|last)/.test(cls)) { continue; }

            // Skip if a parent in the seen list already covers this element
            var isDuplicate = false;
            for (var d = 0; d < seen.length; d++) {
                if (seen[d].contains(el)) { isDuplicate = true; break; }
            }
            if (isDuplicate) { continue; }
            seen.push(el);

            // Check if wrapped in <nav>
            var parent = el.closest('nav');
            if (!parent) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este breadcrumb não está dentro de um elemento <nav>. Breadcrumbs devem ser envolvidos em <nav aria-label="Breadcrumb"> para semântica adequada (WCAG 1.3.1).'
                ));
            } else {
                var navLabel = (parent.getAttribute('aria-label') || '').toLowerCase();
                var navLabelledBy = parent.getAttribute('aria-labelledby');
                var hasLabelledBy = false;
                if (navLabelledBy) {
                    var labelEl = doc.getElementById(navLabelledBy);
                    var labelText = labelEl ? (labelEl.textContent || '').toLowerCase() : '';
                    hasLabelledBy = /breadcrumb|migalha|trilha/.test(labelText);
                }
                if (!hasLabelledBy && navLabel.indexOf('breadcrumb') === -1 && navLabel.indexOf('migalha') === -1 && navLabel.indexOf('trilha') === -1) {
                    nodes.push(makeNode(
                        parent,
                        'minor',
                        'Este <nav> contém um breadcrumb mas não possui aria-label="Breadcrumb" ou aria-labelledby. Leitores de tela não conseguem identificá-lo como navegação de breadcrumb (WCAG 1.3.1).'
                    ));
                }
            }
        }

        return violation(
            'nbr-breadcrumb-semantic',
            'moderate',
            ['wcag2a', 'nbr17225-5.3.1', 'wcag131'],
            'A navegação de breadcrumb deve ser envolvida em um elemento <nav> com aria-label="Breadcrumb".',
            'Envolva breadcrumbs em <nav aria-label="Breadcrumb"> e use uma lista ordenada <ol> para os itens.',
            'https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html',
            nodes
        );
    };

    /**
     * 47. nbr-mobile-menu-aria (WCAG 4.1.2 / NBR 5.10.2)
     * Mobile menu toggles must have aria-expanded state.
     */
    WallyMonitor.NBRRules.checkMobileMenuAria = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var toggles = doc.querySelectorAll('.menu-toggle, .navbar-toggler, .hamburger, [class*="menu-btn"], [class*="nav-toggle"], button[class*="mobile"], [class*="burger"]');

        for (var i = 0; i < toggles.length; i++) {
            var el = toggles[i];
            var cs = getComputed(el);
            if (cs && cs.display === 'none') { continue; }

            var ariaExpanded = el.getAttribute('aria-expanded');
            var ariaControls = el.getAttribute('aria-controls');
            var ariaLabel = el.getAttribute('aria-label') || '';

            if (ariaExpanded === null) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este botão de menu mobile não possui o atributo aria-expanded. Leitores de tela não conseguem determinar se o menu está aberto ou fechado (WCAG 4.1.2).'
                ));
            }
            if (!ariaControls) {
                nodes.push(makeNode(
                    el,
                    'moderate',
                    'Este botão de menu mobile não possui aria-controls apontando para o elemento do menu. Leitores de tela não conseguem associar o botão ao seu menu (WCAG 4.1.2).'
                ));
            }
            if (!ariaLabel && !(el.textContent || '').trim()) {
                nodes.push(makeNode(
                    el,
                    'serious',
                    'Este botão de menu mobile não possui nome acessível (aria-label ou texto visível). Leitores de tela não conseguem identificar seu propósito (WCAG 4.1.2).'
                ));
            }
        }

        return violation(
            'nbr-mobile-menu-aria',
            'serious',
            ['wcag2a', 'nbr17225-5.10.2', 'wcag412'],
            'Botões de menu mobile devem ter aria-expanded, aria-controls e um nome acessível.',
            'Adicione aria-expanded="false", aria-controls="id-do-menu" e aria-label="Menu" aos botões de menu mobile.',
            'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
            nodes
        );
    };

    /**
     * 48. nbr-widget-heading (WCAG 2.4.10 / NBR 5.6.10)
     * Sidebar widgets should have headings or aria-label for identification.
     */
    WallyMonitor.NBRRules.checkWidgetHeading = function(doc) {
        if (!doc || typeof doc.querySelectorAll !== 'function') { return null; }

        var nodes = [];
        var widgets = doc.querySelectorAll('.widget, .wp-block-widget, aside > div, aside > section, [class*="sidebar"] > div, [class*="sidebar"] > section');

        for (var i = 0; i < widgets.length; i++) {
            var el = widgets[i];
            var cs = getComputed(el);
            if (cs && cs.display === 'none') { continue; }

            // Check for any heading inside
            var heading = el.querySelector('h1, h2, h3, h4, h5, h6');
            var ariaLabel = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || '';
            var role = el.getAttribute('role') || '';

            if (!heading && !ariaLabel && role !== 'presentation' && role !== 'none') {
                // Only flag if widget has meaningful content
                var text = (el.textContent || '').trim();
                if (text.length > 20) {
                    nodes.push(makeNode(
                        el,
                        'minor',
                        'Este widget/seção não possui cabeçalho ou aria-label. Usuários que navegam por cabeçalhos não conseguem identificar seu propósito (WCAG 2.4.10).'
                    ));
                }
            }
        }

        return violation(
            'nbr-widget-heading',
            'minor',
            ['wcag2aaa', 'nbr17225-5.6.10', 'wcag2410'],
            'Cabeçalhos de seção devem ser usados para organizar o conteúdo. Widgets devem ter cabeçalhos descritivos.',
            'Adicione um cabeçalho (<h2>, <h3>) ou aria-label a cada widget para identificar seu conteúdo.',
            'https://www.w3.org/WAI/WCAG22/Understanding/section-headings.html',
            nodes
        );
    };

    // ═══════════════════════════════════════════════════════════════════
    //  Runner — execute all 48 checks
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Run every NBR 17.225 custom rule against the given document.
     *
     * @param {Document} doc - The document to audit.
     * @returns {object[]} Array of violation objects (nulls filtered out).
     */
    WallyMonitor.NBRRules.runAllChecks = function(doc) {
        var results = [
            // Group 1 – WCAG 2.2 New Criteria
            WallyMonitor.NBRRules.checkFocusNotObscured(doc),
            WallyMonitor.NBRRules.checkFocusAppearance(doc),
            WallyMonitor.NBRRules.checkDraggingAlternative(doc),
            WallyMonitor.NBRRules.checkTargetSize(doc),
            WallyMonitor.NBRRules.checkConsistentHelp(doc),
            WallyMonitor.NBRRules.checkRedundantEntry(doc),
            WallyMonitor.NBRRules.checkAccessibleAuth(doc),

            // Group 2 – Language & Text
            WallyMonitor.NBRRules.checkLangAttribute(doc),
            WallyMonitor.NBRRules.checkLangParts(doc),
            WallyMonitor.NBRRules.checkAbbreviations(doc),
            WallyMonitor.NBRRules.checkReadingLevel(doc),

            // Group 3 – Keyboard & Focus
            WallyMonitor.NBRRules.checkKeyboardTrap(doc),
            WallyMonitor.NBRRules.checkSkipNavigation(doc),
            WallyMonitor.NBRRules.checkFocusOrder(doc),
            WallyMonitor.NBRRules.checkFocusVisible(doc),
            WallyMonitor.NBRRules.checkPointerCancellation(doc),

            // Group 4 – Visual & Timing
            WallyMonitor.NBRRules.checkAudioAutoplay(doc),
            WallyMonitor.NBRRules.checkTextResize(doc),
            WallyMonitor.NBRRules.checkImagesOfText(doc),
            WallyMonitor.NBRRules.checkReflow(doc),
            WallyMonitor.NBRRules.checkTextSpacing(doc),
            WallyMonitor.NBRRules.checkContentOnHover(doc),
            WallyMonitor.NBRRules.checkTimingAdjustable(doc),
            WallyMonitor.NBRRules.checkPauseStop(doc),

            // Group 5 – Semantics & Structure
            WallyMonitor.NBRRules.checkMeaningfulSequence(doc),
            WallyMonitor.NBRRules.checkSensoryCharacteristics(doc),
            WallyMonitor.NBRRules.checkOrientation(doc),
            WallyMonitor.NBRRules.checkIdentifyInputPurpose(doc),
            WallyMonitor.NBRRules.checkPageTitled(doc),
            WallyMonitor.NBRRules.checkLinkPurpose(doc),
            WallyMonitor.NBRRules.checkMultipleWays(doc),
            WallyMonitor.NBRRules.checkSectionHeadings(doc),

            // Group 6 – Error Handling & Forms
            WallyMonitor.NBRRules.checkErrorIdentification(doc),
            WallyMonitor.NBRRules.checkErrorSuggestion(doc),
            WallyMonitor.NBRRules.checkStatusMessages(doc),

            // Group 7 – Expanded Coverage (v1.2.0)
            WallyMonitor.NBRRules.checkVideoAudioDesc(doc),
            WallyMonitor.NBRRules.checkNonTextContrast(doc),
            WallyMonitor.NBRRules.checkThreeFlashes(doc),
            WallyMonitor.NBRRules.checkMotionAnimation(doc),
            WallyMonitor.NBRRules.checkOnFocus(doc),
            WallyMonitor.NBRRules.checkOnInput(doc),
            WallyMonitor.NBRRules.checkConsistentNavigation(doc),
            WallyMonitor.NBRRules.checkConsistentIdentification(doc),
            WallyMonitor.NBRRules.checkCharacterKeyShortcuts(doc),
            WallyMonitor.NBRRules.checkErrorPrevention(doc),
            WallyMonitor.NBRRules.checkBreadcrumbSemantic(doc),
            WallyMonitor.NBRRules.checkMobileMenuAria(doc),
            WallyMonitor.NBRRules.checkWidgetHeading(doc)
        ];

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

})(jQuery);
