/**
 * @file wally-custom-rules.js
 * @description All 17 custom accessibility check functions for Wally Monitor.
 *              Checks for empty alt images, empty buttons, WordPress galleries,
 *              media accessibility, forms, heading hierarchy, tables, cover block
 *              contrast, navigation structure, site logo, search widget,
 *              custom HTML blocks, embed accessibility, social links, file/download
 *              blocks, details/accordion, and generic landmarks.
 * @module WallyMonitor.Rules
 */
window.WallyMonitor = window.WallyMonitor || {};

(function($) {
    'use strict';

    WallyMonitor.Rules = WallyMonitor.Rules || {};

    // ========================================================================
    // Private helper functions (used by checkCoverBlockContrast)
    // ========================================================================

    /**
     * Calcula luminância relativa de uma cor RGB (fórmula WCAG)
     * @param {number} r - Valor vermelho (0-255)
     * @param {number} g - Valor verde (0-255)
     * @param {number} b - Valor azul (0-255)
     * @returns {number} Luminância relativa (0-1)
     */
    function getRelativeLuminance(r, g, b) {
        // Normaliza valores para 0-1
        const rsRGB = r / 255;
        const gsRGB = g / 255;
        const bsRGB = b / 255;

        // Aplica correção gamma (sRGB to linear)
        const rLinear = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
        const gLinear = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
        const bLinear = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);

        // Calcula luminância relativa usando coeficientes WCAG
        return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
    }

    /**
     * Calcula razão de contraste entre duas cores (fórmula WCAG)
     * @param {number} l1 - Luminância da cor mais clara
     * @param {number} l2 - Luminância da cor mais escura
     * @returns {number} Razão de contraste (1-21)
     */
    function getContrastRatio(l1, l2) {
        const lighter = Math.max(l1, l2);
        const darker = Math.min(l1, l2);
        return (lighter + 0.05) / (darker + 0.05);
    }

    /**
     * Extrai valores RGB de uma string de cor CSS
     * @param {string} colorStr - String CSS (rgb, rgba, hex, etc)
     * @returns {Object|null} Objeto {r, g, b} ou null se inválido
     */
    function parseColor(colorStr) {
        if (!colorStr || colorStr === 'transparent' || colorStr === 'rgba(0, 0, 0, 0)') {
            return null;
        }

        // rgb() ou rgba()
        const rgbMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
        if (rgbMatch) {
            return {
                r: parseInt(rgbMatch[1]),
                g: parseInt(rgbMatch[2]),
                b: parseInt(rgbMatch[3])
            };
        }

        // hex (#RGB ou #RRGGBB)
        const hexMatch = colorStr.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
        if (hexMatch) {
            const hex = hexMatch[1];
            if (hex.length === 3) {
                return {
                    r: parseInt(hex[0] + hex[0], 16),
                    g: parseInt(hex[1] + hex[1], 16),
                    b: parseInt(hex[2] + hex[2], 16)
                };
            } else {
                return {
                    r: parseInt(hex.substring(0, 2), 16),
                    g: parseInt(hex.substring(2, 4), 16),
                    b: parseInt(hex.substring(4, 6), 16)
                };
            }
        }

        // Cores nomeadas comuns
        const namedColors = {
            'white': { r: 255, g: 255, b: 255 },
            'black': { r: 0, g: 0, b: 0 },
            'red': { r: 255, g: 0, b: 0 },
            'green': { r: 0, g: 128, b: 0 },
            'blue': { r: 0, g: 0, b: 255 }
        };
        
        return namedColors[colorStr.toLowerCase()] || null;
    }

    /**
     * Gera um seletor CSS único para um elemento DOM.
     * Garante que cada elemento tenha um target distinto para que a
     * deduplicação em processAxeResults não colapse violações diferentes.
     * @param {Element} el - Elemento DOM
     * @returns {string} Seletor CSS único para o elemento
     */
    function getUniqueSelector(el) {
        if (!el || !el.tagName) return 'unknown';
        if (el.id) return '#' + el.id;
        var tag = el.tagName.toLowerCase();
        var selector = tag;
        if (el.className && typeof el.className === 'string') {
            var classes = el.className.trim().split(/\s+/).filter(function(c) {
                return c && c.indexOf('wp-container-') !== 0;
            });
            if (classes.length > 0) {
                selector += '.' + classes.slice(0, 2).join('.');
            }
        }
        var parent = el.parentElement;
        if (parent) {
            var sameType = [];
            for (var i = 0; i < parent.children.length; i++) {
                if (parent.children[i].tagName === el.tagName) {
                    sameType.push(parent.children[i]);
                }
            }
            if (sameType.length > 1) {
                var idx = sameType.indexOf(el);
                if (idx >= 0) {
                    selector += ':nth-of-type(' + (idx + 1) + ')';
                }
            }
        }
        return selector;
    }

    // ========================================================================
    // Public rule functions
    // ========================================================================

    /**
     * Verifica imagens com alt vazio ou ausente (regra customizada)
     * O axe-core pode não detectar quando a imagem está em <figure>
     * NOVO: Detecta texto embutido em imagens (alt muito longo = possível texto)
     */
    WallyMonitor.Rules.checkEmptyAltImages = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') {
            return null;
        }
        const images = document.querySelectorAll('img');
        const problematicImages = [];
        
        images.forEach((img, index) => {
            const alt = img.getAttribute('alt');
            const src = img.getAttribute('src') || '';
            
            // Verifica se alt está vazio ou ausente
            if (alt === null || alt.trim() === '') {
                problematicImages.push({
                    target: [getUniqueSelector(img)],
                    html: img.outerHTML.substring(0, 100) + '...',
                    impact: 'critical',
                    message: alt === null ? 
                        'Imagem sem atributo alt. Adicione uma descrição.' : 
                        'Imagem com alt vazio. Se for decorativa, tudo bem. Se for informativa, adicione uma descrição.'
                });
            }
            // NOVO: Detecta possível texto embutido em imagem
            else if (alt.trim().length > 50) {
                // Heurísticas para identificar texto em imagem:
                // 1. Alt muito longo (>50 caracteres) sugere descrição de texto
                // 2. Presença de palavras como "banner", "infográfico", "gráfico", "tabela"
                // 3. Nome de arquivo sugestivo
                const altLower = alt.toLowerCase();
                const srcLower = src.toLowerCase();
                const textIndicators = [
                    'banner', 'infográfico', 'infografico', 'gráfico', 'grafico', 
                    'tabela', 'estatística', 'estatistica', 'citação', 'citacao',
                    'quote', 'infographic', 'chart', 'graph', 'table', 'stats'
                ];
                
                const hasTextIndicator = textIndicators.some(indicator => 
                    altLower.includes(indicator) || srcLower.includes(indicator)
                );
                
                // Alt com múltiplas frases ou parágrafos
                const hasMultipleSentences = (alt.match(/\.\s+/g) || []).length > 1;
                
                // Alt muito descritivo (>100 chars) ou com indicadores
                if (alt.length > 100 || hasTextIndicator || hasMultipleSentences) {
                    problematicImages.push({
                        target: [getUniqueSelector(img)],
                        html: img.outerHTML.substring(0, 100) + '...',
                        impact: 'serious',
                        message: `Possível texto embutido em imagem (alt com ${alt.length} caracteres). ` +
                                `Texto em imagens não é acessível para leitores de tela nem selecionável. ` +
                                `Considere usar texto HTML real ou fornecer versão alternativa em texto.`
                    });
                }
            }
        });
        
        if (problematicImages.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-image-alt-empty',
            impact: 'critical',
            tags: ['wcag2a', 'wcag21a', 'wcag111', 'cat.text-alternatives', 'section508'],
            description: 'Imagens devem ter texto alternativo descritivo. Use alt="" apenas para imagens puramente decorativas. Evite texto embutido em imagens.',
            help: 'Imagens sem texto alternativo adequado ou com possível texto embutido',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
            nodes: problematicImages
        };
    };

    /**
     * Verifica botões/links vazios (regra customizada)
     * Detecta botões Gutenberg e links sem texto descritivo
     */
    WallyMonitor.Rules.checkEmptyButtons = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') {
            return null;
        }
        const problematicElements = [];
        
        // Detecta botões Gutenberg com problemas: sem texto, sem href, ou ambos
        const gutenbergButtons = document.querySelectorAll('.wp-block-button__link');
        gutenbergButtons.forEach((btn, index) => {
            const hasText = btn.textContent.trim().length > 0;
            const hasAriaLabel = btn.getAttribute('aria-label') && btn.getAttribute('aria-label').trim().length > 0;
            const hasHref = btn.hasAttribute('href') && btn.getAttribute('href').trim().length > 0;
            const hasRole = btn.hasAttribute('role');
            if (!hasText && !hasAriaLabel) {
                // Caso 1: Botão sem texto E sem aria-label
                problematicElements.push({
                    target: [getUniqueSelector(btn)],
                    html: btn.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Botão Gutenberg sem texto visível ou aria-label.' +
                            (!hasHref ? ' Também está sem destino (href).' : '')
                });
            } else if (!hasHref && !hasRole) {
                // Caso 2: Tem texto mas sem href — <a> não é focável pelo teclado
                // WordPress gera <a class="wp-block-button__link"> ao invés de <button>.
                // Sem href, o <a> perde foco nativo e não é anunciado como interativo.
                problematicElements.push({
                    target: [getUniqueSelector(btn)],
                    html: btn.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Botão Gutenberg sem destino (href). ' +
                            'O elemento <a> sem href não é focável pelo teclado e não é anunciado como interativo por leitores de tela. ' +
                            'Adicione um href válido ou use role="button" com tabindex="0".'
                });
            }
        });
        
        // Detecta links vazios genéricos
        const emptyLinks = document.querySelectorAll('a:not([aria-label])');
        emptyLinks.forEach((link, index) => {
            const hasText = link.textContent.trim().length > 0;
            const hasAriaLabel = link.getAttribute('aria-label') && link.getAttribute('aria-label').trim().length > 0;
            
            if (!hasText && !hasAriaLabel && !link.querySelector('img')) {
                problematicElements.push({
                    target: [getUniqueSelector(link)],
                    html: link.outerHTML.substring(0, 100),
                    impact: 'serious',
                    message: 'Link sem texto descritivo ou aria-label.'
                });
            }
        });
        
        if (problematicElements.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-empty-button-link',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag244', 'wcag412', 'cat.name-role-value', 'best-practice'],
            description: 'Botões e links devem ter texto descritivo ou aria-label para que leitores de tela possam anunciar seu propósito.',
            help: 'Botões/Links vazios sem texto descritivo',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html',
            nodes: problematicElements
        };
    };

    /**
     * Verifica galerias WordPress sem descrições adequadas
     * Detecta blocos de galeria Gutenberg e shortcodes [gallery]
     */
    WallyMonitor.Rules.checkWordPressGalleries = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicGalleries = [];
        
        // Detecta blocos de galeria Gutenberg
        const gutenbergGalleries = document.querySelectorAll('.wp-block-gallery');
        gutenbergGalleries.forEach((gallery, index) => {
            const images = gallery.querySelectorAll('img');
            let imagesWithoutAlt = 0;
            
            images.forEach(img => {
                const alt = img.getAttribute('alt');
                if (!alt || alt.trim() === '') {
                    imagesWithoutAlt++;
                }
            });
            
            if (imagesWithoutAlt > 0) {
                problematicGalleries.push({
                    target: [getUniqueSelector(gallery)],
                    html: gallery.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: `Galeria com ${imagesWithoutAlt} de ${images.length} imagens sem texto alternativo.`
                });
            }
        });
        
        // Detecta shortcode [gallery] renderizado
        const galleryShortcodes = document.querySelectorAll('.gallery');
        galleryShortcodes.forEach((gallery, index) => {
            const images = gallery.querySelectorAll('img');
            let imagesWithoutAlt = 0;
            
            images.forEach(img => {
                const alt = img.getAttribute('alt');
                if (!alt || alt.trim() === '') {
                    imagesWithoutAlt++;
                }
            });
            
            if (imagesWithoutAlt > 0) {
                problematicGalleries.push({
                    target: [getUniqueSelector(gallery)],
                    html: gallery.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: `Galeria [gallery] com ${imagesWithoutAlt} de ${images.length} imagens sem alt.`
                });
            }
        });
        
        if (problematicGalleries.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-wp-gallery-missing-alt',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag111', 'cat.text-alternatives', 'best-practice'],
            description: 'Todas as imagens em galerias devem ter texto alternativo descritivo.',
            help: 'Galerias WordPress com imagens sem descrição',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
            nodes: problematicGalleries
        };
    };

    /**
     * Verifica vídeos e áudios sem legendas/transcrições
     * Detecta blocos de vídeo/áudio Gutenberg, embeds e shortcodes
     */
    WallyMonitor.Rules.checkMediaAccessibility = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') {
            return null;
        }
        const problematicMedia = [];
        
        // Detecta blocos de vídeo Gutenberg
        const videoBlocks = document.querySelectorAll('.wp-block-video video, .wp-block-embed__wrapper iframe[src*="youtube"], .wp-block-embed__wrapper iframe[src*="vimeo"]');
        videoBlocks.forEach((video, index) => {
            const hasTrack = video.querySelector('track[kind="captions"], track[kind="subtitles"]');
            
            if (!hasTrack && video.tagName === 'VIDEO') {
                problematicMedia.push({
                    target: [getUniqueSelector(video)],
                    html: video.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Vídeo sem legendas (track). Adicione legendas para acessibilidade.'
                });
            } else if (video.tagName === 'IFRAME') {
                // Embeds (YouTube, Vimeo) - apenas alerta
                problematicMedia.push({
                    target: [getUniqueSelector(video)],
                    html: video.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Vídeo embed detectado. Verifique se tem legendas habilitadas no YouTube/Vimeo.'
                });
            }
        });
        
        // Detecta blocos de áudio Gutenberg
        const audioBlocks = document.querySelectorAll('.wp-block-audio audio');
        audioBlocks.forEach((audio, index) => {
            // Verifica se há uma transcrição próxima (parágrafo logo após)
            const nextElement = audio.closest('.wp-block-audio')?.nextElementSibling;
            const hasTranscript = nextElement && (
                nextElement.textContent.toLowerCase().includes('transcrição') ||
                nextElement.textContent.toLowerCase().includes('transcript')
            );
            
            if (!hasTranscript) {
                problematicMedia.push({
                    target: [getUniqueSelector(audio)],
                    html: audio.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Áudio sem transcrição visível. Adicione a transcrição logo após o player.'
                });
            }
        });
        
        if (problematicMedia.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-wp-media-no-captions',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag121', 'wcag122', 'cat.time-and-media', 'best-practice'],
            description: 'Vídeos devem ter legendas e áudios devem ter transcrições para usuários surdos ou com dificuldades auditivas.',
            help: 'Vídeos/Áudios sem legendas ou transcrições',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/captions-prerecorded.html',
            nodes: problematicMedia
        };
    };

    /**
     * Verifica formulários de plugins populares sem labels adequados
     * Detecta Contact Form 7, Gravity Forms, WPForms, etc.
     */
    WallyMonitor.Rules.checkWordPressForms = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') {
            return null;
        }
        const problematicFields = [];
        
        // Contact Form 7
        const cf7Forms = document.querySelectorAll('.wpcf7-form');
        cf7Forms.forEach((form, index) => {
            const inputs = form.querySelectorAll('input[type="text"], input[type="email"], input[type="tel"], textarea');
            
            inputs.forEach(input => {
                const hasLabel = form.querySelector(`label[for="${input.id}"]`) || input.closest('label');
                const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby');
                const hasPlaceholder = input.getAttribute('placeholder');
                
                // Placeholder sozinho NÃO é suficiente para acessibilidade
                if (!hasLabel && !hasAriaLabel) {
                    problematicFields.push({
                        target: [getUniqueSelector(input)],
                        html: input.outerHTML.substring(0, 100),
                        impact: 'serious',
                        message: `Campo "${input.name}" do Contact Form 7 sem label. ${hasPlaceholder ? 'Placeholder não substitui label!' : ''}`
                    });
                }
            });
        });
        
        // Gravity Forms
        const gfForms = document.querySelectorAll('.gform_wrapper');
        gfForms.forEach((form, index) => {
            const fields = form.querySelectorAll('.gfield');
            
            fields.forEach(field => {
                const input = field.querySelector('input, textarea, select');
                if (!input) return;
                
                const label = field.querySelector('.gfield_label');
                const isHidden = label && label.classList.contains('gform_hidden');
                
                if (isHidden && !input.getAttribute('aria-label')) {
                    problematicFields.push({
                        target: [getUniqueSelector(input)],
                        html: input.outerHTML.substring(0, 100),
                        impact: 'serious',
                        message: 'Campo do Gravity Forms com label oculto mas sem aria-label.'
                    });
                }
            });
        });
        
        // WPForms
        const wpForms = document.querySelectorAll('.wpforms-form');
        wpForms.forEach((form, index) => {
            const fields = form.querySelectorAll('.wpforms-field');
            
            fields.forEach(field => {
                const input = field.querySelector('input, textarea, select');
                if (!input) return;
                
                const label = field.querySelector('label');
                
                if (!label && !input.getAttribute('aria-label')) {
                    problematicFields.push({
                        target: [getUniqueSelector(input)],
                        html: input.outerHTML.substring(0, 100),
                        impact: 'serious',
                        message: 'Campo do WPForms sem label associado.'
                    });
                }
            });
        });
        
        if (problematicFields.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-wp-forms-missing-labels',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag332', 'cat.forms', 'best-practice'],
            description: 'Campos de formulário devem ter labels descritivos associados. Placeholders não são suficientes.',
            help: 'Formulários WordPress sem labels adequados',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html',
            nodes: problematicFields
        };
    };

    /**
     * Verifica hierarquia de headings (H1, H2, H3, etc.)
     * Detecta saltos incorretos na hierarquia (ex: H1 → H4 sem H2/H3)
     */
    WallyMonitor.Rules.checkHeadingHierarchy = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') {
            return null;
        }
        const problematicHeadings = [];
        const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
        
        if (headings.length === 0) return null;
        
        let previousLevel = 0;
        
        headings.forEach((heading, index) => {
            const currentLevel = parseInt(heading.tagName.substring(1));
            
            // Verifica se pulou níveis (ex: H2 → H4)
            if (previousLevel > 0 && currentLevel > previousLevel + 1) {
                problematicHeadings.push({
                    target: [getUniqueSelector(heading)],
                    html: heading.outerHTML.substring(0, 100),
                    impact: 'moderate',
                    message: `Salto de ${heading.previousElementSibling?.tagName || 'H' + previousLevel} para ${heading.tagName}. Hierarquia deve ser sequencial.`
                });
            }
            
            previousLevel = currentLevel;
        });
        
        // Verifica se tem mais de um H1
        const h1Count = headings.filter(h => h.tagName === 'H1').length;
        if (h1Count > 1) {
            problematicHeadings.push({
                target: ['h1'],
                html: `Encontrados ${h1Count} elementos H1`,
                impact: 'moderate',
                message: `Página tem ${h1Count} títulos H1. Recomendado apenas 1 H1 por página.`
            });
        }
        
        if (problematicHeadings.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-heading-hierarchy',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag131', 'cat.semantics', 'best-practice'],
            description: 'Títulos devem seguir uma hierarquia lógica e sequencial (H1 → H2 → H3) para facilitar a navegação por leitores de tela.',
            help: 'Hierarquia de headings incorreta',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
            nodes: problematicHeadings
        };
    };

    /**
     * Verifica tabelas sem headers ou estrutura semântica adequada
     * Detecta <table> sem <th>, scope, caption, etc.
     */
    WallyMonitor.Rules.checkTableAccessibility = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicTables = [];
        const tables = document.querySelectorAll('table');
        
        tables.forEach((table, index) => {
            const hasHeaders = table.querySelectorAll('th').length > 0;
            const hasCaption = table.querySelector('caption');
            const hasSummary = table.getAttribute('summary') || table.getAttribute('aria-describedby');
            
            if (!hasHeaders) {
                problematicTables.push({
                    target: [getUniqueSelector(table)],
                    html: table.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Tabela sem elementos <th>. Use <th> para cabeçalhos de linhas/colunas.'
                });
            }
            
            // Verifica se headers têm scope definido (para tabelas complexas)
            const headers = table.querySelectorAll('th');
            let headersWithoutScope = 0;
            headers.forEach(th => {
                if (!th.getAttribute('scope')) {
                    headersWithoutScope++;
                }
            });
            
            if (headers.length > 0 && headersWithoutScope === headers.length && headers.length > 2) {
                problematicTables.push({
                    target: [getUniqueSelector(table)],
                    html: table.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Tabela complexa sem atributo "scope" nos <th>. Use scope="col" ou scope="row".'
                });
            }
            
            if (!hasCaption && !hasSummary) {
                problematicTables.push({
                    target: [getUniqueSelector(table)],
                    html: table.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Tabela sem <caption> ou aria-describedby. Adicione uma descrição do conteúdo.'
                });
            }
        });
        
        if (problematicTables.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-table-accessibility',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag131', 'cat.tables', 'best-practice'],
            description: 'Tabelas devem ter estrutura semântica clara com <th>, scope e caption para serem navegáveis por leitores de tela.',
            help: 'Tabelas sem estrutura acessível',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
            nodes: problematicTables
        };
    };

    /**
     * Verifica contraste de texto em blocos Cover (texto sobre imagem)
     * NOVO: Calcula contraste real usando fórmula WCAG de luminância
     */
    WallyMonitor.Rules.checkCoverBlockContrast = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicCovers = [];
        const coverBlocks = document.querySelectorAll('.wp-block-cover');
        
        coverBlocks.forEach((cover, index) => {
            const hasOverlay = cover.querySelector('.wp-block-cover__background');
            const textElements = cover.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span, a');
            
            textElements.forEach(text => {
                const textContent = text.textContent.trim();
                if (textContent.length === 0) return;

                const computedStyle = window.getComputedStyle(text);
                const textColor = parseColor(computedStyle.color);
                const bgColor = parseColor(computedStyle.backgroundColor);
                
                // Se texto tem background próprio, calcula contraste direto
                if (bgColor) {
                    const textLum = getRelativeLuminance(textColor.r, textColor.g, textColor.b);
                    const bgLum = getRelativeLuminance(bgColor.r, bgColor.g, bgColor.b);
                    const contrast = getContrastRatio(textLum, bgLum);
                    
                    // WCAG AA: 4.5:1 para texto normal, 3:1 para texto grande (18pt+ ou 14pt+ bold)
                    const fontSize = parseFloat(computedStyle.fontSize);
                    const fontWeight = computedStyle.fontWeight;
                    const isLargeText = fontSize >= 18 || (fontSize >= 14 && parseInt(fontWeight) >= 700);
                    const minContrast = isLargeText ? 3 : 4.5;
                    
                    if (contrast < minContrast) {
                        problematicCovers.push({
                            target: [getUniqueSelector(text)],
                            html: text.outerHTML.substring(0, 100),
                            impact: 'serious',
                            message: `Contraste insuficiente: ${contrast.toFixed(2)}:1 (mínimo WCAG AA: ${minContrast}:1). Texto "${textContent.substring(0, 30)}..." pode ser ilegível.`
                        });
                    }
                } else if (hasOverlay) {
                    // Texto depende do overlay - verifica opacity
                    const overlayStyle = window.getComputedStyle(hasOverlay);
                    const overlayColor = parseColor(overlayStyle.backgroundColor);
                    const overlayOpacity = parseFloat(overlayStyle.opacity);
                    
                    if (overlayOpacity < 0.3) {
                        problematicCovers.push({
                            target: [getUniqueSelector(cover)],
                            html: text.outerHTML.substring(0, 100),
                            impact: 'serious',
                            message: `Overlay muito transparente (${(overlayOpacity * 100).toFixed(0)}%). Contraste sobre imagem pode ser insuficiente.`
                        });
                    } else if (overlayColor && textColor) {
                        // Calcula contraste assumindo overlay sobre fundo claro (pior caso)
                        const textLum = getRelativeLuminance(textColor.r, textColor.g, textColor.b);
                        const overlayLum = getRelativeLuminance(overlayColor.r, overlayColor.g, overlayColor.b);
                        
                        // Luminância efetiva do overlay considerando opacity
                        const effectiveLum = overlayLum * overlayOpacity + 1.0 * (1 - overlayOpacity);
                        const contrast = getContrastRatio(textLum, effectiveLum);
                        
                        if (contrast < 4.5) {
                            problematicCovers.push({
                                target: [getUniqueSelector(text)],
                                html: text.outerHTML.substring(0, 100),
                                impact: 'serious',
                                message: `Contraste estimado: ${contrast.toFixed(2)}:1 (mínimo: 4.5:1). Aumente opacity do overlay ou ajuste cores.`
                            });
                        }
                    }
                } else {
                    // Sem overlay e sem background próprio - problema crítico
                    problematicCovers.push({
                        target: [getUniqueSelector(text)],
                        html: text.outerHTML.substring(0, 100),
                        impact: 'critical',
                        message: 'Texto diretamente sobre imagem sem overlay. Contraste não garantido (depende da imagem).'
                    });
                }
            });
        });
        
        if (problematicCovers.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-cover-contrast',
            impact: 'serious',
            tags: ['wcag2aa', 'wcag21aa', 'wcag143', 'cat.color', 'best-practice'],
            description: 'Texto sobre imagens (blocos Cover) deve ter contraste mínimo de 4.5:1 (texto normal) ou 3:1 (texto grande). Use overlay com opacidade adequada.',
            help: 'Blocos Cover com contraste insuficiente (cálculo WCAG)',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html',
            nodes: problematicCovers
        };
    };

    /**
     * Verifica menus de navegação sem estrutura semântica <nav>
     * Detecta listas de links que deveriam estar em <nav>
     */
    WallyMonitor.Rules.checkNavigationStructure = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicNavs = [];
        
        // Detecta blocos de navegação Gutenberg sem <nav>
        const navBlocks = document.querySelectorAll('.wp-block-navigation');
        navBlocks.forEach((navBlock, index) => {
            if (navBlock.tagName !== 'NAV') {
                const parentNav = navBlock.closest('nav');
                if (!parentNav) {
                    problematicNavs.push({
                        target: [getUniqueSelector(navBlock)],
                        html: navBlock.outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: 'Bloco de navegação Gutenberg não está dentro de um elemento <nav>.'
                    });
                }
            }
        });
        
        // Detecta menus clássicos do WordPress
        const wpMenus = document.querySelectorAll('.menu, .nav-menu');
        wpMenus.forEach((menu, index) => {
            const parentNav = menu.closest('nav');
            if (!parentNav) {
                problematicNavs.push({
                    target: [getUniqueSelector(menu)],
                    html: menu.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Menu WordPress não está dentro de um elemento <nav>.'
                });
            }
        });
        
        // Verifica se <nav> tem aria-label ou aria-labelledby
        const navElements = document.querySelectorAll('nav');
        navElements.forEach((nav, index) => {
            const hasLabel = nav.getAttribute('aria-label') || nav.getAttribute('aria-labelledby');
            if (!hasLabel && navElements.length > 1) {
                problematicNavs.push({
                    target: [getUniqueSelector(nav)],
                    html: nav.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Elemento <nav> sem aria-label. Com múltiplos <nav>, cada um deve ter label descritivo.'
                });
            }
        });
        
        if (problematicNavs.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-navigation-structure',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag131', 'cat.semantics', 'best-practice'],
            description: 'Menus de navegação devem usar elemento <nav> com aria-label descritivo para facilitar navegação por leitores de tela.',
            help: 'Navegação sem estrutura semântica adequada',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
            nodes: problematicNavs
        };
    };

    /**
     * Verifica logo do site sem texto alternativo adequado
     * Detecta imagens de logo com alt genérico ou vazio
     */
    WallyMonitor.Rules.checkSiteLogo = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicLogos = [];
        
        // Detecta bloco de logo Gutenberg
        const logoBlocks = document.querySelectorAll('.wp-block-site-logo img, .custom-logo');
        logoBlocks.forEach((logo, index) => {
            const alt = logo.getAttribute('alt');
            
            if (!alt || alt.trim() === '') {
                problematicLogos.push({
                    target: [getUniqueSelector(logo)],
                    html: logo.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Logo do site sem texto alternativo.'
                });
            } else if (alt.toLowerCase() === 'logo' || alt.toLowerCase() === 'site logo') {
                problematicLogos.push({
                    target: [getUniqueSelector(logo)],
                    html: logo.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: `Logo com alt genérico "${alt}". Use o nome do site: alt="${document.title || 'Nome do Site'}"`
                });
            }
        });
        
        if (problematicLogos.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-site-logo-alt',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag111', 'cat.text-alternatives', 'best-practice'],
            description: 'Logo do site deve ter texto alternativo descritivo com o nome da empresa/site, não apenas "logo".',
            help: 'Logo do site sem alt adequado',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
            nodes: problematicLogos
        };
    };

    /**
     * Verifica widget de busca sem label adequado
     * Detecta campos de busca WordPress sem label visível ou aria-label
     */
    WallyMonitor.Rules.checkSearchWidget = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicSearches = [];
        
        // Detecta bloco de busca Gutenberg
        const searchBlocks = document.querySelectorAll('.wp-block-search');
        searchBlocks.forEach((searchBlock, index) => {
            const input = searchBlock.querySelector('input[type="search"]');
            if (!input) return;
            
            const hasLabel = searchBlock.querySelector('label') || input.getAttribute('aria-label');
            
            if (!hasLabel) {
                problematicSearches.push({
                    target: [getUniqueSelector(input)],
                    html: input.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Campo de busca sem label visível ou aria-label.'
                });
            }
        });
        
        // Detecta widget de busca clássico
        const searchForms = document.querySelectorAll('.search-form, .searchform');
        searchForms.forEach((form, index) => {
            const input = form.querySelector('input[type="search"], input.s');
            if (!input) return;
            
            const hasLabel = form.querySelector('label') || input.getAttribute('aria-label');
            
            if (!hasLabel) {
                problematicSearches.push({
                    target: [getUniqueSelector(input)],
                    html: input.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: 'Widget de busca WordPress sem label.'
                });
            }
        });
        
        if (problematicSearches.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-search-widget-label',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag332', 'cat.forms', 'best-practice'],
            description: 'Campos de busca devem ter label descritivo (ex: "Buscar no site") para usuários de leitores de tela.',
            help: 'Widget de busca sem label adequado',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html',
            nodes: problematicSearches
        };
    };

    /**
     * Verifica blocos de HTML customizado (podem conter violações)
     * Alerta sobre riscos de acessibilidade em blocos Custom HTML
     */
    WallyMonitor.Rules.checkCustomHTMLBlocks = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicHTML = [];
        
        // Detecta blocos de HTML customizado Gutenberg
        const htmlBlocks = document.querySelectorAll('.wp-block-html');
        htmlBlocks.forEach((block, index) => {
            // Verifica se contém elementos interativos sem acessibilidade
            const hasButtons = block.querySelectorAll('button, input, select, textarea').length > 0;
            const hasImages = block.querySelectorAll('img').length > 0;
            const hasTables = block.querySelectorAll('table').length > 0;
            
            if (hasButtons || hasImages || hasTables) {
                problematicHTML.push({
                    target: [getUniqueSelector(block)],
                    html: block.outerHTML.substring(0, 200),
                    impact: 'moderate',
                    message: 'Bloco HTML customizado contém elementos interativos. Verifique manualmente a acessibilidade (labels, alt, etc.).'
                });
            }
        });
        
        if (problematicHTML.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-html-block-warning',
            impact: 'moderate',
            tags: ['best-practice', 'cat.structure'],
            description: 'Blocos de HTML customizado podem conter código inacessível. Revise manualmente formulários, imagens e tabelas inseridas.',
            help: 'Bloco HTML customizado requer verificação manual',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/quickref/',
            nodes: problematicHTML
        };
    };

    /**
     * Verifica embeds (YouTube, Spotify, TikTok, etc.) sem alternativa textual
     * Detecta iframes de embeds sem descrição ou title adequado
     */
    WallyMonitor.Rules.checkEmbedAccessibility = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicEmbeds = [];
        
        // Lista de embeds comuns do WordPress
        const embedSelectors = [
            '.wp-block-embed-youtube iframe',
            '.wp-block-embed-vimeo iframe',
            '.wp-block-embed-spotify iframe',
            '.wp-block-embed-tiktok iframe',
            '.wp-block-embed-soundcloud iframe',
            '.wp-block-embed-twitter iframe',
            'iframe[src*="youtube.com"]',
            'iframe[src*="vimeo.com"]',
            'iframe[src*="spotify.com"]',
            'iframe[src*="tiktok.com"]',
            'iframe[src*="soundcloud.com"]'
        ];
        
        embedSelectors.forEach(selector => {
            const embeds = document.querySelectorAll(selector);
            embeds.forEach((iframe, index) => {
                const hasTitle = iframe.getAttribute('title') && iframe.getAttribute('title').trim().length > 0;
                const hasAriaLabel = iframe.getAttribute('aria-label');
                
                if (!hasTitle && !hasAriaLabel) {
                    const src = iframe.getAttribute('src') || '';
                    let platform = 'Desconhecido';
                    if (src.includes('youtube')) platform = 'YouTube';
                    if (src.includes('vimeo')) platform = 'Vimeo';
                    if (src.includes('spotify')) platform = 'Spotify';
                    if (src.includes('tiktok')) platform = 'TikTok';
                    if (src.includes('soundcloud')) platform = 'SoundCloud';
                    if (src.includes('twitter')) platform = 'Twitter';
                    
                    problematicEmbeds.push({
                        target: [getUniqueSelector(iframe)],
                        html: iframe.outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: `Embed ${platform} sem atributo "title". Adicione title descritivo (ex: "Vídeo: Nome do vídeo").`
                    });
                }
            });
        });
        
        if (problematicEmbeds.length === 0) {
            return null;
        }
        
        return {
            id: 'custom-embed-no-title',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag412', 'cat.name-role-value', 'best-practice'],
            description: 'Iframes de embeds (YouTube, Spotify, etc.) devem ter atributo "title" descritivo para leitores de tela.',
            help: 'Embeds sem título descritivo',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html',
            nodes: problematicEmbeds
        };
    };

    /**
     * Verifica Social Links Gutenberg sem URL (links incompletos)
     * WordPress gera <a> sem href quando o usuário não preenche a URL do perfil social.
     * O link fica não-focável pelo teclado e sem destino funcional.
     */
    WallyMonitor.Rules.checkSocialLinksIncomplete = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicLinks = [];

        // Bloco Social Links do Gutenberg
        const socialLinks = document.querySelectorAll('.wp-social-link a, .wp-block-social-link a');
        socialLinks.forEach((link) => {
            const hasHref = link.hasAttribute('href') && link.getAttribute('href').trim().length > 0;
            const hasAriaLabel = link.getAttribute('aria-label') && link.getAttribute('aria-label').trim().length > 0;
            const hasText = link.textContent.trim().length > 0;
            const parentLi = link.closest('li');
            const service = parentLi
                ? (parentLi.className.match(/wp-social-link-(\S+)/) || [])[1] || 'desconhecido'
                : 'desconhecido';

            if (!hasHref) {
                problematicLinks.push({
                    target: [getUniqueSelector(parentLi || link)],
                    html: (parentLi || link).outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: `Link social "${service}" sem URL de destino (href). ` +
                            'O <a> sem href não é focável pelo teclado. ' +
                            'Adicione a URL do perfil ou remova o ícone.'
                });
            } else if (!hasAriaLabel && !hasText) {
                problematicLinks.push({
                    target: [getUniqueSelector(parentLi || link)],
                    html: (parentLi || link).outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: `Link social "${service}" sem texto ou aria-label. ` +
                            'Leitores de tela não conseguem anunciar o propósito do link.'
                });
            }
        });

        if (problematicLinks.length === 0) { return null; }

        return {
            id: 'custom-social-links-incomplete',
            impact: 'serious',
            tags: ['wcag2a', 'wcag21a', 'wcag244', 'wcag412', 'cat.name-role-value', 'best-practice'],
            description: 'Links sociais (ícones de redes sociais) devem ter URL de destino válida e texto acessível (aria-label).',
            help: 'Links sociais incompletos ou inacessíveis',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html',
            nodes: problematicLinks
        };
    };

    /**
     * Verifica blocos File/Download sem contexto descritivo acessível.
     * WordPress gera um botão de download genérico: <a class="wp-block-file__button" download>Download</a>
     * Sem aria-label descritivo, leitores de tela anunciam apenas "Download" sem contexto do arquivo.
     */
    WallyMonitor.Rules.checkFileBlockAccessibility = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicFiles = [];

        const fileBlocks = document.querySelectorAll('.wp-block-file');
        fileBlocks.forEach((block) => {
            const downloadBtn = block.querySelector('.wp-block-file__button, a[download]');
            const fileLink = block.querySelector('a:not(.wp-block-file__button)');

            if (downloadBtn) {
                const hasAriaLabel = downloadBtn.getAttribute('aria-label') && downloadBtn.getAttribute('aria-label').trim().length > 0;
                const hasAriaDescribedBy = downloadBtn.getAttribute('aria-describedby');
                const describedEl = hasAriaDescribedBy ? document.getElementById(hasAriaDescribedBy) : null;
                const hasDescribedText = describedEl && describedEl.textContent.trim().length > 0;
                const btnText = downloadBtn.textContent.trim().toLowerCase();
                const isGenericText = ['download', 'baixar', 'descarregar', ''].includes(btnText);

                // Se botão tem texto genérico e nenhuma referência descritiva
                if (isGenericText && !hasAriaLabel && !hasDescribedText) {
                    problematicFiles.push({
                        target: [getUniqueSelector(block)],
                        html: downloadBtn.outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: 'Botão de download com texto genérico ("' + downloadBtn.textContent.trim() + '") sem aria-label descritivo. ' +
                                'Leitores de tela anunciam apenas o texto do botão sem contexto do arquivo. ' +
                                'Adicione aria-label (ex: "Baixar relatório-2024.pdf").'
                    });
                }
            }

            // Verifica se PDF embed tem descrição acessível
            const pdfEmbed = block.querySelector('object[type="application/pdf"]');
            if (pdfEmbed) {
                const hasAriaLabel = pdfEmbed.getAttribute('aria-label') && pdfEmbed.getAttribute('aria-label').trim().length > 0;
                if (!hasAriaLabel) {
                    problematicFiles.push({
                        target: [getUniqueSelector(pdfEmbed)],
                        html: pdfEmbed.outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: 'Embed PDF sem aria-label descritivo. Leitores de tela não conseguem identificar o documento.'
                    });
                }
            }
        });

        if (problematicFiles.length === 0) { return null; }

        return {
            id: 'custom-file-block-accessibility',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag244', 'wcag412', 'cat.name-role-value', 'best-practice'],
            description: 'Botões de download devem ter texto ou aria-label que identifique o arquivo. "Download" genérico não é suficiente.',
            help: 'Bloco de arquivo/download sem identificação acessível',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html',
            nodes: problematicFiles
        };
    };

    /**
     * Verifica blocos Details/Accordion com summary genérico.
     * WordPress gera <details><summary>Details</summary>...</details> quando
     * o usuário não edita o texto do summary. O texto "Details" é genérico e
     * não fornece contexto sobre o conteúdo expandível.
     */
    WallyMonitor.Rules.checkDetailsBlockAccessibility = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicDetails = [];

        const detailsBlocks = document.querySelectorAll('details');
        detailsBlocks.forEach((details) => {
            const summary = details.querySelector('summary');

            if (!summary) {
                // Details sem summary é um problema grave — o browser gera um default
                problematicDetails.push({
                    target: [getUniqueSelector(details)],
                    html: details.outerHTML.substring(0, 150),
                    impact: 'serious',
                    message: 'Bloco <details> sem elemento <summary>. O browser gera um resumo padrão genérico ("Details").'
                });
            } else {
                const summaryText = summary.textContent.trim().toLowerCase();
                const genericTexts = ['details', 'detalhes', 'more', 'mais', 'click here', 'clique aqui', ''];

                if (genericTexts.includes(summaryText)) {
                    problematicDetails.push({
                        target: [getUniqueSelector(summary || details)],
                        html: summary.outerHTML.substring(0, 150),
                        impact: 'moderate',
                        message: `Bloco <details> com summary genérico ("${summary.textContent.trim() || '(vazio)'}"). ` +
                                'O summary deve descrever o conteúdo oculto para que leitores de tela possam anunciá-lo. ' +
                                'Ex: "Informações de contato", "Detalhes do produto".'
                    });
                }
            }
        });

        if (problematicDetails.length === 0) { return null; }

        return {
            id: 'custom-details-generic-summary',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag244', 'wcag131', 'cat.semantics', 'best-practice'],
            description: 'Blocos <details> devem ter <summary> descritivo do conteúdo expandível. Textos genéricos como "Details" não são informativos.',
            help: 'Bloco Details/Accordion com summary genérico',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
            nodes: problematicDetails
        };
    };

    /**
     * Verifica blocos Group com tagName semântico (section, aside) sem aria-label.
     * WordPress permite que blocos Group usem <section> ou <aside> como tag,
     * mas sem aria-label esses landmarks são genéricos e confusos para leitores de tela.
     * Também detecta <section> e <aside> genéricos em temas e plugins.
     */
    WallyMonitor.Rules.checkGenericLandmarks = function(document) {
        if (!document || typeof document.querySelectorAll !== 'function') { return null; }
        const problematicLandmarks = [];

        // Blocos Group com tagName semântico + <section>/<aside> genéricos
        const sections = document.querySelectorAll('section, aside');
        sections.forEach((el) => {
            const hasAriaLabel = el.getAttribute('aria-label') && el.getAttribute('aria-label').trim().length > 0;
            const hasAriaLabelledBy = el.getAttribute('aria-labelledby');
            const labelledByEl = hasAriaLabelledBy ? document.getElementById(hasAriaLabelledBy) : null;
            const hasLabelledByText = labelledByEl && labelledByEl.textContent.trim().length > 0;
            const hasRole = el.getAttribute('role');

            // Se o landmark foi explicitamente neutralizado (ex: role="presentation"), ignorar
            if (hasRole === 'presentation' || hasRole === 'none') { return; }

            if (!hasAriaLabel && !hasLabelledByText) {
                const tag = el.tagName.toLowerCase();
                const isGutenbergBlock = el.classList.contains('wp-block-group') ||
                                         el.classList.contains('wp-block-column') ||
                                         el.classList.contains('wp-block-columns');
                const contextMsg = isGutenbergBlock
                    ? `Bloco Group Gutenberg com tag <${tag}>`
                    : `Elemento <${tag}>`;
                problematicLandmarks.push({
                    target: [getUniqueSelector(el)],
                    html: el.outerHTML.substring(0, 150),
                    impact: 'moderate',
                    message: `${contextMsg} sem aria-label ou aria-labelledby. ` +
                            `O <${tag}> define um landmark "${tag === 'section' ? 'region' : 'complementary'}" ` +
                            'na árvore de acessibilidade. Sem label, leitores de tela anunciam ' +
                            `"${tag === 'section' ? 'region' : 'complementary'}" sem contexto. ` +
                            'Adicione aria-label descritivo ou use <div> se não for um landmark intencional.'
                });
            }
        });

        if (problematicLandmarks.length === 0) { return null; }

        return {
            id: 'custom-generic-landmark',
            impact: 'moderate',
            tags: ['wcag2a', 'wcag21a', 'wcag131', 'cat.semantics', 'best-practice'],
            description: 'Landmarks (section, aside) devem ter aria-label descritivo. Sem label, leitores de tela anunciam landmarks genéricos sem contexto.',
            help: 'Landmarks genéricos sem identificação acessível',
            helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html',
            nodes: problematicLandmarks
        };
    };

    /**
     * Convenience function that runs all custom checks and returns an array of violations (filtering out nulls).
     * @param {Document} document - The document to check
     * @returns {Array} Array of violation objects
     */
    WallyMonitor.Rules.runAllChecks = function(document) {
        var results = [
            WallyMonitor.Rules.checkEmptyAltImages(document),
            WallyMonitor.Rules.checkEmptyButtons(document),
            WallyMonitor.Rules.checkWordPressGalleries(document),
            WallyMonitor.Rules.checkMediaAccessibility(document),
            WallyMonitor.Rules.checkWordPressForms(document),
            WallyMonitor.Rules.checkHeadingHierarchy(document),
            WallyMonitor.Rules.checkTableAccessibility(document),
            WallyMonitor.Rules.checkCoverBlockContrast(document),
            WallyMonitor.Rules.checkNavigationStructure(document),
            WallyMonitor.Rules.checkSiteLogo(document),
            WallyMonitor.Rules.checkSearchWidget(document),
            WallyMonitor.Rules.checkCustomHTMLBlocks(document),
            WallyMonitor.Rules.checkEmbedAccessibility(document),
            WallyMonitor.Rules.checkSocialLinksIncomplete(document),
            WallyMonitor.Rules.checkFileBlockAccessibility(document),
            WallyMonitor.Rules.checkDetailsBlockAccessibility(document),
            WallyMonitor.Rules.checkGenericLandmarks(document)
        ];
        return results.filter(function(v) { return v !== null; });
    };

})(jQuery);
