initial commit

This commit is contained in:
2025-11-05 08:36:56 -05:00
commit 095aa2d112
5 changed files with 3993 additions and 0 deletions

925
vibe-brutalism.js Normal file
View File

@@ -0,0 +1,925 @@
/**
* VIBE BRUTALISM - JavaScript Component Library
* Interactive functionality for neo-brutalist components
* Version: 2.0.0
* WCAG 2.1 AA & Section 508 Compliant
*/
(function() {
'use strict';
// ============================================
// ACCESSIBILITY UTILITIES
// ============================================
const a11y = {
// Create ARIA live region for announcements
createLiveRegion() {
if (!document.getElementById('vb-live-region')) {
const liveRegion = document.createElement('div');
liveRegion.id = 'vb-live-region';
liveRegion.className = 'vb-sr-only';
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
document.body.appendChild(liveRegion);
}
return document.getElementById('vb-live-region');
},
// Announce message to screen readers
announce(message, priority = 'polite') {
const liveRegion = this.createLiveRegion();
liveRegion.setAttribute('aria-live', priority);
liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
},
// Trap focus within element
trapFocus(element) {
const focusableElements = element.querySelectorAll(
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return null;
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
const handler = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
};
element.addEventListener('keydown', handler);
firstFocusable.focus();
// Return cleanup function
return () => element.removeEventListener('keydown', handler);
}
};
// ============================================
// MODAL FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBModal {
constructor(element) {
this.modal = element;
this.closeButtons = this.modal.querySelectorAll('.vb-modal-close, [data-vb-modal-close]');
this.previousActiveElement = null;
this.focusTrapCleanup = null;
this.escHandler = this.handleEscape.bind(this);
this.clickHandler = this.handleBackdropClick.bind(this);
this.init();
}
init() {
// Set ARIA attributes
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-modal', 'true');
if (!this.modal.getAttribute('aria-labelledby')) {
const title = this.modal.querySelector('.vb-modal-title');
if (title && !title.id) {
title.id = `modal-title-${Math.random().toString(36).substr(2, 9)}`;
}
if (title) {
this.modal.setAttribute('aria-labelledby', title.id);
}
}
// Close button events
this.closeButtons.forEach(btn => {
btn.setAttribute('aria-label', 'Close dialog');
btn.addEventListener('click', () => this.close());
});
// Close on outside click
this.modal.addEventListener('click', this.clickHandler);
}
handleBackdropClick(e) {
if (e.target === this.modal) {
this.close();
}
}
handleEscape(e) {
if (e.key === 'Escape' && this.modal.classList.contains('active')) {
this.close();
}
}
open() {
this.previousActiveElement = document.activeElement;
this.modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Trap focus and store cleanup function
this.focusTrapCleanup = a11y.trapFocus(this.modal);
// Add ESC key handler
document.addEventListener('keydown', this.escHandler);
a11y.announce('Dialog opened');
}
close() {
this.modal.classList.remove('active');
document.body.style.overflow = '';
// Cleanup focus trap
if (this.focusTrapCleanup) {
this.focusTrapCleanup();
this.focusTrapCleanup = null;
}
// Remove ESC key handler
document.removeEventListener('keydown', this.escHandler);
// Return focus
if (this.previousActiveElement) {
this.previousActiveElement.focus();
}
a11y.announce('Dialog closed');
}
}
// Initialize all modals
const modals = {};
document.querySelectorAll('.vb-modal').forEach(modal => {
const id = modal.id;
if (id) {
modals[id] = new VBModal(modal);
}
});
// Modal trigger buttons
document.querySelectorAll('[data-vb-modal-target]').forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.vbModalTarget;
if (modals[targetId]) {
modals[targetId].open();
}
});
});
// ============================================
// HAMBURGER MENU FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBHamburgerMenu {
constructor(navbar) {
this.navbar = navbar;
this.toggle = navbar.querySelector('.vb-navbar-toggle');
this.menu = navbar.querySelector('.vb-navbar-menu');
this.escHandler = this.handleEscape.bind(this);
if (this.toggle && this.menu) {
this.init();
}
}
init() {
// Set ARIA attributes
this.toggle.setAttribute('aria-label', 'Toggle navigation menu');
this.toggle.setAttribute('aria-expanded', 'false');
this.toggle.setAttribute('aria-controls', this.menu.id || 'navbar-menu');
if (!this.menu.id) {
this.menu.id = 'navbar-menu';
}
// Toggle on click
this.toggle.addEventListener('click', () => {
this.toggleMenu();
});
// Close on ESC - using bound handler for proper cleanup
document.addEventListener('keydown', this.escHandler);
}
handleEscape(e) {
if (e.key === 'Escape' && this.menu.classList.contains('active')) {
this.closeMenu();
this.toggle.focus();
}
}
toggleMenu() {
const isOpen = this.menu.classList.toggle('active');
this.toggle.classList.toggle('active');
this.toggle.setAttribute('aria-expanded', isOpen);
a11y.announce(isOpen ? 'Menu opened' : 'Menu closed');
}
closeMenu() {
this.menu.classList.remove('active');
this.toggle.classList.remove('active');
this.toggle.setAttribute('aria-expanded', 'false');
}
}
// Initialize hamburger menus
document.querySelectorAll('.vb-navbar').forEach(navbar => {
new VBHamburgerMenu(navbar);
});
// ============================================
// DROPDOWN FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBDropdown {
constructor(element) {
this.dropdown = element;
this.toggle = this.dropdown.querySelector('.vb-dropdown-toggle');
this.menu = this.dropdown.querySelector('.vb-dropdown-menu');
if (!this.toggle || !this.menu) {
console.warn('Vibe Brutalism: Dropdown requires .vb-dropdown-toggle and .vb-dropdown-menu');
return;
}
this.items = this.menu.querySelectorAll('.vb-dropdown-item');
this.outsideClickHandler = this.handleOutsideClick.bind(this);
this.init();
}
init() {
// Set ARIA attributes
this.toggle.setAttribute('aria-haspopup', 'true');
this.toggle.setAttribute('aria-expanded', 'false');
this.menu.setAttribute('role', 'menu');
this.items.forEach(item => {
item.setAttribute('role', 'menuitem');
item.setAttribute('tabindex', '-1');
});
// Toggle on click
this.toggle.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDropdown();
});
// Keyboard navigation
this.toggle.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.open();
this.items[0]?.focus();
}
});
this.items.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
this.items[(index + 1) % this.items.length]?.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.items[(index - 1 + this.items.length) % this.items.length]?.focus();
} else if (e.key === 'Escape') {
e.preventDefault();
this.close();
this.toggle.focus();
}
});
});
// Prevent closing when clicking inside menu
this.menu.addEventListener('click', (e) => {
e.stopPropagation();
});
}
handleOutsideClick() {
this.close();
}
toggleDropdown() {
const isOpen = this.dropdown.classList.toggle('active');
this.toggle.setAttribute('aria-expanded', isOpen);
if (isOpen) {
this.items.forEach(item => item.setAttribute('tabindex', '0'));
// Add outside click handler when open
setTimeout(() => {
document.addEventListener('click', this.outsideClickHandler);
}, 0);
} else {
this.items.forEach(item => item.setAttribute('tabindex', '-1'));
// Remove outside click handler when closed
document.removeEventListener('click', this.outsideClickHandler);
}
}
open() {
this.dropdown.classList.add('active');
this.toggle.setAttribute('aria-expanded', 'true');
this.items.forEach(item => item.setAttribute('tabindex', '0'));
// Add outside click handler when open
setTimeout(() => {
document.addEventListener('click', this.outsideClickHandler);
}, 0);
}
close() {
this.dropdown.classList.remove('active');
this.toggle.setAttribute('aria-expanded', 'false');
this.items.forEach(item => item.setAttribute('tabindex', '-1'));
// Remove outside click handler when closed
document.removeEventListener('click', this.outsideClickHandler);
}
}
// Initialize all dropdowns
document.querySelectorAll('.vb-dropdown').forEach(dropdown => {
new VBDropdown(dropdown);
});
// ============================================
// TABS FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBTabs {
constructor(element) {
this.tabs = element;
this.buttons = this.tabs.querySelectorAll('.vb-tab-button');
this.contents = this.tabs.querySelectorAll('.vb-tab-content');
this.init();
}
init() {
// Set ARIA attributes
const tablist = this.tabs.querySelector('.vb-tab-list');
if (tablist) {
tablist.setAttribute('role', 'tablist');
}
this.buttons.forEach((button, index) => {
const tabId = `tab-${Math.random().toString(36).substr(2, 9)}`;
const panelId = `panel-${Math.random().toString(36).substr(2, 9)}`;
button.setAttribute('role', 'tab');
button.setAttribute('id', tabId);
button.setAttribute('aria-controls', panelId);
button.setAttribute('tabindex', '-1');
this.contents[index].setAttribute('role', 'tabpanel');
this.contents[index].setAttribute('id', panelId);
this.contents[index].setAttribute('aria-labelledby', tabId);
this.contents[index].setAttribute('tabindex', '0');
// Click event
button.addEventListener('click', () => {
this.switchTab(index);
});
// Keyboard navigation
button.addEventListener('keydown', (e) => {
let newIndex = index;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
newIndex = (index + 1) % this.buttons.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
newIndex = (index - 1 + this.buttons.length) % this.buttons.length;
} else if (e.key === 'Home') {
e.preventDefault();
newIndex = 0;
} else if (e.key === 'End') {
e.preventDefault();
newIndex = this.buttons.length - 1;
}
if (newIndex !== index) {
this.switchTab(newIndex);
this.buttons[newIndex].focus();
}
});
});
// Activate first tab by default
if (this.buttons.length > 0 && !this.tabs.querySelector('.vb-tab-button.active')) {
this.switchTab(0);
}
}
switchTab(index) {
// Remove active class and update ARIA
this.buttons.forEach((btn, i) => {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
btn.setAttribute('tabindex', '-1');
});
this.contents.forEach(content => {
content.classList.remove('active');
});
// Add active class to selected button and content
this.buttons[index].classList.add('active');
this.buttons[index].setAttribute('aria-selected', 'true');
this.buttons[index].setAttribute('tabindex', '0');
this.contents[index].classList.add('active');
a11y.announce(`Tab ${index + 1} selected`);
}
}
// Initialize all tabs
document.querySelectorAll('.vb-tabs').forEach(tabs => {
new VBTabs(tabs);
});
// ============================================
// ACCORDION FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBAccordion {
constructor(element) {
this.accordion = element;
this.items = this.accordion.querySelectorAll('.vb-accordion-item');
this.init();
}
init() {
this.items.forEach((item, index) => {
const header = item.querySelector('.vb-accordion-header');
const body = item.querySelector('.vb-accordion-body');
// Set ARIA attributes
const headerId = `accordion-header-${Math.random().toString(36).substr(2, 9)}`;
const bodyId = `accordion-body-${Math.random().toString(36).substr(2, 9)}`;
header.setAttribute('id', headerId);
header.setAttribute('aria-expanded', 'false');
header.setAttribute('aria-controls', bodyId);
body.setAttribute('id', bodyId);
body.setAttribute('role', 'region');
body.setAttribute('aria-labelledby', headerId);
// Click event
header.addEventListener('click', () => {
this.toggle(header, body);
});
// Keyboard support
header.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextHeader = this.items[index + 1]?.querySelector('.vb-accordion-header');
nextHeader?.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevHeader = this.items[index - 1]?.querySelector('.vb-accordion-header');
prevHeader?.focus();
} else if (e.key === 'Home') {
e.preventDefault();
this.items[0]?.querySelector('.vb-accordion-header')?.focus();
} else if (e.key === 'End') {
e.preventDefault();
this.items[this.items.length - 1]?.querySelector('.vb-accordion-header')?.focus();
}
});
});
}
toggle(header, body) {
const isActive = header.classList.contains('active');
// Close all items
this.accordion.querySelectorAll('.vb-accordion-header').forEach(h => {
h.classList.remove('active');
h.setAttribute('aria-expanded', 'false');
});
this.accordion.querySelectorAll('.vb-accordion-body').forEach(b => {
b.classList.remove('active');
});
// Toggle current item
if (!isActive) {
header.classList.add('active');
header.setAttribute('aria-expanded', 'true');
body.classList.add('active');
a11y.announce('Section expanded');
} else {
a11y.announce('Section collapsed');
}
}
}
// Initialize all accordions
document.querySelectorAll('.vb-accordion').forEach(accordion => {
new VBAccordion(accordion);
});
// ============================================
// CAROUSEL FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBCarousel {
constructor(element) {
this.carousel = element;
this.track = this.carousel.querySelector('.vb-carousel-track');
this.items = this.carousel.querySelectorAll('.vb-carousel-item');
this.prevBtn = this.carousel.querySelector('.vb-carousel-control-prev');
this.nextBtn = this.carousel.querySelector('.vb-carousel-control-next');
this.indicators = this.carousel.querySelectorAll('.vb-carousel-indicator');
this.currentIndex = 0;
this.autoplayInterval = null;
this.touchStartX = 0;
this.touchEndX = 0;
this.init();
}
init() {
if (this.items.length === 0 || !this.track) {
console.warn('Vibe Brutalism: Carousel requires .vb-carousel-track and at least one .vb-carousel-item');
return;
}
// Set ARIA attributes
this.carousel.setAttribute('role', 'region');
this.carousel.setAttribute('aria-label', 'Carousel');
this.carousel.setAttribute('aria-roledescription', 'carousel');
this.items.forEach((item, index) => {
item.setAttribute('role', 'group');
item.setAttribute('aria-roledescription', 'slide');
item.setAttribute('aria-label', `Slide ${index + 1} of ${this.items.length}`);
});
// Previous button
if (this.prevBtn) {
this.prevBtn.setAttribute('aria-label', 'Previous slide');
this.prevBtn.addEventListener('click', () => this.prev());
}
// Next button
if (this.nextBtn) {
this.nextBtn.setAttribute('aria-label', 'Next slide');
this.nextBtn.addEventListener('click', () => this.next());
}
// Indicators
this.indicators.forEach((indicator, index) => {
indicator.setAttribute('aria-label', `Go to slide ${index + 1}`);
indicator.setAttribute('role', 'button');
indicator.addEventListener('click', () => this.goTo(index));
});
// Keyboard navigation
this.carousel.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
this.prev();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
this.next();
}
});
// Pause autoplay on hover/focus
this.carousel.addEventListener('mouseenter', () => this.pauseAutoplay());
this.carousel.addEventListener('mouseleave', () => this.startAutoplay());
this.carousel.addEventListener('focusin', () => this.pauseAutoplay());
this.carousel.addEventListener('focusout', () => this.startAutoplay());
// Touch/Swipe support
this.carousel.addEventListener('touchstart', (e) => {
this.touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
this.carousel.addEventListener('touchend', (e) => {
this.touchEndX = e.changedTouches[0].screenX;
this.handleSwipe();
}, { passive: true });
// Initialize
this.goTo(0);
// Start autoplay if enabled
if (this.carousel.dataset.autoplay === 'true') {
this.startAutoplay();
}
}
handleSwipe() {
const swipeThreshold = 50;
const diff = this.touchStartX - this.touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
// Swipe left - go to next
this.next();
} else {
// Swipe right - go to previous
this.prev();
}
}
}
goTo(index) {
this.currentIndex = index;
const offset = -index * 100;
this.track.style.transform = `translateX(${offset}%)`;
// Update indicators
this.indicators.forEach((indicator, i) => {
indicator.classList.toggle('active', i === index);
indicator.setAttribute('aria-current', i === index ? 'true' : 'false');
});
// Update items
this.items.forEach((item, i) => {
item.setAttribute('aria-hidden', i !== index);
});
a11y.announce(`Slide ${index + 1} of ${this.items.length}`);
}
next() {
const nextIndex = (this.currentIndex + 1) % this.items.length;
this.goTo(nextIndex);
}
prev() {
const prevIndex = (this.currentIndex - 1 + this.items.length) % this.items.length;
this.goTo(prevIndex);
}
startAutoplay() {
const interval = parseInt(this.carousel.dataset.interval) || 5000;
this.autoplayInterval = setInterval(() => this.next(), interval);
}
pauseAutoplay() {
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
this.autoplayInterval = null;
}
}
}
// Initialize all carousels
const carousels = {};
document.querySelectorAll('.vb-carousel').forEach(carousel => {
const id = carousel.id || `carousel-${Math.random().toString(36).substr(2, 9)}`;
carousel.id = id;
carousels[id] = new VBCarousel(carousel);
});
// ============================================
// TOAST NOTIFICATIONS (WCAG 2.1 AA)
// ============================================
class VBToastManager {
constructor() {
this.container = null;
this.toasts = [];
}
getContainer(position = 'top-right') {
const containerId = `vb-toast-container-${position}`;
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.className = `vb-toast-container ${position}`;
container.setAttribute('aria-live', 'polite');
container.setAttribute('aria-atomic', 'false');
document.body.appendChild(container);
}
return container;
}
show(message, type = 'info', duration = 5000, position = 'top-right') {
const container = this.getContainer(position);
const toast = document.createElement('div');
toast.className = `vb-toast vb-toast-${type}`;
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
// Icon
const icons = {
success: '✓',
warning: '⚠',
danger: '✕',
info: ''
};
toast.innerHTML = `
<span class="vb-toast-icon" aria-hidden="true">${icons[type] || icons.info}</span>
<div class="vb-toast-content">${message}</div>
<button class="vb-toast-close" aria-label="Close notification">×</button>
`;
// Close button
const closeBtn = toast.querySelector('.vb-toast-close');
closeBtn.addEventListener('click', () => this.remove(toast));
container.appendChild(toast);
this.toasts.push(toast);
// Announce to screen readers
a11y.announce(message, 'polite');
// Auto remove
if (duration > 0) {
setTimeout(() => this.remove(toast), duration);
}
return toast;
}
remove(toast) {
toast.classList.add('removing');
setTimeout(() => {
toast.remove();
const index = this.toasts.indexOf(toast);
if (index > -1) {
this.toasts.splice(index, 1);
}
}, 300);
}
}
const toastManager = new VBToastManager();
// ============================================
// SNACKBAR FUNCTIONALITY (WCAG 2.1 AA)
// ============================================
class VBSnackbar {
constructor() {
this.snackbar = null;
this.timeout = null;
}
show(message, actionText = null, actionCallback = null, duration = 5000) {
// Remove existing snackbar
if (this.snackbar) {
this.hide();
}
// Create snackbar
this.snackbar = document.createElement('div');
this.snackbar.className = 'vb-snackbar';
this.snackbar.setAttribute('role', 'status');
this.snackbar.setAttribute('aria-live', 'polite');
let html = `<div class="vb-snackbar-message">${message}</div>`;
if (actionText && actionCallback) {
html += `<button class="vb-snackbar-action">${actionText}</button>`;
}
html += `<button class="vb-snackbar-close" aria-label="Close">×</button>`;
this.snackbar.innerHTML = html;
// Action button
if (actionText && actionCallback) {
const actionBtn = this.snackbar.querySelector('.vb-snackbar-action');
actionBtn.addEventListener('click', () => {
actionCallback();
this.hide();
});
}
// Close button
const closeBtn = this.snackbar.querySelector('.vb-snackbar-close');
closeBtn.addEventListener('click', () => this.hide());
document.body.appendChild(this.snackbar);
// Show with animation
setTimeout(() => {
this.snackbar.classList.add('show');
}, 10);
// Announce to screen readers
a11y.announce(message, 'polite');
// Auto hide
if (duration > 0) {
this.timeout = setTimeout(() => this.hide(), duration);
}
}
hide() {
if (this.snackbar) {
this.snackbar.classList.remove('show');
setTimeout(() => {
this.snackbar?.remove();
this.snackbar = null;
}, 300);
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
const snackbar = new VBSnackbar();
// ============================================
// ALERT CLOSE FUNCTIONALITY
// ============================================
document.querySelectorAll('[data-vb-alert-close]').forEach(btn => {
btn.addEventListener('click', () => {
const alert = btn.closest('.vb-alert');
if (alert) {
alert.style.opacity = '0';
setTimeout(() => {
alert.remove();
}, 300);
}
});
});
// ============================================
// PROGRESS BAR
// ============================================
window.VBSetProgress = function(elementId, percentage) {
const progressBar = document.querySelector(`#${elementId} .vb-progress-bar`);
if (progressBar) {
progressBar.style.width = `${percentage}%`;
progressBar.textContent = `${percentage}%`;
progressBar.setAttribute('aria-valuenow', percentage);
a11y.announce(`Progress: ${percentage}%`);
}
};
// ============================================
// FORM VALIDATION
// ============================================
window.VBValidateForm = function(formElement) {
const inputs = formElement.querySelectorAll('.vb-input[required], .vb-textarea[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.style.borderColor = 'var(--vb-danger)';
input.setAttribute('aria-invalid', 'true');
isValid = false;
} else {
input.style.borderColor = 'var(--vb-black)';
input.setAttribute('aria-invalid', 'false');
}
});
return isValid;
};
// ============================================
// GLOBAL API
// ============================================
window.VB = {
modals,
carousels,
Toast: (message, type, duration, position) => toastManager.show(message, type, duration, position),
Snackbar: (message, actionText, actionCallback, duration) => snackbar.show(message, actionText, actionCallback, duration),
SetProgress: window.VBSetProgress,
ValidateForm: window.VBValidateForm,
announce: (message, priority) => a11y.announce(message, priority)
};
// Initialize accessibility features
if (document.body) {
a11y.createLiveRegion();
console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!');
} else {
console.warn('Vibe Brutalism: document.body not available. Retrying on DOMContentLoaded...');
document.addEventListener('DOMContentLoaded', () => {
a11y.createLiveRegion();
console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!');
});
}
})();