/**
 * GoValid QR admin scripts.
 *
 * @package GoValid_QR
 */

( function ( $ ) {
	'use strict';

	/**
	 * Copy shortcode to clipboard.
	 */
	$( document ).on( 'click', '.govalid-copy-shortcode', function ( e ) {
		e.preventDefault();
		var text = $( this ).data( 'clipboard' );
		if ( ! text ) {
			text = $( '#govalid-shortcode-output' ).text();
		}

		if ( navigator.clipboard && navigator.clipboard.writeText ) {
			navigator.clipboard.writeText( text ).then( function () {
				showNotice( govalidQR.i18n.copied );
			} );
		} else {
			// Fallback for older browsers.
			var $temp = $( '<textarea>' ).val( text ).appendTo( 'body' ).select();
			document.execCommand( 'copy' );
			$temp.remove();
			showNotice( govalidQR.i18n.copied );
		}
	} );

	/**
	 * Tab switching on generator page.
	 */
	$( document ).on( 'click', '.govalid-tab-link', function ( e ) {
		e.preventDefault();
		var tab = $( this ).data( 'tab' );

		// Update active tab link.
		$( '.govalid-tab-link' ).removeClass( 'active' );
		$( this ).addClass( 'active' );

		// Show the matching panel.
		$( '.govalid-tab-panel' ).removeClass( 'active' );
		$( '#govalid-tab-' + tab ).addClass( 'active' );

		// Update URL hash without scrolling.
		if ( history.replaceState ) {
			history.replaceState( null, null, '#' + tab );
		}
	} );

	// Restore tab from URL hash on load.
	if ( window.location.hash ) {
		var hashTab = window.location.hash.substring( 1 );
		var $link = $( '.govalid-tab-link[data-tab="' + hashTab + '"]' );
		if ( $link.length ) {
			$link.trigger( 'click' );
		}
	}

	/**
	 * Disable generate buttons when not connected to GoValid.
	 */
	if ( ! govalidQR.isConnected && $( '#generate-certificate-btn' ).length ) {
		$( '#generate-certificate-btn, #generate-product-btn, #generate-timeline-btn, #generate-document-btn' )
			.prop( 'disabled', true )
			.css( { opacity: 0.5, cursor: 'not-allowed' } )
			.attr( 'title', 'Connect to GoValid to generate QR codes' );
	}

	/**
	 * Load existing anti-counterfeit tags as reusable chips.
	 */
	if ( $( '#existing_tags_cert' ).length ) {
		$.ajax( {
			url: govalidQR.restUrl + 'tags',
			method: 'GET',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( resp ) {
				var tags = ( resp.data && resp.data.tags ) || [];
				if ( tags.length ) {
					var $container = $( '#existing_tags_cert .govalid-tag-chips' );
					$.each( tags, function ( i, tag ) {
						$container.append(
							'<button type="button" class="govalid-tag-chip" data-tag="' + $( '<span>' ).text( tag ).html() + '" data-input="anti_counterfeit_tags_cert">' +
							'<span class="dashicons dashicons-plus-alt2" style="font-size:13px;width:13px;height:13px;line-height:13px;"></span> ' +
							$( '<span>' ).text( tag ).html() +
							'</button>'
						);
					} );
					$( '#existing_tags_cert' ).show();
				}
			},
		} );
	}

	/**
	 * Click existing tag chip to add to input (handled in timeline section with data-input support).
	 */

	/**
	 * Load QR codes list on List QR page.
	 */
	var $qrList = $( '#govalid-qr-list' );
	var qrListPage = 1;
	var qrListPages = 1;
	var qrPerPage = 15;
	if ( $qrList.length && govalidQR.isConnected ) {
		loadQRCodes();
	}

	function loadQRCodes( page ) {
		page = page || 1;
		$qrList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.loading_qr || 'Loading QR codes...' ) + '</p>' );
		var searchVal = $( '#govalid-qr-search' ).val() || '';
		$.ajax( {
			url: govalidQR.restUrl + 'qr-codes',
			method: 'GET',
			data: { page: page, per_page: qrPerPage, search: searchVal },
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( resp ) {
				// Django returns { success, data: { qr_codes: [...], pagination: {...} } }
				var items = [];
				var pagination = null;
				if ( resp && resp.data && Array.isArray( resp.data.qr_codes ) ) {
					items = resp.data.qr_codes;
					pagination = resp.data.pagination || {};
				} else if ( Array.isArray( resp ) ) {
					items = resp;
				}

				if ( ! items.length ) {
					$qrList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.no_qr || 'No QR codes generated from WordPress yet.' ) + '</p>' );
					$( '#govalid-qr-pagination' ).hide();
					return;
				}

				qrListPage = pagination ? ( pagination.page || 1 ) : 1;
				qrListPages = pagination ? ( pagination.pages || 1 ) : 1;
				renderQRList( items );
				renderQRPagination();
			},
			error: function () {
				$qrList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.error ) + '</p>' );
			},
		} );
	}

	function renderQRList( items ) {
		var html = '';
		items.forEach( function ( qr ) {
			var uuid = qr.uuid || qr.qr_id || '';
			var name = qr.qr_name || qr.name || 'Untitled';
			var type = qr.qr_type || '';
			var level = qr.security_level || '';
			var scans = qr.scan_count || 0;
			var formattedUuid = qr.formatted_uuid || uuid;
			var imageUrl = qr.qr_image_url || qr.image_url || '';
			if ( imageUrl && imageUrl.charAt( 0 ) === '/' ) {
				imageUrl = ( govalidQR.baseUrl || '' ) + imageUrl;
			}
			var verifyUrl = qr.verification_url || '';
			var date = qr.created_at ? new Date( qr.created_at ).toLocaleDateString() : '';

			html += '<div class="govalid-qr-list-item" data-uuid="' + escapeAttr( uuid ) + '">';

			// Checkbox
			html += '<label class="govalid-qr-checkbox-wrap"><input type="checkbox" class="govalid-qr-checkbox" value="' + escapeAttr( uuid ) + '" /><span class="govalid-qr-checkmark"></span></label>';

			// QR image or placeholder
			if ( imageUrl ) {
				html += '<img src="' + escapeAttr( imageUrl ) + '" alt="' + escapeAttr( name ) + '" class="govalid-qr-list-img govalid-qr-preview-trigger" data-full-src="' + escapeAttr( imageUrl ) + '" data-qr-name="' + escapeAttr( name ) + '" style="cursor:pointer;" />';
			} else {
				html += '<div class="govalid-qr-list-placeholder"><span class="dashicons dashicons-screenoptions"></span></div>';
			}

			html += '<div class="govalid-qr-list-item-info">';
			html += '<div class="govalid-qr-list-item-name">' + escapeHtml( name ) + '</div>';
			html += '<div class="govalid-qr-list-item-detail">';
			if ( type ) {
				html += '<span class="govalid-qr-badge govalid-qr-badge-' + escapeAttr( type.toLowerCase() ) + '">' + escapeHtml( type ) + '</span>';
			}
			if ( level ) {
				html += '<span class="govalid-qr-badge govalid-qr-badge-level">' + escapeHtml( level ) + '</span>';
			}
			if ( qr.password_protected && qr.password_type ) {
				var pwLabel = qr.password_type === 'unique' ? 'Unique Password' : 'Master Password';
				html += '<span class="govalid-qr-badge govalid-qr-badge-pw"><span class="dashicons dashicons-lock"></span> ' + escapeHtml( pwLabel ) + '</span>';
			}
			html += '<span class="govalid-qr-scans"><span class="dashicons dashicons-chart-bar"></span> ' + scans + '</span>';
			if ( date ) {
				html += '<span class="govalid-qr-date">' + escapeHtml( date ) + '</span>';
			}
			html += '</div>';
			html += '<div class="govalid-qr-list-item-meta">';
			html += '<span class="govalid-qr-code-badge">' + escapeHtml( formattedUuid ) + '</span>';
			html += '<button type="button" class="button button-small govalid-copy-shortcode" data-clipboard="'
				+ escapeAttr( formattedUuid ) + '" title="' + escapeAttr( govalidQR.i18n.copy || 'Copy' ) + '"><span class="dashicons dashicons-admin-page"></span></button>';
			if ( verifyUrl ) {
				html += ' <a href="' + escapeAttr( verifyUrl ) + '" target="_blank" class="button button-small"><span class="dashicons dashicons-visibility" style="font-size:14px;width:14px;height:14px;vertical-align:middle;margin-right:2px;"></span>'
					+ escapeHtml( govalidQR.i18n.verify || 'Verify' ) + '</a>';
			}
			html += '</div>';
			html += '</div>';

			// Delete button
			html += '<button type="button" class="govalid-action-btn govalid-action-btn--danger govalid-qr-delete" data-uuid="' + escapeAttr( uuid ) + '" title="' + escapeAttr( govalidQR.i18n.delete || 'Delete' ) + '"><span class="dashicons dashicons-trash"></span></button>';

			html += '</div>';
		} );
		$qrList.html( html );
		updateBulkDeleteVisibility();
	}

	function renderQRPagination() {
		var $pag = $( '#govalid-qr-pagination' );
		var html = '';

		// Per-page selector
		html += '<span class="govalid-qr-per-page-wrap">';
		html += '<select id="govalid-qr-per-page" class="govalid-qr-per-page">';
		[ 15, 25, 50 ].forEach( function ( n ) {
			html += '<option value="' + n + '"' + ( n === qrPerPage ? ' selected' : '' ) + '>' + n + '</option>';
		} );
		html += '</select> / page';
		html += '</span>';

		if ( qrListPages > 1 ) {
			html += '<button type="button" class="govalid-btn govalid-btn-sm govalid-qr-page-btn" data-page="' + ( qrListPage - 1 ) + '"' + ( qrListPage <= 1 ? ' disabled' : '' ) + '>&laquo;</button> ';
			html += '<span class="govalid-qr-page-info">' + qrListPage + ' / ' + qrListPages + '</span> ';
			html += '<button type="button" class="govalid-btn govalid-btn-sm govalid-qr-page-btn" data-page="' + ( qrListPage + 1 ) + '"' + ( qrListPage >= qrListPages ? ' disabled' : '' ) + '>&raquo;</button>';
		}

		$pag.html( html ).show();
	}

	$( document ).on( 'click', '.govalid-qr-page-btn', function () {
		var p = parseInt( $( this ).data( 'page' ), 10 );
		if ( p >= 1 && p <= qrListPages ) {
			loadQRCodes( p );
		}
	} );

	$( document ).on( 'change', '#govalid-qr-per-page', function () {
		qrPerPage = parseInt( $( this ).val(), 10 );
		loadQRCodes( 1 );
	} );

	// Search
	var qrSearchTimer;
	$( document ).on( 'input', '#govalid-qr-search', function () {
		clearTimeout( qrSearchTimer );
		qrSearchTimer = setTimeout( function () { loadQRCodes( 1 ); }, 400 );
	} );

	/* =========================================================
	   QR Delete — single & bulk
	   ========================================================= */

	/**
	 * Single delete — trash icon on each row.
	 */
	$( document ).on( 'click', '.govalid-qr-delete', function () {
		if ( ! confirm( govalidQR.i18n.confirm_delete_qr || 'Are you sure you want to delete this QR code?' ) ) {
			return;
		}
		var $btn = $( this );
		var uuid = $btn.data( 'uuid' );
		$btn.prop( 'disabled', true );

		$.ajax( {
			url: govalidQR.restUrl + 'qr-codes/' + encodeURIComponent( uuid ),
			method: 'DELETE',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function () {
				showNotice( govalidQR.i18n.qr_deleted || 'QR code deleted.' );
				loadQRCodes( qrListPage );
			},
			error: function () {
				showNotice( govalidQR.i18n.error, 'error' );
				$btn.prop( 'disabled', false );
			},
		} );
	} );

	/**
	 * Select All checkbox.
	 */
	$( document ).on( 'change', '#govalid-qr-select-all', function () {
		var checked = $( this ).is( ':checked' );
		$( '.govalid-qr-checkbox' ).prop( 'checked', checked );
		updateBulkDeleteVisibility();
	} );

	/**
	 * Individual checkbox — update Select All and bulk button visibility.
	 */
	$( document ).on( 'change', '.govalid-qr-checkbox', function () {
		var total = $( '.govalid-qr-checkbox' ).length;
		var checked = $( '.govalid-qr-checkbox:checked' ).length;
		$( '#govalid-qr-select-all' ).prop( 'checked', total > 0 && checked === total );
		updateBulkDeleteVisibility();
	} );

	/**
	 * Show/hide bulk delete button based on selected count.
	 */
	function updateBulkDeleteVisibility() {
		var checked = $( '.govalid-qr-checkbox:checked' ).length;
		if ( checked > 0 ) {
			$( '#govalid-qr-bulk-delete' ).show().find( '.govalid-bulk-count' ).text( checked );
			$( '#govalid-qr-label-layout-btn' ).show().find( '.govalid-bulk-count' ).text( checked );
		} else {
			$( '#govalid-qr-bulk-delete' ).hide();
			$( '#govalid-qr-label-layout-btn' ).hide();
		}
	}

	/* =========================================================
	   Export Excel — fetch all QR codes and generate .xlsx
	   ========================================================= */
	$( document ).on( 'click', '#govalid-qr-export-excel', function () {
		if ( typeof XLSX === 'undefined' ) {
			alert( 'SheetJS library not loaded.' );
			return;
		}

		var $btn = $( this );
		var originalHtml = $btn.html();
		$btn.prop( 'disabled', true ).html(
			'<span class="dashicons dashicons-update govalid-spin"></span> ' + escapeHtml( govalidQR.i18n.exporting || 'Exporting...' )
		);

		var allItems = [];
		var searchVal = $( '#govalid-qr-search' ).val() || '';

		function fetchPage( page ) {
			$.ajax( {
				url: govalidQR.restUrl + 'qr-codes',
				method: 'GET',
				data: { page: page, per_page: 50, search: searchVal },
				beforeSend: function ( xhr ) {
					xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
				},
				success: function ( resp ) {
					var items = [];
					var pagination = null;
					if ( resp && resp.data && Array.isArray( resp.data.qr_codes ) ) {
						items = resp.data.qr_codes;
						pagination = resp.data.pagination || {};
					} else if ( Array.isArray( resp ) ) {
						items = resp;
					}

					allItems = allItems.concat( items );

					var totalPages = pagination ? ( pagination.pages || 1 ) : 1;
					if ( page < totalPages ) {
						fetchPage( page + 1 );
					} else {
						buildAndDownload( allItems );
						$btn.prop( 'disabled', false ).html( originalHtml );
					}
				},
				error: function () {
					alert( govalidQR.i18n.error || 'Failed to fetch QR codes.' );
					$btn.prop( 'disabled', false ).html( originalHtml );
				},
			} );
		}

		function buildAndDownload( items ) {
			var rows = items.map( function ( qr ) {
				return {
					'Name':               qr.qr_name || qr.name || '',
					'Type':               qr.qr_type || '',
					'Security Level':     qr.security_level || '',
					'Formatted ID':       qr.formatted_uuid || qr.uuid || qr.qr_id || '',
					'Scans':              qr.scan_count || 0,
					'Created Date':       qr.created_at ? new Date( qr.created_at ).toLocaleDateString() : '',
					'Password Protected': qr.password_protected ? 'Yes' : 'No',
					'Password Type':      qr.password_type || '',
					'Verification URL':   qr.verification_url || '',
				};
			} );

			var ws = XLSX.utils.json_to_sheet( rows );

			/* Set column widths */
			ws['!cols'] = [
				{ wch: 30 }, /* Name */
				{ wch: 14 }, /* Type */
				{ wch: 16 }, /* Security Level */
				{ wch: 22 }, /* Formatted ID */
				{ wch: 8 },  /* Scans */
				{ wch: 14 }, /* Created Date */
				{ wch: 18 }, /* Password Protected */
				{ wch: 16 }, /* Password Type */
				{ wch: 40 }, /* Verification URL */
			];

			var wb = XLSX.utils.book_new();
			XLSX.utils.book_append_sheet( wb, ws, 'QR Codes' );

			var today = new Date().toISOString().slice( 0, 10 );
			XLSX.writeFile( wb, 'govalid-qr-codes-' + today + '.xlsx' );
		}

		fetchPage( 1 );
	} );

	/**
	 * Bulk delete — sequential AJAX calls.
	 */
	$( document ).on( 'click', '#govalid-qr-bulk-delete', function () {
		var $checked = $( '.govalid-qr-checkbox:checked' );
		var count = $checked.length;
		if ( ! count ) {
			return;
		}

		if ( ! confirm( ( govalidQR.i18n.confirm_bulk_delete_qr || 'Delete {count} selected QR code(s)?' ).replace( '{count}', count ) ) ) {
			return;
		}

		var $btn = $( this );
		$btn.prop( 'disabled', true ).html(
			'<span class="dashicons dashicons-update govalid-spin"></span> ' + escapeHtml( govalidQR.i18n.deleting || 'Deleting...' )
		);

		var uuids = [];
		$checked.each( function () {
			uuids.push( $( this ).val() );
		} );

		var completed = 0;
		var errors = 0;

		function deleteNext() {
			if ( completed >= uuids.length ) {
				var msg = errors
					? ( ( completed - errors ) + ' deleted, ' + errors + ' failed.' )
					: ( govalidQR.i18n.bulk_deleted || 'Selected QR codes deleted.' );
				showNotice( msg, errors ? 'error' : undefined );
				loadQRCodes( qrListPage );
				$btn.prop( 'disabled', false ).html(
					'<span class="dashicons dashicons-trash"></span> ' + escapeHtml( govalidQR.i18n.delete_selected || 'Delete Selected' ) + ' (<span class="govalid-bulk-count">0</span>)'
				);
				return;
			}

			$.ajax( {
				url: govalidQR.restUrl + 'qr-codes/' + encodeURIComponent( uuids[ completed ] ),
				method: 'DELETE',
				beforeSend: function ( xhr ) {
					xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
				},
				error: function () {
					errors++;
				},
				complete: function () {
					completed++;
					deleteNext();
				},
			} );
		}

		deleteNext();
	} );

	/**
	 * Label Layout — collect selected QR data and open layout modal.
	 */
	$( document ).on( 'click', '#govalid-qr-label-layout-btn', function () {
		var selected = [];
		$( '.govalid-qr-checkbox:checked' ).each( function () {
			var $item = $( this ).closest( '.govalid-qr-list-item' );
			selected.push( {
				uuid:         $( this ).val(),
				name:         $item.find( '.govalid-qr-list-item-name' ).text().trim(),
				formatted_id: $item.find( '.govalid-qr-code-badge' ).text().trim(),
				image_url:    $item.find( '.govalid-qr-list-img' ).attr( 'src' ) || '',
			} );
		} );
		if ( selected.length && typeof window.govalidOpenLabelLayout === 'function' ) {
			window.govalidOpenLabelLayout( selected );
		}
	} );

	/**
	 * Label Layout link in ?created notice — select all and open layout.
	 */
	$( document ).on( 'click', '#govalid-qr-layout-created', function ( e ) {
		e.preventDefault();
		$( '#govalid-qr-select-all' ).prop( 'checked', true ).trigger( 'change' );
		setTimeout( function () {
			$( '#govalid-qr-label-layout-btn' ).trigger( 'click' );
		}, 100 );
	} );

	function showNotice( msg, type ) {
		var cls = 'govalid-toast';
		if ( type === 'error' ) {
			cls += ' govalid-toast-error';
		}
		var $notice = $( '<div class="' + cls + '">' + escapeHtml( msg ) + '</div>' );
		$( 'body' ).append( $notice );
		// Trigger reflow then add visible class for animation.
		$notice[0].offsetHeight;
		$notice.addClass( 'visible' );
		setTimeout( function () {
			$notice.removeClass( 'visible' );
			setTimeout( function () { $notice.remove(); }, 300 );
		}, 3000 );
	}

	/* =========================================================
	   Form interactions (Generator page)
	   ========================================================= */

	/**
	 * Security level card selection.
	 */
	var securityDescriptions = {
		SMART: govalidQR.i18n.smart_qr_desc || 'Compact QR code with 96-bit security. Perfect for tiny labels and product tags. Smallest QR code size.',
		SECURE: govalidQR.i18n.secure_qr_desc || '256-bit encryption protects your data from unauthorized changes and tampering. Balanced QR code size.',
		ENTERPRISE: govalidQR.i18n.enterprise_qr_desc || '256-bit encryption with digital signatures and non-repudiation guarantee. Tamper-proof verification with legally binding proof of origin. Large QR code size.',
	};

	$( document ).on( 'change', '.govalid-security-option input[type="radio"]', function () {
		var $cards = $( this ).closest( '.govalid-security-cards' );
		$cards.find( '.govalid-security-option' ).removeClass( 'active' );
		$( this ).closest( '.govalid-security-option' ).addClass( 'active' );

		var level = $( this ).val();
		var $desc = $cards.siblings( '.govalid-security-desc' );
		if ( $desc.length && securityDescriptions[ level ] ) {
			$desc.text( securityDescriptions[ level ] );
		}
	} );

	/**
	 * Field toggle switches — show/hide toggle bodies.
	 */
	$( document ).on( 'change', '.govalid-field-toggle', function () {
		var targetId = $( this ).data( 'target' );
		var $body = $( '#' + targetId );
		if ( $( this ).is( ':checked' ) ) {
			$body.slideDown( 200 ).removeClass( 'disabled' );
			$body.find( 'input, textarea, select' ).prop( 'disabled', false );
		} else {
			$body.slideUp( 200 ).addClass( 'disabled' );
			$body.find( 'input, textarea, select' ).prop( 'disabled', true );
		}
	} );

	/**
	 * Password protection toggle — sync hidden field.
	 */
	$( document ).on( 'change', '[id^="toggle_password_"]', function () {
		var formId = $( this ).closest( '.govalid-form' ).attr( 'id' ) || '';
		var suffix = formId.replace( 'govalid-form-', '' );
		$( '#hidden_is_password_protected_' + suffix ).val( $( this ).is( ':checked' ) ? 'true' : 'false' );
	} );

	/**
	 * Password type radio — show/hide unique password fields.
	 */
	$( document ).on( 'change', 'input[name="password_type"]', function () {
		var $form = $( this ).closest( '.govalid-form' );
		var $container = $form.find( '[id^="unique_password_container_"]' );
		if ( $( this ).val() === 'unique' ) {
			$container.slideDown( 200 );
		} else {
			$container.slideUp( 200 );
		}
	} );

	/**
	 * Toggle password visibility.
	 */
	$( document ).on( 'click', '.govalid-toggle-password', function () {
		var $input = $( this ).siblings( 'input' );
		var $icon = $( this ).find( '.dashicons' );
		if ( $input.attr( 'type' ) === 'password' ) {
			$input.attr( 'type', 'text' );
			$icon.removeClass( 'dashicons-visibility' ).addClass( 'dashicons-hidden' );
		} else {
			$input.attr( 'type', 'password' );
			$icon.removeClass( 'dashicons-hidden' ).addClass( 'dashicons-visibility' );
		}
	} );

	/**
	 * Password strength indicator.
	 */
	$( document ).on( 'input', '[id^="field_password_"]:not([id*="confirm"])', function () {
		var val = $( this ).val();
		var $bar = $( this ).closest( '.govalid-form' ).find( '[id^="password_bar_"]' );
		var strength = 0;
		if ( val.length >= 6 ) strength++;
		if ( val.length >= 10 ) strength++;
		if ( /[A-Z]/.test( val ) && /[a-z]/.test( val ) ) strength++;
		if ( /\d/.test( val ) ) strength++;
		if ( /[^A-Za-z0-9]/.test( val ) ) strength++;

		var pct = Math.min( strength * 20, 100 );
		var color = pct <= 20 ? '#ef4444' : pct <= 60 ? '#f59e0b' : '#22c55e';
		$bar.css( { width: pct + '%', background: color } );
	} );

	/**
	 * Identifier format — show/hide custom pattern and institution pattern fields.
	 */
	$( document ).on( 'change', '.govalid-identifier-format', function () {
		var val = $( this ).val();
		var $section = $( this ).closest( '.govalid-toggle-body' );
		var $custom = $section.find( '[id^="custom_pattern_container_"]' );
		var $institution = $section.find( '[id^="institution_pattern_container_"]' );

		// Hide both first.
		$custom.slideUp( 200 );
		$institution.slideUp( 200 );

		if ( val === 'custom' ) {
			$custom.slideDown( 200 );
		} else if ( val === 'institution_pattern' ) {
			$institution.slideDown( 200 );
			loadInstitutionPatterns( $institution.find( 'select' ) );
		}
	} );

	/**
	 * Load institution numbering patterns from GoValid API.
	 */
	var institutionPatternsCache = null;

	function loadInstitutionPatterns( $select ) {
		if ( institutionPatternsCache ) {
			renderPatternOptions( $select, institutionPatternsCache );
			return;
		}

		$select.html( '<option value="">' + escapeHtml( govalidQR.i18n.loading || 'Loading...' ) + '</option>' );

		$.ajax( {
			url: govalidQR.restUrl + 'numbering/patterns',
			method: 'GET',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				var patterns = data.patterns || data || [];
				institutionPatternsCache = patterns;
				renderPatternOptions( $select, patterns );
			},
			error: function () {
				$select.html( '<option value="">' + escapeHtml( govalidQR.i18n.no_patterns || 'No patterns available' ) + '</option>' );
			},
		} );
	}

	function renderPatternOptions( $select, patterns ) {
		if ( ! Array.isArray( patterns ) || patterns.length === 0 ) {
			$select.html( '<option value="">' + escapeHtml( govalidQR.i18n.no_patterns || 'No patterns available' ) + '</option>' );
			return;
		}
		var html = '<option value="">Select a pattern...</option>';
		patterns.forEach( function ( p ) {
			html += '<option value="' + escapeAttr( String( p.id ) ) + '">'
				+ escapeHtml( p.name + ' (' + p.pattern_template + ')' )
				+ '</option>';
		} );
		$select.html( html );
	}

	/**
	 * Generate identifier based on selected format.
	 */
	$( document ).on( 'click', '.govalid-generate-id-btn', function () {
		var targetId = $( this ).data( 'target' );
		var formatId = $( this ).data( 'format' );
		var format = $( '#' + formatId ).val();
		var $textarea = $( '#' + targetId );
		var $btn = $( this );
		var $qtyInput = $btn.closest( 'div' ).find( '.govalid-generate-qty' );
		var qty = Math.max( 1, Math.min( 999, parseInt( $qtyInput.val(), 10 ) || 1 ) );

		if ( format === 'institution_pattern' ) {
			// Use API to generate from institution pattern.
			var $section = $btn.closest( '.govalid-toggle-body' );
			var patternId = $section.find( '[id^="institution_pattern_select_"]' ).val();
			if ( ! patternId ) {
				showNotice( govalidQR.i18n.select_pattern || 'Please select a pattern first.' );
				return;
			}
			$btn.prop( 'disabled', true );
			$.ajax( {
				url: govalidQR.restUrl + 'numbering/generate',
				method: 'POST',
				contentType: 'application/json',
				data: JSON.stringify( { pattern_id: parseInt( patternId, 10 ), quantity: qty } ),
				beforeSend: function ( xhr ) {
					xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
				},
				success: function ( resp ) {
					var generated = [];
					if ( resp.data && resp.data.generated ) {
						generated = resp.data.generated;
					} else if ( resp.generated ) {
						generated = resp.generated;
					} else if ( Array.isArray( resp ) ) {
						generated = resp;
					}
					if ( generated.length ) {
						var ids = generated.map( function ( n ) { return n.number || n; } ).join( '\n' );
						appendToTextarea( $textarea, ids );
					} else {
						showNotice( govalidQR.i18n.error || 'No number generated.' );
					}
				},
				error: function () {
					showNotice( govalidQR.i18n.error || 'Error generating number.' );
				},
				complete: function () {
					$btn.prop( 'disabled', false );
				},
			} );
			return;
		}

		if ( format === 'custom' ) {
			var $section2 = $btn.closest( '.govalid-toggle-body' );
			var customPattern = $section2.find( 'input[id^="custom_pattern_"]' ).val() || '';
			var lines = [];
			for ( var i = 0; i < qty; i++ ) {
				lines.push( customPattern ? applyCustomPattern( customPattern ) : generateIdentifier( 'alpha5' ) );
			}
			appendToTextarea( $textarea, lines.join( '\n' ) );
			return;
		}

		var lines2 = [];
		for ( var j = 0; j < qty; j++ ) {
			lines2.push( generateIdentifier( format ) );
		}
		appendToTextarea( $textarea, lines2.join( '\n' ) );
	} );

	/**
	 * Click a custom pattern example to paste it into the input.
	 */
	$( document ).on( 'click', '.govalid-pattern-example', function () {
		var pat = $( this ).data( 'pattern' );
		var $input = $( this ).closest( '.govalid-custom-pattern-row' ).find( 'input[id^="custom_pattern_"]' );
		$input.val( pat ).trigger( 'focus' );
	} );

	/**
	 * Append generated identifier to textarea and update counter.
	 */
	function appendToTextarea( $textarea, value ) {
		var current = $textarea.val().trim();
		$textarea.val( current ? current + '\n' + value : value );
		if ( $textarea.hasClass( 'govalid-bulk-input' ) ) {
			updateEntryCounter( $textarea );
		}
	}

	/**
	 * Generate identifier based on selected format.
	 */
	function generateIdentifier( format ) {
		var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
		var digits = '0123456789';
		var alpha = chars + digits;
		var now = new Date();
		var yyyy = String( now.getFullYear() );
		var yy = yyyy.slice( -2 );
		var mm = String( now.getMonth() + 1 ).padStart( 2, '0' );
		var dd = String( now.getDate() ).padStart( 2, '0' );

		function rand( set, len ) {
			var r = '';
			for ( var i = 0; i < len; i++ ) r += set.charAt( Math.floor( Math.random() * set.length ) );
			return r;
		}

		switch ( format ) {
			case 'numeric5': return rand( digits, 5 );
			case 'numeric6': return rand( digits, 6 );
			case 'invoice':  return 'INV-' + yy + mm + dd + '-' + rand( digits, 3 );
			case 'serial':   return 'SN-' + rand( alpha, 5 ) + '-' + rand( digits, 3 );
			case 'dateseq':  return dd + mm + yy + '-' + rand( digits, 3 );
			case 'cert':     return 'CERT/' + yyyy + '/' + mm + '/' + rand( digits, 4 );
			case 'alpha5':
			default:         return rand( alpha, 5 );
		}
	}

	/**
	 * Apply a custom pattern to generate an identifier.
	 * Supports: {A+}=letters, {#+}=digits, {YYYY},{YY},{MM},{DD}=date parts.
	 * Repeated chars set length: {####}=4 digits, {AAA}=3 letters.
	 */
	function applyCustomPattern( pattern ) {
		var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
		var digits = '0123456789';
		var now = new Date();
		var result = pattern;

		result = result.replace( /\{YYYY\}/g, String( now.getFullYear() ) );
		result = result.replace( /\{YY\}/g, String( now.getFullYear() ).slice( -2 ) );
		result = result.replace( /\{MM\}/g, String( now.getMonth() + 1 ).padStart( 2, '0' ) );
		result = result.replace( /\{DD\}/g, String( now.getDate() ).padStart( 2, '0' ) );

		// {###...} = N random digits, {AAA...} = N random letters.
		result = result.replace( /\{(#+)\}/g, function ( _m, g ) {
			var r = '';
			for ( var i = 0; i < g.length; i++ ) r += digits.charAt( Math.floor( Math.random() * digits.length ) );
			return r;
		} );
		result = result.replace( /\{(A+)\}/g, function ( _m, g ) {
			var r = '';
			for ( var i = 0; i < g.length; i++ ) r += chars.charAt( Math.floor( Math.random() * chars.length ) );
			return r;
		} );

		return result;
	}

	/**
	 * Bulk input entry counter.
	 */
	$( document ).on( 'input', '.govalid-bulk-input', function () {
		updateEntryCounter( $( this ) );
	} );

	function updateEntryCounter( $textarea ) {
		var val = $textarea.val().trim();
		var max = parseInt( $textarea.data( 'max-entries' ), 10 ) || 999;
		var entries = val ? val.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } ) : [];
		var count = entries.length;

		var $counter = $textarea.closest( '.govalid-field-group' ).find( '.govalid-entry-counter' );
		if ( ! $counter.length ) {
			return;
		}

		$counter.find( '.govalid-counter-current' ).text( count );
		$counter.find( '.govalid-counter-max' ).text( max );
		$counter.removeClass( 'govalid-counter-warning govalid-counter-error' );

		if ( count > max ) {
			$counter.addClass( 'govalid-counter-error' );
		} else if ( count >= max * 0.9 ) {
			$counter.addClass( 'govalid-counter-warning' );
		}

		// Show/hide bulk badge.
		var $badge = $counter.find( '.govalid-bulk-badge' );
		if ( $badge.length ) {
			if ( count > 1 ) {
				$badge.show();
			} else {
				$badge.hide();
			}
		}
	}

	/* =========================================================
	   Signed by — auto-ENTERPRISE + PIN verification
	   ========================================================= */

	var pinVerified = false;

	/**
	 * When "Signed by" toggle changes, auto-switch security to ENTERPRISE and show PIN.
	 */
	$( document ).on( 'change', '#toggle_sign_by_cert', function () {
		var $form = $( this ).closest( '.govalid-form' );
		var isChecked = $( this ).is( ':checked' );

		if ( isChecked ) {
			// Set hidden sign_by value from display field.
			$( '#hidden_field_sign_by_cert' ).val( $( '#field_sign_by_display_cert' ).val() );

			// Auto-switch security level to ENTERPRISE.
			var $enterprise = $form.find( 'input[name="security_level"][value="ENTERPRISE"]' );
			$enterprise.prop( 'checked', true ).trigger( 'change' );

			// Visually lock security cards.
			$form.find( '.govalid-security-cards' ).addClass( 'govalid-security-locked' );

			// Check PIN status.
			checkPINStatus();
		} else {
			// Clear sign_by.
			$( '#hidden_field_sign_by_cert' ).val( '' );

			// Unlock security cards.
			$form.find( '.govalid-security-cards' ).removeClass( 'govalid-security-locked' );

			// Reset PIN state.
			resetPINState();
		}
	} );

	/**
	 * Check PIN status from GoValid API.
	 */
	function checkPINStatus() {
		$( '#pin_not_set_cert, #pin_input_section_cert, #pin_success_cert, #pin_locked_cert, #pin_error_cert' ).hide();

		$.ajax( {
			url: govalidQR.restUrl + 'pin/status',
			method: 'GET',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( resp ) {
				var pin = resp.data || resp;
				if ( ! pin.is_set ) {
					$( '#pin_not_set_cert' ).show();
				} else if ( pin.is_locked ) {
					$( '#pin_locked_cert' ).show();
				} else {
					$( '#pin_input_section_cert' ).show();
					$( '#pin_inputs_cert .govalid-pin-digit' ).first().focus();
				}
			},
			error: function () {
				// Fallback: show PIN input anyway.
				$( '#pin_input_section_cert' ).show();
			},
		} );
	}

	/**
	 * PIN digit input — auto-advance, backspace, auto-verify on 6th digit.
	 */
	$( document ).on( 'input', '.govalid-pin-digit', function () {
		var val = $( this ).val();
		// Allow only digits.
		$( this ).val( val.replace( /\D/g, '' ) );
		if ( $( this ).val().length === 1 ) {
			var idx = parseInt( $( this ).data( 'index' ), 10 );
			if ( idx < 6 ) {
				$( this ).next( '.govalid-pin-digit' ).focus();
			}
			// Auto-verify when all 6 filled.
			if ( idx === 6 ) {
				setTimeout( function () { verifyPIN(); }, 100 );
			}
		}
	} );

	$( document ).on( 'keydown', '.govalid-pin-digit', function ( e ) {
		if ( e.key === 'Backspace' && ! $( this ).val() ) {
			$( this ).prev( '.govalid-pin-digit' ).focus();
		}
	} );

	// Handle paste into first PIN digit.
	$( document ).on( 'paste', '.govalid-pin-digit', function ( e ) {
		e.preventDefault();
		var pasted = ( e.originalEvent.clipboardData || window.clipboardData ).getData( 'text' ).replace( /\D/g, '' );
		if ( pasted.length >= 6 ) {
			var $digits = $( '#pin_inputs_cert .govalid-pin-digit' );
			for ( var i = 0; i < 6; i++ ) {
				$digits.eq( i ).val( pasted.charAt( i ) );
			}
			setTimeout( function () { verifyPIN(); }, 100 );
		}
	} );

	/**
	 * Verify PIN button click.
	 */
	$( document ).on( 'click', '#verify_pin_btn_cert', function () {
		verifyPIN();
	} );

	/**
	 * Verify PIN via REST API.
	 */
	function verifyPIN() {
		var pin = '';
		$( '#pin_inputs_cert .govalid-pin-digit' ).each( function () {
			pin += $( this ).val();
		} );

		if ( pin.length !== 6 ) {
			$( '#pin_error_cert' ).text( govalidQR.i18n.pin_enter_6 || 'Please enter all 6 digits.' ).show();
			return;
		}

		var $btn = $( '#verify_pin_btn_cert' );
		$btn.prop( 'disabled', true ).html( '<span class="dashicons dashicons-update govalid-spin"></span> ' + ( govalidQR.i18n.verifying || 'Verifying...' ) );
		$( '#pin_error_cert' ).hide();

		$.ajax( {
			url: govalidQR.restUrl + 'pin/verify',
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( { pin: pin } ),
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				if ( data.success || data.verified ) {
					pinVerified = true;
					$( '#hidden_pin_verified_cert' ).val( 'true' );
					$( '#pin_input_section_cert' ).hide();
					$( '#pin_success_cert' ).show();
				} else {
					var errMsg = data.error || ( govalidQR.i18n.pin_invalid || 'Invalid PIN.' );
					$( '#pin_error_cert' ).text( errMsg ).show();
					// Clear digits.
					$( '#pin_inputs_cert .govalid-pin-digit' ).val( '' );
					$( '#pin_inputs_cert .govalid-pin-digit' ).first().focus();
					if ( data.is_locked ) {
						$( '#pin_input_section_cert' ).hide();
						$( '#pin_locked_cert' ).show();
					}
					$btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-yes"></span> ' + ( govalidQR.i18n.verify_pin || 'Verify PIN' ) );
				}
			},
			error: function ( jqXHR ) {
				var msg = govalidQR.i18n.error || 'Error';
				if ( jqXHR.responseJSON ) {
					msg = jqXHR.responseJSON.error || jqXHR.responseJSON.message || msg;
				}
				$( '#pin_error_cert' ).text( msg ).show();
				$( '#pin_inputs_cert .govalid-pin-digit' ).val( '' );
				$( '#pin_inputs_cert .govalid-pin-digit' ).first().focus();
				$btn.prop( 'disabled', false ).html( '<span class="dashicons dashicons-yes"></span> ' + ( govalidQR.i18n.verify_pin || 'Verify PIN' ) );
			},
		} );
	}

	/**
	 * Reset PIN verification state.
	 */
	function resetPINState() {
		pinVerified = false;
		$( '#hidden_pin_verified_cert' ).val( 'false' );
		$( '#pin_not_set_cert, #pin_input_section_cert, #pin_success_cert, #pin_locked_cert, #pin_error_cert' ).hide();
		$( '#pin_inputs_cert .govalid-pin-digit' ).val( '' );
		$( '#verify_pin_btn_cert' ).prop( 'disabled', false ).html( '<span class="dashicons dashicons-yes"></span> ' + ( govalidQR.i18n.verify_pin || 'Verify PIN' ) );
	}

	/**
	 * Block form submission — validate PIN + bulk name/identifier counts.
	 */
	$( document ).on( 'submit', '#govalid-form-certificate', function ( e ) {
		if ( $( '#toggle_sign_by_cert' ).is( ':checked' ) && ! pinVerified ) {
			e.preventDefault();
			showNotice( govalidQR.i18n.pin_required || 'Please verify your signing PIN first.', 'error' );
			$( 'html, body' ).animate( { scrollTop: $( '#pin_card_cert' ).offset().top - 60 }, 300 );
			return false;
		}

		// Validate bulk name/identifier counts.
		// Valid: equal counts, or exactly 1 identifier for multiple names.
		var nameVal = $( '#field_name_cert' ).val().trim();
		var names = nameVal ? nameVal.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } ) : [];
		var identifierEnabled = $( '#container_identifier_cert' ).is( ':visible' );

		if ( identifierEnabled ) {
			var idVal = $( '#field_identifier_cert' ).val().trim();
			var identifiers = idVal ? idVal.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } ) : [];

			if ( identifiers.length > 0 && identifiers.length !== names.length && identifiers.length !== 1 ) {
				e.preventDefault();
				showNotice(
					( govalidQR.i18n.bulk_mismatch || 'Bulk mismatch: you have ' )
					+ names.length + ( govalidQR.i18n.bulk_names || ' name(s) and ' )
					+ identifiers.length + ( govalidQR.i18n.bulk_identifiers || ' identifier(s). Counts must match, or use a single identifier for all names.' ),
					'error'
				);
				$( 'html, body' ).animate( { scrollTop: $( '#field_name_cert' ).offset().top - 60 }, 300 );
				return false;
			}
		}
	} );

	/**
	 * File upload — preview file name and remove.
	 */
	$( document ).on( 'change', '.govalid-file-input', function () {
		var $area = $( this ).closest( '.govalid-file-upload' );
		var $placeholder = $area.find( '.govalid-file-upload-placeholder' );
		var $preview = $area.find( '.govalid-file-preview' );
		if ( this.files && this.files.length ) {
			var file = this.files[0];
			// Validate size (5MB).
			if ( file.size > 5 * 1024 * 1024 ) {
				showNotice( 'File exceeds 5MB limit.', 'error' );
				$( this ).val( '' );
				return;
			}
			$preview.find( '.govalid-file-preview-name' ).text( file.name );
			$placeholder.hide();
			$preview.show();
		}
	} );

	$( document ).on( 'click', '.govalid-file-remove', function ( e ) {
		e.preventDefault();
		e.stopPropagation();
		var $area = $( this ).closest( '.govalid-file-upload' );
		$area.find( '.govalid-file-input' ).val( '' );
		$area.find( '.govalid-file-upload-placeholder' ).show();
		$area.find( '.govalid-file-preview' ).hide();
	} );

	// Drag hover styling.
	$( document ).on( 'dragenter dragover', '.govalid-file-upload', function ( e ) {
		e.preventDefault();
		$( this ).addClass( 'dragging' );
	} );
	$( document ).on( 'dragleave drop', '.govalid-file-upload', function ( e ) {
		e.preventDefault();
		$( this ).removeClass( 'dragging' );
	} );

	/**
	 * Removal date — combine date + time into hidden field.
	 */
	$( document ).on( 'change', '[id^="field_removal_date_"], [id^="field_removal_time_"]', function () {
		var $form = $( this ).closest( '.govalid-form' );
		var date = $form.find( '[id^="field_removal_date_"]' ).val();
		var time = $form.find( '[id^="field_removal_time_"]' ).val() || '23:59';
		if ( date ) {
			$form.find( '[id^="combined_removal_datetime_"]' ).val( date + 'T' + time + ':00' );
		}
	} );

	/* =========================================================
	   Humanize Links — Two-step flow: suggest → choose → check → create
	   ========================================================= */

	var $linksList = $( '#govalid-links-list' );
	var linksCurrentPage = 1;
	var linkState = {
		targetUrl: '',
		title: '',
		description: '',
		audience: 'local',
		suggestions: [],
		selectedDomain: '',
		selectedSlug: '',
		aiProvider: 'manual',
		slugVerified: false,
	};

	if ( $linksList.length ) {
		loadLinks();
	}

	/**
	 * Step 1: Get AI suggestions.
	 */
	$( document ).on( 'click', '#govalid-suggest-btn', function () {
		var $btn = $( this );
		var targetUrl   = $( '#govalid-link-url' ).val().trim();
		var title       = $( '#govalid-link-title' ).val().trim();
		var description = $( '#govalid-link-description' ).val().trim();
		var audience    = $( '#govalid-link-audience' ).val();

		if ( ! targetUrl ) {
			$( '#govalid-link-url' ).focus();
			return;
		}

		// Save state.
		linkState.targetUrl = targetUrl;
		linkState.title = title;
		linkState.description = description;
		linkState.audience = audience;
		linkState.slugVerified = false;

		$btn.prop( 'disabled', true ).html(
			'<span class="dashicons dashicons-update govalid-spin"></span> '
			+ escapeHtml( govalidQR.i18n.suggesting || 'Generating suggestions...' )
		);

		var body = { url: targetUrl };
		if ( title ) body.title = title;
		if ( description ) body.description = description;
		if ( audience ) body.audience = audience;

		$.ajax( {
			url: govalidQR.restUrl + 'links/suggest',
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( body ),
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				var suggestions = data.suggestions || [];
				if ( ! suggestions.length ) {
					showNotice( govalidQR.i18n.error || 'No suggestions returned.' );
					return;
				}

				linkState.suggestions = suggestions;

				// Render suggestion cards.
				var html = '';
				suggestions.forEach( function ( s, i ) {
					// country_code present = regional flag, empty = international globe
					var flag = s.country_code ? getFlagEmoji( s.country_code ) : '\uD83C\uDF0D';
					html += '<div class="govalid-suggestion-card" data-index="' + i + '">';
					html += '<span class="govalid-suggestion-flag">' + flag + '</span>';
					html += '<div class="govalid-suggestion-info">';
					html += '<div class="govalid-suggestion-url">' + escapeHtml( s.full_url || ( 'https://' + s.domain + '/' + s.slug ) ) + '</div>';
					html += '<div class="govalid-suggestion-domain">' + escapeHtml( s.domain ) + '</div>';
					html += '</div>';
					if ( s.is_premium ) {
						html += '<span class="govalid-suggestion-premium">Premium</span>';
					}
					html += '</div>';
				} );

				$( '#govalid-suggestion-list' ).html( html );
				$( '#govalid-slug-editor' ).hide();
				$( '#govalid-suggestions-card' ).show();
				$( '#govalid-link-result' ).hide();

				// Scroll to suggestions.
				$( 'html, body' ).animate( { scrollTop: $( '#govalid-suggestions-card' ).offset().top - 40 }, 300 );
			},
			error: function ( jqXHR ) {
				var msg = govalidQR.i18n.error;
				if ( jqXHR.responseJSON ) {
					msg = jqXHR.responseJSON.error || jqXHR.responseJSON.message || msg;
				}
				showNotice( msg );
			},
			complete: function () {
				$btn.prop( 'disabled', false ).html(
					'<span class="dashicons dashicons-randomize"></span> '
					+ escapeHtml( 'Humanize' )
				);
			},
		} );
	} );

	/**
	 * Click a suggestion card — populate slug editor.
	 */
	$( document ).on( 'click', '.govalid-suggestion-card', function () {
		var index = parseInt( $( this ).data( 'index' ), 10 );
		var s = linkState.suggestions[ index ];
		if ( ! s ) return;

		// Mark selected.
		$( '.govalid-suggestion-card' ).removeClass( 'selected' );
		$( this ).addClass( 'selected' );

		// Populate editor.
		linkState.selectedDomain = s.domain;
		linkState.selectedSlug = s.slug;
		linkState.aiProvider = s.provider || 'ai';
		linkState.slugVerified = false;

		$( '#govalid-slug-domain-prefix' ).text( 'https://' + s.domain + '/' );
		$( '#govalid-slug-input' ).val( s.slug );
		$( '#govalid-slug-status' ).hide();
		$( '#govalid-create-link-btn' ).prop( 'disabled', true );
		$( '#govalid-slug-editor' ).show();
	} );

	/**
	 * Check slug availability.
	 */
	$( document ).on( 'click', '#govalid-check-slug-btn', function () {
		var slug = $( '#govalid-slug-input' ).val().trim();
		var domain = linkState.selectedDomain;
		var $btn = $( this );
		var $status = $( '#govalid-slug-status' );

		if ( ! slug ) {
			$( '#govalid-slug-input' ).focus();
			return;
		}

		$btn.prop( 'disabled', true ).text( govalidQR.i18n.checking_slug || 'Checking...' );
		$status.hide();
		linkState.slugVerified = false;
		$( '#govalid-create-link-btn' ).prop( 'disabled', true );

		$.ajax( {
			url: govalidQR.restUrl + 'links/check-slug',
			method: 'GET',
			data: { slug: slug, domain: domain },
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				if ( data.available ) {
					$status.removeClass( 'is-taken is-reserved' ).addClass( 'is-available' )
						.html( '<span class="dashicons dashicons-yes-alt"></span> ' + escapeHtml( govalidQR.i18n.slug_available || 'Available!' ) )
						.show();
					linkState.selectedSlug = slug;
					linkState.slugVerified = true;
					$( '#govalid-create-link-btn' ).prop( 'disabled', false );
				} else if ( data.reason === 'reserved' ) {
					$status.removeClass( 'is-available is-taken' ).addClass( 'is-reserved' )
						.html( '<span class="dashicons dashicons-lock"></span> ' + escapeHtml( govalidQR.i18n.slug_reserved || 'Reserved.' ) )
						.show();
				} else {
					$status.removeClass( 'is-available is-reserved' ).addClass( 'is-taken' )
						.html( '<span class="dashicons dashicons-no-alt"></span> ' + escapeHtml( govalidQR.i18n.slug_taken || 'Already taken.' ) )
						.show();
				}
			},
			error: function () {
				showNotice( govalidQR.i18n.error );
			},
			complete: function () {
				$btn.prop( 'disabled', false ).text( 'Check' );
			},
		} );
	} );

	/**
	 * When slug input changes, reset verification.
	 */
	$( document ).on( 'input', '#govalid-slug-input', function () {
		linkState.slugVerified = false;
		$( '#govalid-create-link-btn' ).prop( 'disabled', true );
		$( '#govalid-slug-status' ).hide();
	} );

	/**
	 * Create the link (Step 3).
	 */
	$( document ).on( 'click', '#govalid-create-link-btn', function () {
		if ( ! linkState.slugVerified ) return;

		var $btn = $( this );
		$btn.prop( 'disabled', true ).html(
			'<span class="dashicons dashicons-update govalid-spin"></span> '
			+ escapeHtml( govalidQR.i18n.creating_link || 'Creating link...' )
		);

		var body = {
			slug: linkState.selectedSlug,
			domain: linkState.selectedDomain,
			target_url: linkState.targetUrl,
			title: linkState.title,
			ai_provider: linkState.aiProvider,
		};

		$.ajax( {
			url: govalidQR.restUrl + 'links/create',
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( body ),
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				var link = data.link || data;
				var fullUrl = link.full_url || '';
				var uuid = link.id || '';

				// Show result.
				$( '#govalid-link-result-heading' ).text( 'Link Created' );
				$( '#govalid-link-result-url' ).text( fullUrl );
				$( '#govalid-copy-link-btn' ).data( 'clipboard', fullUrl );
				$( '#govalid-suggestions-card' ).hide();
				$( '#govalid-link-result' ).show();
				$( '#govalid-back-to-list-btn' ).hide();

				// Generate live QR code for the created link.
				initLinkQR( fullUrl, uuid );

				// Reload list.
				linksCurrentPage = 1;
				loadLinks();

				showNotice( govalidQR.i18n.link_created || 'Link created!' );
			},
			error: function ( jqXHR ) {
				var msg = govalidQR.i18n.error;
				if ( jqXHR.responseJSON ) {
					msg = jqXHR.responseJSON.error || jqXHR.responseJSON.message || msg;
				}
				showNotice( msg );
			},
			complete: function () {
				$btn.prop( 'disabled', false ).html(
					'<span class="dashicons dashicons-yes-alt"></span> '
					+ escapeHtml( 'Create Link' )
				);
			},
		} );
	} );

	/**
	 * Back button — return to suggestions.
	 */
	$( document ).on( 'click', '#govalid-back-btn', function () {
		$( '#govalid-suggestions-card' ).hide();
		$( '#govalid-link-form-card' ).show();
		$( 'html, body' ).animate( { scrollTop: $( '#govalid-link-form-card' ).offset().top - 40 }, 200 );
	} );

	/**
	 * Create Another — reset everything.
	 */
	$( document ).on( 'click', '#govalid-create-another-btn', function () {
		$( '#govalid-link-result' ).hide();
		$( '#govalid-suggestions-card' ).hide();
		$( '#govalid-link-url' ).val( '' );
		$( '#govalid-link-title' ).val( '' );
		$( '#govalid-link-description' ).val( '' );
		$( '#govalid-link-form-card' ).show();
		resetLinkQR();
		$( 'html, body' ).animate( { scrollTop: $( '#govalid-link-form-card' ).offset().top - 40 }, 200 );
	} );

	/**
	 * Get flag emoji from country code.
	 */
	function getFlagEmoji( cc ) {
		if ( ! cc || cc.length !== 2 ) return '';
		var code = cc.toUpperCase();
		return String.fromCodePoint(
			0x1F1E6 + code.charCodeAt( 0 ) - 65,
			0x1F1E6 + code.charCodeAt( 1 ) - 65
		);
	}

	/**
	 * Load links list.
	 */
	function loadLinks( page ) {
		page = page || linksCurrentPage;
		$linksList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.loading_links || 'Loading links...' ) + '</p>' );

		$.ajax( {
			url: govalidQR.restUrl + 'links',
			method: 'GET',
			data: { page: page, per_page: 15 },
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				var items = data.results || data || [];
				var linkCount = data.count || items.length || 0;

				if ( ! Array.isArray( items ) || items.length === 0 ) {
					$linksList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.no_links || 'No links yet.' ) + '</p>' );
					$( '#govalid-links-pagination' ).hide();
					return;
				}
				renderLinksList( items );
				renderLinksPagination( data.page || 1, data.total_pages || 1 );

			},
			error: function () {
				$linksList.html( '<p class="govalid-loading">' + escapeHtml( govalidQR.i18n.error ) + '</p>' );
			},
		} );
	}

	/**
	 * Render the links table.
	 */
	function renderLinksList( items ) {
		var isAnon = ! govalidQR.isConnected;
		var html = '<table class="govalid-links-table"><thead><tr>'
			+ '<th class="govalid-th-qr"></th>'
			+ '<th>' + escapeHtml( 'Link' ) + '</th>';
		if ( ! isAnon ) {
			html += '<th>' + escapeHtml( 'Clicks' ) + '</th>'
				+ '<th>' + escapeHtml( 'Status' ) + '</th>';
		}
		html += '<th></th>'
			+ '</tr></thead><tbody>';

		items.forEach( function ( link ) {
			var targetShort = link.target_url.length > 50
				? link.target_url.substring( 0, 50 ) + '…'
				: link.target_url;

			html += '<tr data-uuid="' + escapeAttr( link.id ) + '">';

			/* --- QR thumbnail column --- */
			html += '<td class="govalid-link-qr-cell">';
			html += '<div class="govalid-link-qr-thumb" data-url="' + escapeAttr( link.full_url )
				+ '" data-uuid="' + escapeAttr( link.id )
				+ '" id="govalid-qr-thumb-' + escapeAttr( link.id ) + '"></div>';
			html += '</td>';

			/* --- Merged Link + Target column --- */
			html += '<td class="govalid-link-cell" data-uuid="' + escapeAttr( link.id ) + '" data-url="' + escapeAttr( link.target_url ) + '">';
			html += '<a href="' + escapeAttr( link.full_url ) + '" target="_blank" rel="noopener" class="govalid-link-url">'
				+ escapeHtml( link.full_url ) + '</a>';
			if ( link.title ) {
				html += '<span class="govalid-link-title">' + escapeHtml( link.title ) + '</span>';
			}
			html += '<span class="govalid-link-target-text" title="' + escapeAttr( link.target_url ) + '">' + escapeHtml( targetShort ) + '</span>';
			if ( ! isAnon ) {
				html += '<div class="govalid-link-target-edit" style="display:none;">';
				html += '<input type="url" class="govalid-link-target-input govalid-input" value="' + escapeAttr( link.target_url ) + '" />';
				html += '<div class="govalid-link-target-btns">';
				html += '<button type="button" class="govalid-action-btn govalid-action-btn--primary govalid-link-save" title="Save"><span class="dashicons dashicons-yes"></span></button>';
				html += '<button type="button" class="govalid-action-btn govalid-link-cancel" title="Cancel"><span class="dashicons dashicons-no-alt"></span></button>';
				html += '</div></div>';
			}
			html += '</td>';

			/* --- Clicks & Status columns (connected only) --- */
			if ( ! isAnon ) {
				var statusLabel = link.is_active
					? ( govalidQR.i18n.active || 'Active' )
					: ( govalidQR.i18n.inactive || 'Inactive' );
				var statusClass = link.is_active ? 'govalid-status-active' : 'govalid-status-inactive';
				html += '<td class="govalid-link-clicks">' + escapeHtml( String( link.click_count || 0 ) ) + '</td>';
				html += '<td><span class="govalid-status-badge ' + statusClass + '">' + escapeHtml( statusLabel ) + '</span></td>';
			}

			/* --- Actions column: three-dot dropdown --- */
			html += '<td class="govalid-link-actions">';
			html += '<div class="govalid-actions-dropdown">';
			html += '<button type="button" class="govalid-actions-trigger" title="Actions"><span class="dashicons dashicons-ellipsis"></span></button>';
			html += '<div class="govalid-actions-menu">';
			html += '<button type="button" class="govalid-link-copy govalid-copy-shortcode" data-clipboard="'
				+ escapeAttr( link.full_url ) + '"><span class="dashicons dashicons-clipboard"></span> Copy Link</button>';
			html += '<button type="button" class="govalid-link-qr-btn" data-url="'
				+ escapeAttr( link.full_url ) + '" data-uuid="' + escapeAttr( link.id )
				+ '"><span class="dashicons dashicons-format-image"></span> QR Design</button>';
			if ( ! isAnon ) {
				html += '<button type="button" class="govalid-link-edit" data-uuid="'
					+ escapeAttr( link.id ) + '"><span class="dashicons dashicons-edit"></span> Edit Target</button>';
				html += '<button type="button" class="govalid-link-toggle" data-uuid="'
					+ escapeAttr( link.id ) + '"><span class="dashicons dashicons-'
					+ ( link.is_active ? 'hidden' : 'visibility' ) + '"></span> '
					+ ( link.is_active ? 'Deactivate' : 'Activate' ) + '</button>';
				html += '<hr class="govalid-actions-divider">';
				html += '<button type="button" class="govalid-link-delete govalid-actions-danger" data-uuid="'
					+ escapeAttr( link.id ) + '"><span class="dashicons dashicons-trash"></span> Delete</button>';
			}
			html += '</div></div>';
			html += '</td>';
			html += '</tr>';
		} );

		html += '</tbody></table>';
		$linksList.html( html );

		/* --- Render QR thumbnails after DOM insert --- */
		if ( typeof QRCodeStyling !== 'undefined' ) {
			items.forEach( function ( link ) {
				var $thumb = $( '#govalid-qr-thumb-' + link.id.replace( /[^a-zA-Z0-9-]/g, '' ) );
				if ( $thumb.length ) {
					var thumbQR = new QRCodeStyling( {
						width: 40, height: 40, type: 'canvas',
						data: link.full_url,
						dotsOptions: { color: '#000000', type: 'square' },
						backgroundOptions: { color: '#FFFFFF' },
						qrOptions: { errorCorrectionLevel: 'L' },
						margin: 0,
					} );
					thumbQR.append( $thumb[0] );
				}
			} );
		}
	}

	/**
	 * Render pagination.
	 */
	function renderLinksPagination( currentPage, totalPages ) {
		if ( totalPages <= 1 ) {
			$( '#govalid-links-pagination' ).hide();
			return;
		}
		var html = '';
		if ( currentPage > 1 ) {
			html += '<button type="button" class="button govalid-links-page" data-page="' + ( currentPage - 1 ) + '">&laquo; Prev</button> ';
		}
		html += '<span style="margin: 0 8px; font-size: 13px; color: var(--gv-gray-500);">'
			+ currentPage + ' / ' + totalPages + '</span>';
		if ( currentPage < totalPages ) {
			html += ' <button type="button" class="button govalid-links-page" data-page="' + ( currentPage + 1 ) + '">Next &raquo;</button>';
		}
		$( '#govalid-links-pagination' ).html( html ).show();
	}

	/**
	 * Pagination click.
	 */
	$( document ).on( 'click', '.govalid-links-page', function () {
		linksCurrentPage = parseInt( $( this ).data( 'page' ), 10 );
		loadLinks( linksCurrentPage );
	} );

	/* ---- Three-dot dropdown menu ---- */
	$( document ).on( 'click', '.govalid-actions-trigger', function ( e ) {
		e.stopPropagation();
		var $menu = $( this ).siblings( '.govalid-actions-menu' );
		var wasOpen = $menu.hasClass( 'govalid-actions-open' );
		$( '.govalid-actions-menu.govalid-actions-open' ).removeClass( 'govalid-actions-open' );
		if ( ! wasOpen ) { $menu.addClass( 'govalid-actions-open' ); }
	} );
	$( document ).on( 'click', function () {
		$( '.govalid-actions-menu.govalid-actions-open' ).removeClass( 'govalid-actions-open' );
	} );
	$( document ).on( 'click', '.govalid-actions-menu button', function () {
		$( this ).closest( '.govalid-actions-menu' ).removeClass( 'govalid-actions-open' );
	} );

	/* ---- QR thumbnail lightbox modal ---- */
	var $linkQrModal = $( '<div class="govalid-qr-modal-overlay govalid-link-qr-modal" style="display:none;">'
		+ '<div class="govalid-qr-modal">'
		+ '<button type="button" class="govalid-qr-modal-close">&times;</button>'
		+ '<div class="govalid-qr-modal-body"><div id="govalid-link-qr-modal-preview"></div></div>'
		+ '<div class="govalid-qr-modal-footer">'
		+ '<div class="govalid-link-qr-modal-actions">'
		+ '<button type="button" class="button govalid-link-qr-modal-png"><span class="dashicons dashicons-download"></span> PNG</button>'
		+ '<button type="button" class="button govalid-link-qr-modal-hd"><span class="dashicons dashicons-download"></span> HD</button>'
		+ '<button type="button" class="button button-primary govalid-link-qr-modal-design"><span class="dashicons dashicons-art"></span> Customize</button>'
		+ '</div></div></div></div>' );
	$( 'body' ).append( $linkQrModal );

	$( document ).on( 'click', '.govalid-link-qr-thumb', function ( e ) {
		e.stopPropagation();
		var url  = $( this ).data( 'url' );
		var uuid = $( this ).data( 'uuid' );
		if ( ! url || typeof QRCodeStyling === 'undefined' ) { return; }

		var $preview = $( '#govalid-link-qr-modal-preview' );
		$preview.empty();
		$linkQrModal.data( 'url', url ).data( 'uuid', uuid );

		var previewQR = new QRCodeStyling( {
			width: 200, height: 200, type: 'canvas',
			data: url,
			dotsOptions: { color: '#000000', type: 'square' },
			backgroundOptions: { color: '#FFFFFF' },
			qrOptions: { errorCorrectionLevel: 'H' },
		} );
		previewQR.append( $preview[0] );
		$linkQrModal.data( 'qrInstance', previewQR );
		$linkQrModal.fadeIn( 200 );
	} );

	/* Close lightbox */
	$linkQrModal.on( 'click', '.govalid-qr-modal-close', function () {
		$linkQrModal.fadeOut( 200 );
	} );
	$linkQrModal.on( 'click', function ( e ) {
		if ( $( e.target ).hasClass( 'govalid-qr-modal-overlay' ) ) {
			$linkQrModal.fadeOut( 200 );
		}
	} );

	/* Lightbox download PNG (500px) */
	$linkQrModal.on( 'click', '.govalid-link-qr-modal-png', function () {
		var qr = $linkQrModal.data( 'qrInstance' );
		if ( qr ) { qr.download( { name: 'qr-code', extension: 'png' } ); }
	} );

	/* Lightbox download HD (1000px) */
	$linkQrModal.on( 'click', '.govalid-link-qr-modal-hd', function () {
		var url = $linkQrModal.data( 'url' );
		if ( ! url || typeof QRCodeStyling === 'undefined' ) { return; }
		var hdQR = new QRCodeStyling( {
			width: 1000, height: 1000, type: 'canvas',
			data: url,
			dotsOptions: { color: '#000000', type: 'square' },
			backgroundOptions: { color: '#FFFFFF' },
			qrOptions: { errorCorrectionLevel: 'H' },
		} );
		var tmp = document.createElement( 'div' );
		tmp.style.cssText = 'position:absolute;left:-9999px';
		document.body.appendChild( tmp );
		hdQR.append( tmp );
		setTimeout( function () {
			var c = tmp.querySelector( 'canvas' );
			if ( c ) {
				c.toBlob( function ( blob ) {
					var a = document.createElement( 'a' );
					a.href = URL.createObjectURL( blob );
					a.download = 'qr-code-hd.png';
					document.body.appendChild( a );
					a.click();
					document.body.removeChild( a );
					URL.revokeObjectURL( a.href );
				}, 'image/png' );
			}
			document.body.removeChild( tmp );
		}, 300 );
	} );

	/* Lightbox → open full QR designer */
	$linkQrModal.on( 'click', '.govalid-link-qr-modal-design', function () {
		var url  = $linkQrModal.data( 'url' );
		var uuid = $linkQrModal.data( 'uuid' );
		$linkQrModal.fadeOut( 200 );
		if ( url ) {
			/* Trigger the existing QR designer flow */
			$( '.govalid-link-qr-btn[data-uuid="' + uuid + '"]' ).first().trigger( 'click' );
		}
	} );

	/**
	 * Toggle link active/inactive.
	 */
	$( document ).on( 'click', '.govalid-link-toggle', function () {
		var $btn = $( this );
		var uuid = $btn.data( 'uuid' );
		$btn.prop( 'disabled', true );

		$.ajax( {
			url: govalidQR.restUrl + 'links/' + encodeURIComponent( uuid ) + '/toggle',
			method: 'POST',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				var msg = data.message || ( data.is_active ? 'Activated' : 'Deactivated' );
				showNotice( msg );
				loadLinks( linksCurrentPage );
			},
			error: function () {
				showNotice( govalidQR.i18n.error );
			},
			complete: function () {
				$btn.prop( 'disabled', false );
			},
		} );
	} );

	/**
	 * Delete a link.
	 */
	/**
	 * Edit link — show inline input.
	 */
	$( document ).on( 'click', '.govalid-link-edit', function () {
		var uuid = $( this ).data( 'uuid' );
		var $td = $( 'td.govalid-link-cell[data-uuid="' + uuid + '"]' );
		$td.find( '.govalid-link-target-text' ).hide();
		$td.find( '.govalid-link-target-edit' ).show();
		$td.find( '.govalid-link-target-input' ).focus();
	} );

	/**
	 * Cancel edit — hide inline input.
	 */
	$( document ).on( 'click', '.govalid-link-cancel', function () {
		var $td = $( this ).closest( 'td.govalid-link-cell' );
		var originalUrl = $td.data( 'url' );
		$td.find( '.govalid-link-target-input' ).val( originalUrl );
		$td.find( '.govalid-link-target-edit' ).hide();
		$td.find( '.govalid-link-target-text' ).show();
	} );

	/**
	 * Save edited target URL.
	 */
	$( document ).on( 'click', '.govalid-link-save', function () {
		var $btn = $( this );
		var $td = $btn.closest( 'td.govalid-link-cell' );
		var uuid = $td.data( 'uuid' );
		var newUrl = $td.find( '.govalid-link-target-input' ).val().trim();

		if ( ! newUrl ) {
			showNotice( 'Target URL cannot be empty.', 'error' );
			return;
		}

		$btn.prop( 'disabled', true );

		$.ajax( {
			url: govalidQR.restUrl + 'links/' + encodeURIComponent( uuid ) + '/update',
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( { target_url: newUrl } ),
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( data ) {
				showNotice( data.message || 'Link updated.' );
				loadLinks( linksCurrentPage );
			},
			error: function ( jqXHR ) {
				var msg = govalidQR.i18n.error || 'Error updating link.';
				if ( jqXHR.responseJSON ) {
					msg = jqXHR.responseJSON.error || jqXHR.responseJSON.message || msg;
				}
				showNotice( msg, 'error' );
			},
			complete: function () {
				$btn.prop( 'disabled', false );
			},
		} );
	} );

	/**
	 * Delete a link.
	 */
	$( document ).on( 'click', '.govalid-link-delete', function () {
		if ( ! confirm( govalidQR.i18n.confirm_delete_link || 'Delete this link?' ) ) {
			return;
		}
		var $btn = $( this );
		var uuid = $btn.data( 'uuid' );
		$btn.prop( 'disabled', true );

		$.ajax( {
			url: govalidQR.restUrl + 'links/' + encodeURIComponent( uuid ),
			method: 'DELETE',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function () {
				showNotice( govalidQR.i18n.link_deleted || 'Link deleted.' );
				loadLinks( linksCurrentPage );
			},
			error: function () {
				showNotice( govalidQR.i18n.error );
				$btn.prop( 'disabled', false );
			},
		} );
	} );

	/* =========================================================
	   Link QR Code — Live design & download
	   ========================================================= */

	var linkQRInstance = null;
	var linkQRUrl = '';
	var linkQRUuid = '';
	var linkQRLogoUrl = '';
	var linkQREyeCache = null;  // cached eye detection info
	var linkQRGridCache = null; // cached module grid
	var linkQRTimer = null;
	var linkQRDesign = {
		moduleShape: 'square',
		outerEye: 'square',
		innerEye: 'square',
		moduleColor: '#000000',
		outerColor: '#000000',
		innerColor: '#000000',
	};

	/**
	 * Map module shape names to qr-code-styling dot types.
	 */
	function qrMapModuleShape( shape ) {
		var map = {
			'square': 'square',
			'circle': 'dots',
			'rounded': 'rounded',
			'diamond': 'classy',
			'star': 'classy-rounded',
			'cross': 'square',
			'blob': 'extra-rounded',
			'heart': 'extra-rounded',
		};
		return map[ shape ] || 'square';
	}

	/**
	 * Map eye shape names to qr-code-styling corner types.
	 * Only square, extra-rounded, dot are truly supported.
	 */
	function qrMapEyeShape( shape ) {
		var map = {
			'square': 'square',
			'rounded': 'extra-rounded',
			'circle': 'dot',
			'leaf': 'extra-rounded',
			'drop': 'dot',
			'dropin': 'dot',
			'dropeye': 'square',
			'flower': 'extra-rounded',
			'flurry': 'square',
			'sdoz': 'square',
			'squarecircle': 'dot',
			'dropeyeright': 'dot',
			'dropeyeleft': 'dot',
			'dropeyeleaf': 'dot',
		};
		return map[ shape ] || 'square';
	}

	/**
	 * Map inner eye shape names to qr-code-styling cornersDot types.
	 */
	function qrMapInnerEyeShape( shape ) {
		var map = {
			'square': 'square',
			'rounded': 'rounded',
			'circle': 'dot',
			'drop': 'dot',
			'dropeye': 'square',
			'dropin': 'dot',
			'flurry': 'square',
			'star': 'square',
			'diamond': 'square',
			'heart': 'dot',
			'leaf': 'square',
			'sun': 'dot',
			'cross': 'square',
			'sdoz': 'square',
		};
		return map[ shape ] || 'square';
	}

	/* ----- Custom eye shapes: SVG path data ----- */

	var QR_EYE_SHAPES = {
		outer: {
			square:       { path: 'M0,0v14h14V0H0z M12,12H2V2h10V12z' },
			rounded:      { path: 'M4.5,14h5.1C12,14,14,12,14,9.6V4.5C14,2,12,0,9.5,0H4.4C2,0,0,2,0,4.4v5.1C0,12,2,14,4.5,14z M12,4.8v4.4 c0,1.5-1.3,2.8-2.8,2.8H4.8C3.2,12,2,10.8,2,9.2V4.8C2,3.3,3.3,2,4.8,2h4.4C10.8,2,12,3.2,12,4.8z' },
			circle:       { path: 'M0,7L0,7c0,3.9,3.1,7,7,7h0c3.9,0,7-3.1,7-7v0c0-3.9-3.1-7-7-7h0C3.1,0,0,3.1,0,7z M7,12L7,12c-2.8,0-5-2.2-5-5v0 c0-2.8,2.2-5,5-5h0c2.8,0,5,2.2,5,5v0C12,9.8,9.8,12,7,12z' },
			drop:         { path: 'M0,7L0,7c0,3.9,3.1,7,7,7h7V7c0-3.9-3.1-7-7-7h0C3.1,0,0,3.1,0,7z M12,12H7c-2.8,0-5-2.2-5-5v0c0-2.8,2.2-5,5-5h0 c2.8,0,5,2.2,5,5V12z' },
			dropeye:      { path: 'M0,0l0,7c0,3.9,3.1,7,7,7h7V7c0-3.9-3.1-7-7-7H0z M12,12H7c-2.8,0-5-2.2-5-5V2h5c2.8,0,5,2.2,5,5V12z' },
			dropeyeright: { path: 'M0,0l0,7c0,3.9,3.1,7,7,7h7V7c0-3.9-3.1-7-7-7H0z M7,12L7,12c-2.8,0-5-2.2-5-5V2h5c2.8,0,5,2.2,5,5v0C12,9.8,9.8,12,7,12z' },
			dropeyeleft:  { path: 'M0,0l0,7c0,3.9,3.1,7,7,7h7V7c0-3.9-3.1-7-7-7H0z M12,12H7c-2.8,0-5-2.2-5-5v0c0-2.8,2.2-5,5-5h0c2.8,0,5,2.2,5,5V12z' },
			dropeyeleaf:  { path: 'M0,0l0,7c0,3.9,3.1,7,7,7h7V7c0-3.9-3.1-7-7-7H0z M7,12L7,12c-2.8,0-5-2.2-5-5v0c0-2.8,2.2-5,5-5h0c2.8,0,5,2.2,5,5v0 C12,9.8,9.8,12,7,12z' },
			flurry:       { path: 'M0.2,0.1L0.1,0.6l0.1,0.5L0.2,1.6L0.1,2.1l0.1,0.5L0,3.1l0,0.5L0,4l0.1,0.5l0,0.5l0.1,0.5L0.1,6l0,0.5l0,0.5 l0.2,0.5L0.1,8l0.1,0.5L0,8.9l0.2,0.5L0,9.9l0.2,0.5l-0.1,0.5l0,0.5L0,11.9l0.2,0.5l-0.1,0.5l0.1,0.5l0,0.4L0.6,14l0.5,0l0.5-0.1 l0.5,0.1l0.5,0.1L3.1,14l0.5-0.1l0.5-0.1l0.5,0L5,13.9l0.5-0.1l0.5,0l0.5,0L7,13.9L7.5,14L8,14l0.5-0.2L9,13.9L9.4,14l0.5-0.2 l0.5,0.2l0.5-0.1l0.5,0l0.5-0.1l0.5,0l0.5,0l0.5,0l0.5,0.1l-0.1-0.5l0.2-0.5l-0.1-0.5l-0.1-0.5l0.2-0.5l-0.2-0.5l0.1-0.5L14,9.9 l-0.2-0.5L13.9,9L14,8.5L14,8l-0.2-0.5l0-0.5l0.1-0.5L13.8,6l0-0.5L14,5l0-0.5l-0.1-0.5l0.1-0.5l-0.2-0.5l0.1-0.5l0-0.5l0-0.5 l-0.1-0.5L14,0.6l-0.2-0.4l-0.4,0l-0.5,0L12.4,0l-0.5,0.2l-0.5,0l-0.5-0.1l-0.5,0L10,0l-0.5,0L9,0L8.5,0.2L8,0.2L7.5,0.1L7,0.1 L6.5,0.2L6,0L5.5,0.2L5.1,0.2L4.6,0.1L4.1,0.1L3.6,0L3.1,0.1L2.6,0L2.1,0.1l-0.5,0l-0.5,0L0.6,0.2L0.2,0.1z M11.9,11.9l-0.5-0.1 L10.9,12l-0.5-0.1L10,11.9l-0.5,0.1L9,11.8l-0.5,0l-0.5,0l-0.5,0.1l-0.5,0L6.5,12L6,11.9l-0.5,0l-0.5,0L4.6,12l-0.5-0.2L3.6,12 l-0.5-0.1L2.6,12l-0.4-0.1L2,11.4l0.1-0.5l0-0.5l0.1-0.5L2,9.5L2.1,9L2,8.5L2,8l0.2-0.5l0-0.5L2,6.5L2.1,6l0.1-0.5L2.2,5L2.1,4.5 L2,4.1l0-0.5l0.2-0.5L2,2.6L2,2l0.5,0l0.5,0.1l0.5,0.1l0.5,0L4.5,2L5,2.1l0.5,0.1L6,2l0.5,0.2L7,2.1l0.5,0L8,2l0.5,0L9,2l0.5,0 l0.5,0.1L10.4,2l0.5,0.2L11.4,2L12,2l-0.1,0.6L12,3.1l-0.1,0.5L11.8,4L12,4.5L11.9,5l-0.1,0.5l0,0.5L12,6.5L12,7l-0.1,0.5L12,8 l0,0.5l-0.2,0.5l0,0.5l0.1,0.5l-0.1,0.5l0.2,0.5l-0.1,0.5L11.9,11.9z' },
			dropin:       { path: 'M0,0l0,7c0,3.9,3.1,7,7,7h0c3.9,0,7-3.1,7-7v0c0-3.9-3.1-7-7-7H0z M7,12L7,12c-2.8,0-5-2.2-5-5V2h5c2.8,0,5,2.2,5,5v0 C12,9.8,9.8,12,7,12z' },
			leaf:         { path: 'M0,0L0,10Q0,14,4,14L14,14L14,4Q14,0,10,0Z M2,2L10,2Q12,2,12,4L12,12L4,12Q2,12,2,10Z' },
			flower:       { path: 'M0,0v9.6C0,12,2,14,4.4,14h5.1C12,14,14,12,14,9.6V4.4C14,2,12,0,9.6,0H0z M9.2,12H4.8C3.3,12,2,10.7,2,9.2V2h7.2 C10.7,2,12,3.3,12,4.8v4.4C12,10.7,10.7,12,9.2,12z' },
			sdoz:         { path: 'M0,0l0.9,13c0,0.6,0.5,1,1,1h12V2c0-0.6-0.4-1-1-1L0,0z M12,12H3.8c-0.5,0-1-0.4-1-1L2,2l9,0.7c0.5,0,1,0.5,1,1 V12z' },
			squarecircle: { path: 'M0,0L14,0L14,14L0,14Z M7,2A5,5,0,1,0,7,12A5,5,0,1,0,7,2Z' },
		},
		inner: {
			square:  { path: 'M0,0L6,0L6,6L0,6Z' },
			rounded: { path: 'M1,0Q0,0,0,1L0,5Q0,6,1,6L5,6Q6,6,6,5L6,1Q6,0,5,0Z' },
			circle:  { path: 'M3,0A3,3,0,1,1,3,6A3,3,0,1,1,3,0Z' },
			drop:    { path: 'M6,6H3C1.3,6,0,4.7,0,3v0c0-1.7,1.3-3,3-3h0c1.7,0,3,1.3,3,3V6z' },
			dropeye: { path: 'M6,6H3C1.3,6,0,4.7,0,3l0-3l3,0c1.7,0,3,1.3,3,3V6z' },
			dropin:  { path: 'M3,6L3,6C1.3,6,0,4.7,0,3l0-3l3,0c1.7,0,3,1.3,3,3v0C6,4.7,4.7,6,3,6z' },
			flurry:  { path: 'M5.9,5.9 5.6,5.9 5.3,6 5,5.7 4.7,5.8 4.4,5.8 4.1,5.8 3.9,5.7 3.6,5.7 3.3,5.8 3,5.9 2.7,5.8 2.4,5.8 2.1,5.8 1.9,5.7 1.6,5.7 1.3,5.7 1,5.8 0.7,5.8 0.4,5.8 0.1,5.9 0,5.5 0.1,5.3 0,5 0.3,4.7 0.3,4.4 0.2,4.1 0.2,3.8 0.1,3.5 0.3,3.3 0.1,3 0.1,2.7 0.2,2.4 0.1,2.1 0.1,1.8 0.1,1.5 0.2,1.3 0.3,1 0,0.7 0,0.4 0.3,0.2 0.4,0.1 0.7,0.1 1,0.2 1.3,0.1 1.6,0.3 1.9,0.1 2.1,0.1 2.4,0.2 2.7,0.1 3,0.3 3.3,0.2 3.6,0.2 3.8,0.2 4.1,0.1 4.4,0.3 4.7,0.1 5,0.2 5.3,0.1 5.6,0 5.9,0 5.8,0.4 6,0.7 6,1 5.9,1.2 5.7,1.5 5.7,1.8 5.9,2.1 5.7,2.4 5.8,2.7 6,3 5.9,3.3 5.8,3.5 5.8,3.8 5.8,4.1 6,4.4 5.8,4.7 5.7,5 5.7,5.3 5.8,5.5' },
			star:    { path: 'M3.2,0.3l0.6,1.3C4,1.8,4.1,1.9,4.3,1.9l1.4,0.2c0.2,0,0.3,0.3,0.2,0.5l-1,1C4.7,3.7,4.7,3.9,4.7,4.1L5,5.5 c0,0.2-0.2,0.4-0.4,0.3L3.3,5.2c-0.2-0.1-0.4-0.1-0.6,0L1.4,5.8C1.2,5.9,1,5.8,1,5.5l0.2-1.4c0-0.2,0-0.4-0.2-0.5l-1-1 C-0.1,2.4,0,2.2,0.2,2.1l1.4-0.2c0.2,0,0.4-0.2,0.5-0.3l0.6-1.3C2.9,0.1,3.1,0.1,3.2,0.3z' },
			diamond: { path: 'M3,0L6,3L3,6L0,3Z' },
			heart:   { path: 'M1.5,1Q0,1,0,2.5Q0,3.5,3,6Q6,3.5,6,2.5Q6,1,4.5,1Q3,1,3,2Q3,1,1.5,1Z' },
			leaf:    { path: 'M0,0L0,4Q0,6,2,6L6,6L6,2Q6,0,4,0Z' },
			sun:     { path: 'M3,0 L3.4,0.7 L4,0.2 L4.1,0.9 L4.9,0.7 L4.8,1.5 L5.6,1.5 L5.2,2.2 L5.9,2.5 L5.3,3 L5.9,3.5 L5.2,3.8 L5.6,4.5 L4.8,4.5 L4.9,5.3 L4.1,5.1 L4,5.8 L3.4,5.3 L3,6 L2.5,5.3 L1.9,5.8 L1.8,5.1 L1,5.3 L1.1,4.5 L0.4,4.5 L0.7,3.8 L0,3.5 L0.6,3 L0,2.5 L0.7,2.2 L0.4,1.5 L1.1,1.5 L1,0.7 L1.8,0.9 L1.9,0.2 L2.5,0.7 Z' },
			cross:   { path: 'M6,1.5 4.5,1.5 4.5,0 1.5,0 1.5,1.5 0,1.5 0,4.5 1.5,4.5 1.5,6 4.5,6 4.5,4.5 6,4.5' },
			sdoz:    { path: 'M6,6 0.5,6 0,0 6,0.5' },
		},
	};

	/* ----- Custom module shapes: canvas drawing for unsupported types ----- */

	var CUSTOM_DOT_SHAPES = { diamond: true, star: true, cross: true, heart: true };

	var MODULE_DRAWERS = {
		diamond: function ( ctx, x, y, s ) {
			var h = s / 2;
			ctx.beginPath();
			ctx.moveTo( x + h, y );
			ctx.lineTo( x + s, y + h );
			ctx.lineTo( x + h, y + s );
			ctx.lineTo( x, y + h );
			ctx.closePath();
			ctx.fill();
		},
		star: function ( ctx, x, y, s ) {
			var cx = x + s / 2, cy = y + s / 2, or = s / 2, ir = s / 5;
			ctx.beginPath();
			for ( var i = 0; i < 10; i++ ) {
				var r = i % 2 === 0 ? or : ir;
				var a = -Math.PI / 2 + i * Math.PI / 5;
				var px = cx + r * Math.cos( a );
				var py = cy + r * Math.sin( a );
				if ( i === 0 ) { ctx.moveTo( px, py ); } else { ctx.lineTo( px, py ); }
			}
			ctx.closePath();
			ctx.fill();
		},
		cross: function ( ctx, x, y, s ) {
			var t = s * 0.35, o = ( s - t ) / 2;
			ctx.fillRect( x + o, y, t, s );
			ctx.fillRect( x, y + o, s, t );
		},
		heart: function ( ctx, x, y, s ) {
			var cx = x + s / 2;
			ctx.beginPath();
			ctx.moveTo( cx, y + s * 0.35 );
			ctx.bezierCurveTo( cx, y + s * 0.15, x, y, x, y + s * 0.35 );
			ctx.bezierCurveTo( x, y + s * 0.6, cx, y + s * 0.8, cx, y + s );
			ctx.bezierCurveTo( cx, y + s * 0.8, x + s, y + s * 0.6, x + s, y + s * 0.35 );
			ctx.bezierCurveTo( x + s, y, cx, y + s * 0.15, cx, y + s * 0.35 );
			ctx.closePath();
			ctx.fill();
		},
	};

	/**
	 * Detect QR module grid from a detection canvas (square dots, black corners).
	 */
	function qrDetectModuleGrid( canvas, eyeInfo ) {
		var ctx  = canvas.getContext( '2d' );
		var size = canvas.width;
		var data = ctx.getImageData( 0, 0, size, size ).data;
		var ms   = eyeInfo.eyeSize / 7;
		var mc   = Math.round( ( size - eyeInfo.ox * 2 ) / ms );
		var ox   = eyeInfo.ox;
		var oy   = eyeInfo.oy;
		var grid = [];

		for ( var r = 0; r < mc; r++ ) {
			grid[ r ] = [];
			for ( var c = 0; c < mc; c++ ) {
				var px = Math.round( ox + c * ms + ms / 2 );
				var py = Math.round( oy + r * ms + ms / 2 );
				if ( px >= size || py >= size ) { grid[ r ][ c ] = false; continue; }
				var i = ( py * size + px ) * 4;
				grid[ r ][ c ] = ! ( data[ i ] > 240 && data[ i + 1 ] > 240 && data[ i + 2 ] > 240 );
			}
		}
		return { grid: grid, moduleSize: ms, moduleCount: mc };
	}

	/**
	 * Redraw data modules with a custom shape (diamond, star, cross, heart).
	 * Skips eye areas and the centre logo zone.
	 */
	function qrRedrawCustomModules( canvas, gridInfo, shape, color, eyeInfo ) {
		var ctx = canvas.getContext( '2d' );
		var size = canvas.width;
		var grid = gridInfo.grid;
		var ms   = gridInfo.moduleSize;
		var mc   = gridInfo.moduleCount;
		var ox   = eyeInfo.ox;
		var oy   = eyeInfo.oy;
		var drawer = MODULE_DRAWERS[ shape ];
		if ( ! drawer ) { return; }

		// Logo exclusion zone (centre ≈ 35 % of canvas).
		var logoR = size * 0.18;
		var cx    = size / 2;
		var cy    = size / 2;

		ctx.fillStyle = color;

		for ( var r = 0; r < mc; r++ ) {
			for ( var c = 0; c < mc; c++ ) {
				if ( ! grid[ r ][ c ] ) { continue; }

				// Skip eye areas (7 × 7) + 1-module separator.
				if ( r < 8 && c < 8 )                continue; // top-left
				if ( r < 8 && c >= mc - 8 )           continue; // top-right
				if ( r >= mc - 8 && c < 8 )           continue; // bottom-left

				var px = ox + c * ms;
				var py = oy + r * ms;

				// Skip logo zone.
				if ( Math.abs( px + ms / 2 - cx ) < logoR && Math.abs( py + ms / 2 - cy ) < logoR ) {
					continue;
				}

				// Clear old square module, draw custom shape.
				ctx.clearRect( px, py, ms, ms );
				ctx.fillStyle = '#FFFFFF';
				ctx.fillRect( px, py, ms, ms );
				ctx.fillStyle = color;
				var gap = ms * 0.08;
				drawer( ctx, px + gap, py + gap, ms - gap * 2 );
			}
		}
	}

	/* ----- Canvas eye renderer — detect, clear & redraw custom shapes ----- */

	/**
	 * Detect QR eye info by scanning the canvas for the top-left eye.
	 * Returns actual pixel dimensions to avoid rounding errors.
	 */
	function qrDetectEyeInfo( canvas ) {
		var ctx  = canvas.getContext( '2d' );
		var size = canvas.width;
		var data = ctx.getImageData( 0, 0, size, size ).data;

		function isDark( x, y ) {
			if ( x < 0 || x >= size || y < 0 || y >= size ) return false;
			var i = ( y * size + x ) * 4;
			// Not white/near-white — works for any foreground color.
			return ! ( data[ i ] > 240 && data[ i + 1 ] > 240 && data[ i + 2 ] > 240 );
		}

		// Find first dark pixel in top-left quadrant.
		var sx = -1, sy = -1;
		for ( var y = 0; y < size / 4 && sx < 0; y++ ) {
			for ( var x = 0; x < size / 4; x++ ) {
				if ( isDark( x, y ) ) { sx = x; sy = y; break; }
			}
		}
		if ( sx < 0 ) {
			return { eyeSize: Math.round( size * 7 / 25 ), ox: 0, oy: 0 };
		}

		// Scan DOWN from first dark pixel to measure eye height.
		// The left edge of the outer eye is a solid dark column (7 modules).
		// Below the eye there is always a 1-module white separator.
		var eh = 0;
		for ( var y2 = sy; y2 < size / 2; y2++ ) {
			if ( isDark( sx, y2 ) ) { eh++; } else { break; }
		}

		// Sanity: eye can't be more than size / 3.
		if ( eh <= 0 || eh > size / 3 ) {
			eh = Math.round( size * 7 / 25 );
		}

		return { eyeSize: eh, ox: sx, oy: sy };
	}

	/**
	 * Draw an SVG-path shape on canvas.
	 */
	function qrDrawShape( ctx, shapeData, x, y, drawSize, color, isInner ) {
		ctx.save();
		ctx.translate( x, y );
		var scale = isInner ? drawSize / 6 : drawSize / 14;
		ctx.scale( scale, scale );

		if ( shapeData.transform ) {
			var m = shapeData.transform.match( /rotate\(([^)]+)\)/ );
			if ( m ) {
				var p  = m[1].split( /[\s,]+/ );
				var a  = parseFloat( p[0] ) * Math.PI / 180;
				var cx = parseFloat( p[1] ) || 3;
				var cy = parseFloat( p[2] ) || 3;
				ctx.translate( cx, cy );
				ctx.rotate( a );
				ctx.translate( -cx, -cy );
			}
		}

		ctx.fillStyle = color;
		var path = new Path2D( shapeData.path );
		ctx.fill( path, isInner ? 'nonzero' : 'evenodd' );
		ctx.restore();
	}

	/**
	 * Replace the three QR eyes on a canvas with custom shapes.
	 */
	function qrApplyCustomEyes( canvas, outerShape, innerShape, colors, eyeInfo ) {
		var ctx  = canvas.getContext( '2d' );
		var size = canvas.width;
		var info = eyeInfo || qrDetectEyeInfo( canvas );

		var eyeSize      = info.eyeSize;
		var innerEyeSize = Math.round( eyeSize * 3 / 7 );
		var ox           = info.ox;
		var oy           = info.oy;

		var positions = [
			{ x: ox,                  y: oy },
			{ x: size - eyeSize - ox, y: oy },
			{ x: ox,                  y: size - eyeSize - oy },
		];

		var outerData = QR_EYE_SHAPES.outer[ outerShape ] || QR_EYE_SHAPES.outer.square;
		var innerData = QR_EYE_SHAPES.inner[ innerShape ] || QR_EYE_SHAPES.inner.square;

		positions.forEach( function ( pos ) {
			// Clear eye area.
			ctx.fillStyle = '#FFFFFF';
			ctx.fillRect( pos.x, pos.y, eyeSize, eyeSize );

			// Draw outer eye.
			qrDrawShape( ctx, outerData, pos.x, pos.y, eyeSize, colors.outerColor, false );

			// Draw inner eye centred within outer.
			var off = ( eyeSize - innerEyeSize ) / 2;
			qrDrawShape( ctx, innerData, pos.x + off, pos.y + off, innerEyeSize, colors.innerColor, true );
		} );
	}

	/**
	 * Render a high-res QR on a temp canvas with custom eyes and trigger download.
	 */
	function qrDownloadAsImage( dlSize, filename ) {
		var tmp = document.createElement( 'div' );
		tmp.style.cssText = 'position:absolute;left:-9999px';
		document.body.appendChild( tmp );

		var isCustomDots = !! CUSTOM_DOT_SHAPES[ linkQRDesign.moduleShape ];

		// White corners — custom eyes drawn after.
		var config = buildQRConfig( dlSize );
		config.cornersSquareOptions = { type: 'square', color: '#FFFFFF' };
		config.cornersDotOptions    = { type: 'square', color: '#FFFFFF' };
		if ( isCustomDots ) { config.dotsOptions.type = 'square'; }

		var qr = new QRCodeStyling( config );
		qr.append( tmp );

		setTimeout( function () {
			var c = tmp.querySelector( 'canvas' );
			if ( c ) {
				// Detection canvas: black square dots & corners for measurement.
				var det2 = document.createElement( 'div' );
				det2.style.cssText = 'position:absolute;left:-9999px';
				document.body.appendChild( det2 );
				var detCfg2 = buildQRConfig( dlSize );
				detCfg2.cornersSquareOptions = { type: 'square', color: '#000000' };
				detCfg2.cornersDotOptions    = { type: 'square', color: '#000000' };
				detCfg2.dotsOptions = { type: 'square', color: '#000000' };
				delete detCfg2.image;
				var detQR2 = new QRCodeStyling( detCfg2 );
				detQR2.append( det2 );

				setTimeout( function () {
					var dc2 = det2.querySelector( 'canvas' );
					var dlEyeInfo  = null;
					var dlGridInfo = null;
					if ( dc2 ) {
						dlEyeInfo  = qrDetectEyeInfo( dc2 );
						dlGridInfo = qrDetectModuleGrid( dc2, dlEyeInfo );
					}
					document.body.removeChild( det2 );

					if ( dlEyeInfo ) {
						if ( isCustomDots && dlGridInfo ) {
							qrRedrawCustomModules( c, dlGridInfo, linkQRDesign.moduleShape, linkQRDesign.moduleColor, dlEyeInfo );
						}
						qrApplyCustomEyes( c, linkQRDesign.outerEye, linkQRDesign.innerEye, {
							outerColor: linkQRDesign.outerColor,
							innerColor: linkQRDesign.innerColor,
						}, dlEyeInfo );
					}

					c.toBlob( function ( blob ) {
						var a = document.createElement( 'a' );
						a.href = URL.createObjectURL( blob );
						a.download = filename;
						document.body.appendChild( a );
						a.click();
						document.body.removeChild( a );
						URL.revokeObjectURL( a.href );
					}, 'image/png' );
					document.body.removeChild( tmp );
				}, 300 );
			} else {
				document.body.removeChild( tmp );
			}
		}, 300 );
	}

	/**
	 * Initialize QR code with the created link URL.
	 */
	function initLinkQR( url, uuid ) {
		if ( typeof QRCodeStyling === 'undefined' ) {
			return;
		}

		linkQRUrl = url;
		linkQRUuid = uuid || '';
		linkQRLogoUrl = '';
		linkQREyeCache = null;
		linkQRGridCache = null;

		// Reset design to defaults.
		linkQRDesign = {
			moduleShape: 'square',
			outerEye: 'square',
			innerEye: 'square',
			moduleColor: '#000000',
			outerColor: '#000000',
			innerColor: '#000000',
		};

		// Reset UI controls.
		$( '#govalid-module-shapes .govalid-shape-option' ).removeClass( 'active' )
			.filter( '[data-shape="square"]' ).addClass( 'active' );
		$( '#govalid-outer-eyes .govalid-eye-option' ).removeClass( 'active' )
			.filter( '[data-eye="square"]' ).addClass( 'active' );
		$( '#govalid-inner-eyes .govalid-eye-option' ).removeClass( 'active' )
			.filter( '[data-eye="square"]' ).addClass( 'active' );
		$( '#govalid-qr-module-color' ).val( '#000000' );
		$( '#govalid-qr-outer-color' ).val( '#000000' );
		$( '#govalid-qr-inner-color' ).val( '#000000' );
		$( '#govalid-qr-logo-preview' ).hide();
		$( '#govalid-qr-logo-btn' ).show();

		// Collapse design panel.
		$( '#govalid-qr-design-body' ).hide();
		$( '.govalid-qr-toggle-icon' ).removeClass( 'govalid-qr-toggle-open' );

		renderLinkQR();
	}

	/**
	 * Render / re-render the QR code preview.
	 */
	/**
	 * Build a shared QR config object (without corner colours).
	 */
	function buildQRConfig( w ) {
		var cfg = {
			width: w, height: w, type: 'canvas',
			data: linkQRUrl,
			dotsOptions: {
				color: linkQRDesign.moduleColor,
				type: qrMapModuleShape( linkQRDesign.moduleShape ),
			},
			backgroundOptions: { color: '#FFFFFF' },
			imageOptions: {
				crossOrigin: 'anonymous',
				margin: 2,
				imageSize: 0.3,
				hideBackgroundDots: true,
			},
			qrOptions: { errorCorrectionLevel: 'H' },
		};
		if ( linkQRLogoUrl ) { cfg.image = linkQRLogoUrl; }
		return cfg;
	}

	function renderLinkQR() {
		if ( ! linkQRUrl || typeof QRCodeStyling === 'undefined' ) {
			return;
		}
		if ( linkQRTimer ) { clearTimeout( linkQRTimer ); }

		var $container = $( '#govalid-link-qr-preview' );
		$container.empty();

		var isCustomDots = !! CUSTOM_DOT_SHAPES[ linkQRDesign.moduleShape ];

		// Visible QR uses WHITE corners — the library draws them invisible.
		// Custom eyes are drawn on top without interference.
		var config = buildQRConfig( 220 );
		config.cornersSquareOptions = { type: 'square', color: '#FFFFFF' };
		config.cornersDotOptions    = { type: 'square', color: '#FFFFFF' };
		// Force square dots for custom shapes — they will be redrawn.
		if ( isCustomDots ) { config.dotsOptions.type = 'square'; }

		linkQRInstance = new QRCodeStyling( config );
		linkQRInstance.append( $container[0] );

		// If we already know the eye size for this URL, apply immediately.
		if ( linkQREyeCache ) {
			linkQRTimer = setTimeout( function () {
				var canvas = $container.find( 'canvas' )[0];
				if ( canvas ) {
					// Redraw custom modules first (skips eye areas).
					if ( isCustomDots && linkQRGridCache ) {
						qrRedrawCustomModules( canvas, linkQRGridCache, linkQRDesign.moduleShape, linkQRDesign.moduleColor, linkQREyeCache );
					}
					qrApplyCustomEyes( canvas, linkQRDesign.outerEye, linkQRDesign.innerEye, {
						outerColor: linkQRDesign.outerColor,
						innerColor: linkQRDesign.innerColor,
					}, linkQREyeCache );
				}
			}, 200 );
			return;
		}

		// First time: run a hidden detection render with BLACK square dots.
		var det = document.createElement( 'div' );
		det.style.cssText = 'position:absolute;left:-9999px';
		document.body.appendChild( det );

		var detCfg = buildQRConfig( 220 );
		detCfg.cornersSquareOptions = { type: 'square', color: '#000000' };
		detCfg.cornersDotOptions    = { type: 'square', color: '#000000' };
		detCfg.dotsOptions = { type: 'square', color: '#000000' };
		// No logo for detection — avoids image-load delays.
		delete detCfg.image;

		var detQR = new QRCodeStyling( detCfg );
		detQR.append( det );

		linkQRTimer = setTimeout( function () {
			var dc = det.querySelector( 'canvas' );
			if ( dc ) {
				linkQREyeCache  = qrDetectEyeInfo( dc );
				linkQRGridCache = qrDetectModuleGrid( dc, linkQREyeCache );
			}
			document.body.removeChild( det );

			// Now apply on the visible canvas.
			var canvas = $container.find( 'canvas' )[0];
			if ( canvas && linkQREyeCache ) {
				if ( isCustomDots && linkQRGridCache ) {
					qrRedrawCustomModules( canvas, linkQRGridCache, linkQRDesign.moduleShape, linkQRDesign.moduleColor, linkQREyeCache );
				}
				qrApplyCustomEyes( canvas, linkQRDesign.outerEye, linkQRDesign.innerEye, {
					outerColor: linkQRDesign.outerColor,
					innerColor: linkQRDesign.innerColor,
				}, linkQREyeCache );
			}
		}, 350 );
	}

	/**
	 * Reset QR state when creating another link.
	 */
	function resetLinkQR() {
		if ( linkQRTimer ) { clearTimeout( linkQRTimer ); }
		linkQRInstance = null;
		linkQRUrl = '';
		linkQRUuid = '';
		linkQRLogoUrl = '';
		linkQREyeCache = null;
		linkQRGridCache = null;
		$( '#govalid-link-qr-preview' ).empty();
	}

	/**
	 * Toggle design panel.
	 */
	$( document ).on( 'click', '#govalid-qr-design-toggle', function () {
		var $body = $( '#govalid-qr-design-body' );
		var $icon = $( '.govalid-qr-toggle-icon' );
		$body.slideToggle( 200 );
		$icon.toggleClass( 'govalid-qr-toggle-open' );
	} );

	/**
	 * Module shape selection.
	 */
	$( document ).on( 'click', '#govalid-module-shapes .govalid-shape-option', function () {
		$( '#govalid-module-shapes .govalid-shape-option' ).removeClass( 'active' );
		$( this ).addClass( 'active' );
		linkQRDesign.moduleShape = $( this ).data( 'shape' );
		renderLinkQR();
	} );

	/**
	 * Outer eye shape selection.
	 */
	$( document ).on( 'click', '#govalid-outer-eyes .govalid-eye-option', function () {
		$( '#govalid-outer-eyes .govalid-eye-option' ).removeClass( 'active' );
		$( this ).addClass( 'active' );
		linkQRDesign.outerEye = $( this ).data( 'eye' );
		renderLinkQR();
	} );

	/**
	 * Inner eye shape selection.
	 */
	$( document ).on( 'click', '#govalid-inner-eyes .govalid-eye-option', function () {
		$( '#govalid-inner-eyes .govalid-eye-option' ).removeClass( 'active' );
		$( this ).addClass( 'active' );
		linkQRDesign.innerEye = $( this ).data( 'eye' );
		renderLinkQR();
	} );

	/**
	 * Color picker changes.
	 */
	$( document ).on( 'input', '#govalid-qr-module-color', function () {
		linkQRDesign.moduleColor = $( this ).val();
		renderLinkQR();
	} );

	$( document ).on( 'input', '#govalid-qr-outer-color', function () {
		linkQRDesign.outerColor = $( this ).val();
		renderLinkQR();
	} );

	$( document ).on( 'input', '#govalid-qr-inner-color', function () {
		linkQRDesign.innerColor = $( this ).val();
		renderLinkQR();
	} );

	/**
	 * Logo upload via WP Media Library.
	 */
	$( document ).on( 'click', '#govalid-qr-logo-btn', function ( e ) {
		e.preventDefault();

		if ( typeof wp === 'undefined' || typeof wp.media === 'undefined' ) {
			showNotice( 'Media library not available.' );
			return;
		}

		var frame = wp.media( {
			title: 'Select Center Logo',
			button: { text: 'Use as Logo' },
			multiple: false,
			library: { type: 'image' },
		} );

		frame.on( 'select', function () {
			var attachment = frame.state().get( 'selection' ).first().toJSON();
			linkQRLogoUrl = attachment.url;
			$( '#govalid-qr-logo-img' ).attr( 'src', attachment.url );
			$( '#govalid-qr-logo-preview' ).show();
			renderLinkQR();
		} );

		frame.open();
	} );

	/**
	 * Remove logo.
	 */
	$( document ).on( 'click', '#govalid-qr-logo-remove', function () {
		linkQRLogoUrl = '';
		$( '#govalid-qr-logo-preview' ).hide();
		renderLinkQR();
	} );

	/**
	 * Download QR as PNG (500px).
	 */
	$( document ).on( 'click', '#govalid-qr-download-png', function () {
		if ( linkQRUrl ) {
			qrDownloadAsImage( 500, 'qr-code.png' );
		}
	} );

	/**
	 * Download QR as high-res PNG (1000px).
	 */
	$( document ).on( 'click', '#govalid-qr-download-svg', function () {
		if ( linkQRUrl ) {
			qrDownloadAsImage( 1000, 'qr-code-hd.png' );
		}
	} );

	/**
	 * Apply a saved design object to the UI controls and state.
	 */
	function applyLinkQRDesign( design ) {
		if ( ! design ) {
			return;
		}

		linkQRDesign.moduleShape = design.moduleShape || 'square';
		linkQRDesign.outerEye   = design.outerEye || 'square';
		linkQRDesign.innerEye   = design.innerEye || 'square';
		linkQRDesign.moduleColor = design.moduleColor || '#000000';
		linkQRDesign.outerColor  = design.outerColor || '#000000';
		linkQRDesign.innerColor  = design.innerColor || '#000000';
		linkQRLogoUrl = design.logoUrl || '';

		// Update UI controls.
		$( '#govalid-module-shapes .govalid-shape-option' ).removeClass( 'active' )
			.filter( '[data-shape="' + linkQRDesign.moduleShape + '"]' ).addClass( 'active' );
		$( '#govalid-outer-eyes .govalid-eye-option' ).removeClass( 'active' )
			.filter( '[data-eye="' + linkQRDesign.outerEye + '"]' ).addClass( 'active' );
		$( '#govalid-inner-eyes .govalid-eye-option' ).removeClass( 'active' )
			.filter( '[data-eye="' + linkQRDesign.innerEye + '"]' ).addClass( 'active' );
		$( '#govalid-qr-module-color' ).val( linkQRDesign.moduleColor );
		$( '#govalid-qr-outer-color' ).val( linkQRDesign.outerColor );
		$( '#govalid-qr-inner-color' ).val( linkQRDesign.innerColor );

		if ( linkQRLogoUrl ) {
			$( '#govalid-qr-logo-img' ).attr( 'src', linkQRLogoUrl );
			$( '#govalid-qr-logo-preview' ).show();
		} else {
			$( '#govalid-qr-logo-preview' ).hide();
			$( '#govalid-qr-logo-btn' ).show();
		}
	}

	/**
	 * QR button in links table — show QR designer for an existing link.
	 */
	$( document ).on( 'click', '.govalid-link-qr-btn', function () {
		var url  = $( this ).data( 'url' );
		var uuid = $( this ).data( 'uuid' );
		if ( ! url ) {
			return;
		}

		// Populate the result card.
		$( '#govalid-link-result-heading' ).text( 'Link QR Code' );
		$( '#govalid-link-result-url' ).text( url );
		$( '#govalid-copy-link-btn' ).data( 'clipboard', url );

		// Hide form/suggestions and show result.
		$( '#govalid-link-form-card' ).hide();
		$( '#govalid-suggestions-card' ).hide();
		$( '#govalid-link-result' ).show();
		$( '#govalid-back-to-list-btn' ).show();

		// Initialize QR with defaults first.
		initLinkQR( url, uuid );

		// Load saved design if available.
		if ( uuid ) {
			$.ajax( {
				url: govalidQR.restUrl + 'links/' + encodeURIComponent( uuid ) + '/qr-design',
				method: 'GET',
				beforeSend: function ( xhr ) {
					xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
				},
				success: function ( resp ) {
					if ( resp.design ) {
						applyLinkQRDesign( resp.design );
						renderLinkQR();
					}
				},
			} );
		}

		// Scroll to the result card.
		$( 'html, body' ).animate( { scrollTop: $( '#govalid-link-result' ).offset().top - 40 }, 300 );
	} );

	/**
	 * Save QR design to the server.
	 */
	$( document ).on( 'click', '#govalid-qr-save-design', function () {
		if ( ! linkQRUuid ) {
			showNotice( 'Cannot save design — no link ID.' );
			return;
		}

		var $btn = $( this );
		$btn.prop( 'disabled', true );

		var designData = {
			moduleShape: linkQRDesign.moduleShape,
			outerEye:    linkQRDesign.outerEye,
			innerEye:    linkQRDesign.innerEye,
			moduleColor: linkQRDesign.moduleColor,
			outerColor:  linkQRDesign.outerColor,
			innerColor:  linkQRDesign.innerColor,
			logoUrl:     linkQRLogoUrl,
		};

		$.ajax( {
			url: govalidQR.restUrl + 'links/' + encodeURIComponent( linkQRUuid ) + '/qr-design',
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( designData ),
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function () {
				showNotice( 'QR design saved!' );
			},
			error: function () {
				showNotice( govalidQR.i18n.error );
			},
			complete: function () {
				$btn.prop( 'disabled', false );
			},
		} );
	} );

	/**
	 * Back to list — hide result card and show form + scroll to list.
	 */
	$( document ).on( 'click', '#govalid-back-to-list-btn', function () {
		$( '#govalid-link-result' ).hide();
		$( '#govalid-link-form-card' ).show();
		resetLinkQR();
		$( 'html, body' ).animate( { scrollTop: $( '#govalid-link-form-card' ).offset().top - 40 }, 200 );
	} );

	/* =========================================================
	   Timeline — dynamic history entries + map
	   ========================================================= */

	var timelineEntryCount = 1;

	/**
	 * Load anti-counterfeit tags for timeline form.
	 */
	if ( $( '#existing_tags_tl' ).length ) {
		$.ajax( {
			url: govalidQR.restUrl + 'tags',
			method: 'GET',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( resp ) {
				var tags = ( resp.data && resp.data.tags ) || [];
				if ( tags.length ) {
					var $container = $( '#existing_tags_tl .govalid-tag-chips' );
					$.each( tags, function ( i, tag ) {
						$container.append(
							'<button type="button" class="govalid-tag-chip" data-tag="' + $( '<span>' ).text( tag ).html() + '" data-input="anti_counterfeit_tags_tl">' +
							'<span class="dashicons dashicons-plus-alt2" style="font-size:13px;width:13px;height:13px;line-height:13px;"></span> ' +
							$( '<span>' ).text( tag ).html() +
							'</button>'
						);
					} );
					$( '#existing_tags_tl' ).show();
				}
			},
		} );
	}

	/**
	 * Click tag chip — support both cert and timeline via data-input attribute.
	 */
	$( document ).off( 'click', '.govalid-tag-chip' ).on( 'click', '.govalid-tag-chip', function () {
		var tag = $( this ).data( 'tag' );
		var inputId = $( this ).data( 'input' ) || 'anti_counterfeit_tags_cert';
		var $input = $( '#' + inputId );
		var current = $input.val().trim();
		var existing = current ? current.split( ',' ).map( function ( t ) { return t.trim().toLowerCase(); } ) : [];

		if ( existing.indexOf( tag.toLowerCase() ) !== -1 ) {
			return;
		}

		$input.val( current ? current + ', ' + tag : tag );
		$( this ).addClass( 'govalid-tag-added' );
	} );

	/**
	 * Add new timeline history entry.
	 */
	$( document ).on( 'click', '#govalid-add-timeline-entry', function () {
		timelineEntryCount++;
		var idx = timelineEntryCount;
		var arrIdx = idx - 1; // 0-based for form array naming.

		var html = '<div class="govalid-timeline-entry" data-entry-id="' + idx + '">'
			+ '<div class="govalid-timeline-entry-header">'
			+ '<span class="govalid-timeline-entry-title">'
			+ '<span class="dashicons dashicons-clock"></span> '
			+ ( govalidQR.i18n.history_entry || 'History Entry' ) + ' #' + idx
			+ '</span>'
			+ '<button type="button" class="govalid-btn govalid-btn-xs govalid-btn-danger-outline govalid-remove-timeline-entry" data-entry-id="' + idx + '">'
			+ '<span class="dashicons dashicons-trash"></span> '
			+ ( govalidQR.i18n.remove || 'Remove' )
			+ '</button>'
			+ '</div>'

			// Create Date (required)
			+ '<div class="govalid-field-group">'
			+ '<label class="govalid-field-label" for="field_create_date_tl_' + idx + '">'
			+ ( govalidQR.i18n.create_date || 'Create Date' ) + ' <span class="govalid-required">*</span>'
			+ '</label>'
			+ '<div class="govalid-input-wrap">'
			+ '<span class="govalid-input-icon"><span class="dashicons dashicons-calendar-alt"></span></span>'
			+ '<input type="date" id="field_create_date_tl_' + idx + '" name="timeline_entries[' + arrIdx + '][create_date]" class="govalid-input" required />'
			+ '</div>'
			+ '</div>'

			// Description
			+ '<div class="govalid-field-group govalid-toggleable">'
			+ '<div class="govalid-toggle-header">'
			+ '<label class="govalid-switch">'
			+ '<input type="checkbox" class="govalid-field-toggle" data-target="container_description_tl_' + idx + '" checked />'
			+ '<span class="govalid-switch-slider"></span>'
			+ '</label>'
			+ '<span class="govalid-toggle-label">' + ( govalidQR.i18n.description || 'Description' ) + '</span>'
			+ '</div>'
			+ '<div id="container_description_tl_' + idx + '" class="govalid-toggle-body">'
			+ '<div class="govalid-input-wrap">'
			+ '<span class="govalid-input-icon"><span class="dashicons dashicons-editor-alignleft"></span></span>'
			+ '<textarea id="field_description_tl_' + idx + '" name="timeline_entries[' + arrIdx + '][description]" class="govalid-input govalid-textarea" rows="3" placeholder="' + ( govalidQR.i18n.describe_event || 'Describe this timeline event' ) + '"></textarea>'
			+ '</div>'
			+ '</div>'
			+ '</div>'

			// Upload Media
			+ '<div class="govalid-field-group govalid-toggleable">'
			+ '<div class="govalid-toggle-header">'
			+ '<label class="govalid-switch">'
			+ '<input type="checkbox" class="govalid-field-toggle" data-target="container_upload_tl_' + idx + '" checked />'
			+ '<span class="govalid-switch-slider"></span>'
			+ '</label>'
			+ '<span class="govalid-toggle-label">' + ( govalidQR.i18n.upload_media || 'Upload Media' ) + '</span>'
			+ '</div>'
			+ '<div id="container_upload_tl_' + idx + '" class="govalid-toggle-body">'
			+ '<div class="govalid-file-upload" id="file_upload_area_tl_' + idx + '">'
			+ '<input type="file" id="field_upload_tl_' + idx + '" name="timeline_entries_media_' + arrIdx + '" class="govalid-file-input" accept=".jpg,.jpeg,.png,.pdf" />'
			+ '<div class="govalid-file-upload-placeholder">'
			+ '<span class="dashicons dashicons-cloud-upload"></span>'
			+ '<p>' + ( govalidQR.i18n.drag_drop || 'Drag & drop or click to upload' ) + '</p>'
			+ '<small>' + ( govalidQR.i18n.file_types || 'JPG, PNG, PDF (max 5MB)' ) + '</small>'
			+ '</div>'
			+ '<div class="govalid-file-preview" style="display:none;">'
			+ '<span class="govalid-file-preview-name"></span>'
			+ '<button type="button" class="govalid-file-remove"><span class="dashicons dashicons-no-alt"></span></button>'
			+ '</div>'
			+ '</div>'
			+ '</div>'
			+ '</div>'

			// Location
			+ '<div class="govalid-field-group govalid-toggleable">'
			+ '<div class="govalid-toggle-header">'
			+ '<label class="govalid-switch">'
			+ '<input type="checkbox" class="govalid-field-toggle" data-target="container_map_tl_' + idx + '" />'
			+ '<span class="govalid-switch-slider"></span>'
			+ '</label>'
			+ '<span class="govalid-toggle-label">' + ( govalidQR.i18n.location || 'Location' ) + '</span>'
			+ '</div>'
			+ '<div id="container_map_tl_' + idx + '" class="govalid-toggle-body" style="display:none;">'
			+ '<div class="govalid-map-wrap" id="map_wrap_tl_' + idx + '">'
			+ '<div class="govalid-map-container" id="leaflet-map-tl-' + idx + '" style="height:300px;"></div>'
			+ '<div class="govalid-map-overlay" id="map_overlay_tl_' + idx + '">'
			+ '<button type="button" class="govalid-btn govalid-btn-primary govalid-load-map-btn" data-entry-id="' + idx + '">'
			+ '<span class="dashicons dashicons-location"></span> '
			+ ( govalidQR.i18n.load_map || 'Load Map' )
			+ '</button>'
			+ '</div>'
			+ '<div class="govalid-map-search" id="map_search_tl_' + idx + '" style="display:none;">'
			+ '<div class="govalid-input-wrap">'
			+ '<input type="text" class="govalid-input govalid-map-search-input" placeholder="' + ( govalidQR.i18n.search_location || 'Search for a location...' ) + '" />'
			+ '<button type="button" class="govalid-input-action govalid-map-search-btn"><span class="dashicons dashicons-search"></span></button>'
			+ '</div>'
			+ '</div>'
			+ '<div class="govalid-map-locate" id="map_locate_tl_' + idx + '" style="display:none;">'
			+ '<button type="button" class="govalid-btn govalid-btn-sm govalid-btn-primary govalid-get-location-btn" data-entry-id="' + idx + '">'
			+ '<span class="dashicons dashicons-location-alt"></span>'
			+ '</button>'
			+ '</div>'
			+ '</div>'
			+ '<div class="govalid-location-status" id="location_status_tl_' + idx + '" style="display:none;">'
			+ '<div class="govalid-location-status-inner">'
			+ '<span class="dashicons dashicons-location"></span>'
			+ '<div class="govalid-location-info">'
			+ '<span class="govalid-location-address" id="location_address_tl_' + idx + '"></span>'
			+ '<span class="govalid-location-coords" id="location_coords_tl_' + idx + '"></span>'
			+ '</div>'
			+ '<button type="button" class="govalid-btn govalid-btn-xs govalid-btn-danger-outline govalid-clear-location-btn" data-entry-id="' + idx + '">'
			+ '<span class="dashicons dashicons-no-alt"></span>'
			+ '</button>'
			+ '</div>'
			+ '</div>'
			+ '<input type="hidden" id="field_map_lat_tl_' + idx + '" name="timeline_entries[' + arrIdx + '][map_lat]" value="" />'
			+ '<input type="hidden" id="field_map_lng_tl_' + idx + '" name="timeline_entries[' + arrIdx + '][map_lng]" value="" />'
			+ '<input type="hidden" id="field_map_address_tl_' + idx + '" name="timeline_entries[' + arrIdx + '][map_address]" value="" />'
			+ '</div>'
			+ '</div>'

			+ '</div>';

		$( '#timeline-entries-container' ).append( html );

		// Scroll to new entry.
		var $newEntry = $( '.govalid-timeline-entry[data-entry-id="' + idx + '"]' );
		$( 'html, body' ).animate( { scrollTop: $newEntry.offset().top - 60 }, 300 );
	} );

	/**
	 * Remove a timeline history entry.
	 */
	$( document ).on( 'click', '.govalid-remove-timeline-entry', function () {
		var entryId = $( this ).data( 'entry-id' );
		var totalEntries = $( '.govalid-timeline-entry' ).length;

		if ( totalEntries <= 1 ) {
			showNotice( govalidQR.i18n.min_one_entry || 'At least one history entry is required.', 'error' );
			return;
		}

		$( '.govalid-timeline-entry[data-entry-id="' + entryId + '"]' ).slideUp( 200, function () {
			$( this ).remove();
			// Re-number remaining entries.
			$( '.govalid-timeline-entry' ).each( function ( i ) {
				var newIdx = i + 1;
				$( this ).attr( 'data-entry-id', newIdx );
				$( this ).find( '.govalid-timeline-entry-title' ).html(
					'<span class="dashicons dashicons-clock"></span> '
					+ ( govalidQR.i18n.history_entry || 'History Entry' ) + ' #' + newIdx
				);
				$( this ).find( '.govalid-remove-timeline-entry' ).attr( 'data-entry-id', newIdx );

				// Update name attributes for form array indexing.
				$( this ).find( '[name^="timeline_entries["]' ).each( function () {
					var name = $( this ).attr( 'name' );
					$( this ).attr( 'name', name.replace( /timeline_entries\[\d+\]/, 'timeline_entries[' + i + ']' ) );
				} );
				$( this ).find( '[name^="timeline_entries_media_"]' ).each( function () {
					$( this ).attr( 'name', 'timeline_entries_media_' + i );
				} );
			} );
		} );
	} );

	/**
	 * Timeline map — stored map instances per entry.
	 */
	var timelineMaps = {};

	/**
	 * Load Map button — initialize Leaflet map for a timeline entry.
	 */
	$( document ).on( 'click', '.govalid-load-map-btn', function () {
		var entryId = $( this ).data( 'entry-id' );
		var mapContainerId = 'leaflet-map-tl-' + entryId;

		// Check if Leaflet is available.
		if ( typeof L === 'undefined' ) {
			showNotice( govalidQR.i18n.map_unavailable || 'Map library not loaded.', 'error' );
			return;
		}

		// Hide overlay, show search + locate.
		$( '#map_overlay_tl_' + entryId ).hide();
		$( '#map_search_tl_' + entryId ).show();
		$( '#map_locate_tl_' + entryId ).show();

		// Initialize map.
		var map = L.map( mapContainerId ).setView( [ 51.505, -0.09 ], 13 );
		L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
			attribution: '&copy; OpenStreetMap contributors',
			maxZoom: 19,
		} ).addTo( map );

		var marker = L.marker( [ 51.505, -0.09 ], { draggable: true } ).addTo( map );

		// Store reference.
		timelineMaps[ entryId ] = { map: map, marker: marker };

		// Fix map size after container becomes visible.
		setTimeout( function () { map.invalidateSize(); }, 200 );

		// Marker drag end — update fields.
		marker.on( 'dragend', function () {
			var pos = marker.getLatLng();
			updateMapFields( entryId, pos.lat, pos.lng, '' );
			reverseGeocode( pos.lat, pos.lng, entryId );
		} );

		// Map click — move marker.
		map.on( 'click', function ( e ) {
			marker.setLatLng( e.latlng );
			updateMapFields( entryId, e.latlng.lat, e.latlng.lng, '' );
			reverseGeocode( e.latlng.lat, e.latlng.lng, entryId );
		} );
	} );

	/**
	 * Map search button.
	 */
	$( document ).on( 'click', '.govalid-map-search-btn', function () {
		var $wrap = $( this ).closest( '.govalid-map-search' );
		var query = $wrap.find( '.govalid-map-search-input' ).val().trim();
		var entryId = $wrap.closest( '.govalid-timeline-entry' ).data( 'entry-id' );

		if ( ! query || ! timelineMaps[ entryId ] ) return;

		searchLocationNominatim( query, function ( result ) {
			if ( result ) {
				var mapObj = timelineMaps[ entryId ];
				mapObj.map.setView( [ result.lat, result.lng ], 15 );
				mapObj.marker.setLatLng( [ result.lat, result.lng ] );
				updateMapFields( entryId, result.lat, result.lng, result.display_name );
			} else {
				showNotice( govalidQR.i18n.location_not_found || 'Location not found.' );
			}
		} );
	} );

	/**
	 * Enter key in search input triggers search.
	 */
	$( document ).on( 'keypress', '.govalid-map-search-input', function ( e ) {
		if ( e.which === 13 ) {
			e.preventDefault();
			$( this ).siblings( '.govalid-map-search-btn' ).trigger( 'click' );
		}
	} );

	/**
	 * Get current location button.
	 */
	$( document ).on( 'click', '.govalid-get-location-btn', function () {
		var entryId = $( this ).data( 'entry-id' );
		if ( ! navigator.geolocation || ! timelineMaps[ entryId ] ) return;

		navigator.geolocation.getCurrentPosition( function ( position ) {
			var lat = position.coords.latitude;
			var lng = position.coords.longitude;
			var mapObj = timelineMaps[ entryId ];
			mapObj.map.setView( [ lat, lng ], 15 );
			mapObj.marker.setLatLng( [ lat, lng ] );
			updateMapFields( entryId, lat, lng, '' );
			reverseGeocode( lat, lng, entryId );
		}, function () {
			showNotice( govalidQR.i18n.location_denied || 'Could not get your location.' );
		} );
	} );

	/**
	 * Clear location.
	 */
	$( document ).on( 'click', '.govalid-clear-location-btn', function () {
		var entryId = $( this ).data( 'entry-id' );
		$( '#field_map_lat_tl_' + entryId ).val( '' );
		$( '#field_map_lng_tl_' + entryId ).val( '' );
		$( '#field_map_address_tl_' + entryId ).val( '' );
		$( '#location_status_tl_' + entryId ).slideUp( 200 );
	} );

	/**
	 * Update hidden map fields and show location status.
	 */
	function updateMapFields( entryId, lat, lng, address ) {
		$( '#field_map_lat_tl_' + entryId ).val( lat );
		$( '#field_map_lng_tl_' + entryId ).val( lng );
		$( '#field_map_address_tl_' + entryId ).val( address );
		$( '#location_address_tl_' + entryId ).text( address || 'Selected location' );
		$( '#location_coords_tl_' + entryId ).text( parseFloat( lat ).toFixed( 5 ) + ', ' + parseFloat( lng ).toFixed( 5 ) );
		$( '#location_status_tl_' + entryId ).slideDown( 200 );
	}

	/**
	 * Reverse geocode using OpenStreetMap Nominatim.
	 */
	function reverseGeocode( lat, lng, entryId ) {
		var url = 'https://nominatim.openstreetmap.org/reverse?format=json&lat=' + lat + '&lon=' + lng + '&zoom=18';
		$.getJSON( url, function ( data ) {
			if ( data && data.display_name ) {
				$( '#field_map_address_tl_' + entryId ).val( data.display_name );
				$( '#location_address_tl_' + entryId ).text( data.display_name );
			}
		} );
	}

	/**
	 * Search location via OpenStreetMap Nominatim.
	 */
	function searchLocationNominatim( query, callback ) {
		var url = 'https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent( query ) + '&limit=1';
		$.getJSON( url, function ( data ) {
			if ( data && data.length ) {
				callback( {
					lat: parseFloat( data[0].lat ),
					lng: parseFloat( data[0].lon ),
					display_name: data[0].display_name,
				} );
			} else {
				callback( null );
			}
		} ).fail( function () {
			callback( null );
		} );
	}

	/**
	 * Timeline form submission validation.
	 */
	$( document ).on( 'submit', '#govalid-form-timeline', function ( e ) {
		var errors = [];

		// Check QR name.
		if ( ! $( '#qr_name_tl' ).val().trim() ) {
			errors.push( govalidQR.i18n.qr_name_required || 'QR Code Name is required.' );
		}

		// Validate identifiers (max 50 chars each, max 999 entries).
		var identifierEnabled = $( '#container_identifier_tl' ).is( ':visible' );
		if ( identifierEnabled ) {
			var idVal = $( '#field_identifier_tl' ).val().trim();
			if ( idVal ) {
				var identifiers = idVal.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } );
				if ( identifiers.length > 999 ) {
					errors.push( 'Maximum 999 identifiers allowed. You have ' + identifiers.length + '.' );
				}
				for ( var ii = 0; ii < identifiers.length; ii++ ) {
					if ( identifiers[ ii ].trim().length > 50 ) {
						errors.push( 'Identifier "' + identifiers[ ii ].trim().substring( 0, 20 ) + '..." exceeds 50 character limit.' );
						break;
					}
				}
			}
		}

		// Check each entry has a create date.
		$( '.govalid-timeline-entry' ).each( function () {
			var entryId = $( this ).data( 'entry-id' );
			var dateField = $( this ).find( 'input[type="date"]' );
			if ( ! dateField.val() ) {
				errors.push( ( govalidQR.i18n.create_date_required || 'Create date is required for History Entry #' ) + entryId );
			}
		} );

		if ( errors.length ) {
			e.preventDefault();
			showNotice( errors[0], 'error' );
			// Scroll to first problem.
			var $firstInvalid = $( '.govalid-timeline-entry' ).find( 'input[type="date"]' ).filter( function () {
				return ! $( this ).val();
			} ).first();
			if ( $firstInvalid.length ) {
				$( 'html, body' ).animate( { scrollTop: $firstInvalid.offset().top - 60 }, 300 );
			}
			return false;
		}
	} );

	/**
	 * Product (Goods) form submission validation.
	 */
	$( document ).on( 'submit', '#govalid-form-product', function ( e ) {
		var errors = [];

		// Check QR name.
		if ( ! $( '#qr_name_prod' ).val().trim() ) {
			errors.push( govalidQR.i18n.qr_name_required || 'QR Code Name is required.' );
		}

		// Validate identifiers (max 50 chars each, max 999 entries).
		var identifierEnabled = $( '#container_identifier_prod' ).is( ':visible' );
		if ( identifierEnabled ) {
			var idVal = $( '#field_identifier_prod' ).val().trim();
			if ( idVal ) {
				var identifiers = idVal.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } );
				if ( identifiers.length > 999 ) {
					errors.push( 'Maximum 999 identifiers allowed. You have ' + identifiers.length + '.' );
				}
				for ( var ii = 0; ii < identifiers.length; ii++ ) {
					if ( identifiers[ ii ].trim().length > 50 ) {
						errors.push( 'Identifier "' + identifiers[ ii ].trim().substring( 0, 20 ) + '..." exceeds 50 character limit.' );
						break;
					}
				}
			}
		}

		if ( errors.length ) {
			e.preventDefault();
			showNotice( errors[0], 'error' );
			$( 'html, body' ).animate( { scrollTop: $( '#qr_name_prod' ).offset().top - 60 }, 300 );
			return false;
		}
	} );

	/**
	 * Product form — password protection toggle.
	 */
	$( document ).on( 'change', '#toggle_password_prod', function () {
		$( '#hidden_is_password_protected_product' ).val( this.checked ? 'true' : 'false' );
	} );
	$( document ).on( 'change', '#govalid-form-product input[name="password_type"]', function () {
		if ( $( this ).val() === 'unique' ) {
			$( '#unique_password_container_prod' ).slideDown( 200 );
		} else {
			$( '#unique_password_container_prod' ).slideUp( 200 );
		}
	} );

	/**
	 * Product form — removal date/time combiner.
	 */
	$( document ).on( 'change', '#field_removal_date_prod, #field_removal_time_prod', function () {
		var date = $( '#field_removal_date_prod' ).val();
		var time = $( '#field_removal_time_prod' ).val() || '00:00';
		$( '#combined_removal_datetime_prod' ).val( date ? date + 'T' + time : '' );
	} );

	/**
	 * Product form — anti-counterfeit tags loader.
	 */
	$( document ).on( 'change', '#toggle_anti_counterfeit_prod', function () {
		if ( this.checked && ! $( '#existing_tags_prod' ).data( 'loaded' ) ) {
			loadExistingTags( '#existing_tags_prod', 'anti_counterfeit_tags_prod' );
		}
	} );

	/* =========================================================
	   Document form handlers
	   ========================================================= */

	/**
	 * Document form submission validation.
	 */
	$( document ).on( 'submit', '#govalid-form-document', function ( e ) {
		var errors = [];

		// Check QR name.
		if ( ! $( '#qr_name_doc' ).val().trim() ) {
			errors.push( govalidQR.i18n.qr_name_required || 'QR Code Name is required.' );
		}

		// Validate identifiers (max 50 chars each, max 999 entries).
		var identifierEnabled = $( '#container_identifier_doc' ).is( ':visible' );
		if ( identifierEnabled ) {
			var idVal = $( '#field_identifier_doc' ).val().trim();
			if ( idVal ) {
				var identifiers = idVal.split( /[\n;]+/ ).filter( function ( s ) { return s.trim() !== ''; } );
				if ( identifiers.length > 999 ) {
					errors.push( 'Maximum 999 identifiers allowed. You have ' + identifiers.length + '.' );
				}
				for ( var ii = 0; ii < identifiers.length; ii++ ) {
					if ( identifiers[ ii ].trim().length > 50 ) {
						errors.push( 'Identifier "' + identifiers[ ii ].trim().substring( 0, 20 ) + '..." exceeds 50 character limit.' );
						break;
					}
				}
			}
		}

		if ( errors.length ) {
			e.preventDefault();
			showNotice( errors[0], 'error' );
			$( 'html, body' ).animate( { scrollTop: $( '#qr_name_doc' ).offset().top - 60 }, 300 );
			return false;
		}
	} );

	/**
	 * Document form — password protection toggle.
	 */
	$( document ).on( 'change', '#toggle_password_doc', function () {
		$( '#hidden_is_password_protected_document' ).val( this.checked ? 'true' : 'false' );
	} );
	$( document ).on( 'change', '#govalid-form-document input[name="password_type"]', function () {
		if ( $( this ).val() === 'unique' ) {
			$( '#unique_password_container_doc' ).slideDown( 200 );
		} else {
			$( '#unique_password_container_doc' ).slideUp( 200 );
		}
	} );

	/**
	 * Document form — removal date/time combiner.
	 */
	$( document ).on( 'change', '#field_removal_date_doc, #field_removal_time_doc', function () {
		var date = $( '#field_removal_date_doc' ).val();
		var time = $( '#field_removal_time_doc' ).val() || '00:00';
		$( '#combined_removal_datetime_doc' ).val( date ? date + 'T' + time : '' );
	} );

	/**
	 * Document form — anti-counterfeit tags loader.
	 */
	$( document ).on( 'change', '#toggle_anti_counterfeit_doc', function () {
		if ( this.checked && ! $( '#existing_tags_doc' ).data( 'loaded' ) ) {
			loadExistingTags( '#existing_tags_doc', 'anti_counterfeit_tags_doc' );
		}
	} );

	/* =========================================================
	   Subscription & Quota (Settings page)
	   ========================================================= */

	if ( $( '#govalid-subscription-card' ).length ) {
		$.ajax( {
			url: govalidQR.restUrl + 'subscription',
			method: 'GET',
			beforeSend: function ( xhr ) {
				xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
			},
			success: function ( resp ) {
				var d = resp.data || resp;
				$( '#govalid-sub-loading' ).hide();
				$( '#govalid-sub-data' ).show();

				var bm = d.business_model || 'plan';
				var sub = d.subscription || {};
				var plan = sub.plan || {};

				// Plan badge
				var tier = ( plan.tier || 'FREE' ).toUpperCase();
				var planName = plan.name || 'Free';
				var badgeClass = 'govalid-plan-free';
				if ( tier === 'STRT' ) badgeClass = 'govalid-plan-starter';
				if ( tier === 'BUSI' ) badgeClass = 'govalid-plan-business';
				if ( tier === 'ULTI' ) badgeClass = 'govalid-plan-ultimate';

				$( '#govalid-plan-badge' )
					.text( planName )
					.removeClass( 'govalid-plan-free govalid-plan-starter govalid-plan-business govalid-plan-ultimate' )
					.addClass( badgeClass );

				// Source label
				var src = d.subscription_source || sub.source || '';
				var srcLabel = '';
				if ( src === 'apple_iap' ) srcLabel = 'via Apple';
				if ( src === 'institution' ) {
					srcLabel = 'via ' + ( sub.institution_name || 'Institution' );
				}
				if ( srcLabel ) {
					$( '#govalid-plan-source' ).text( srcLabel );
				}

				// QR quota
				var limit = plan.monthly_qr_limit || 10;
				var used = sub.qr_codes_generated_this_month || 0;
				var remaining = ( typeof sub.remaining_qr_codes !== 'undefined' )
					? sub.remaining_qr_codes
					: Math.max( 0, limit - used );

				$( '#govalid-qr-used' ).text( used );
				$( '#govalid-qr-limit' ).text( limit );
				$( '#govalid-qr-remaining' ).text( remaining );

				var pct = limit > 0 ? Math.min( 100, Math.round( ( used / limit ) * 100 ) ) : 0;
				var $bar = $( '#govalid-qr-progress' );
				$bar.css( 'width', pct + '%' );
				if ( pct >= 90 ) {
					$bar.addClass( 'govalid-progress-danger' );
				} else if ( pct >= 70 ) {
					$bar.addClass( 'govalid-progress-warning' );
				}

				// Storage
				var storageMb = plan.storage_mb || 100;
				var storageDisplay = storageMb >= 1000
					? ( storageMb / 1000 ).toFixed( 1 ) + ' GB'
					: storageMb + ' MB';
				$( '#govalid-storage-display' ).text( storageDisplay );

				// Features
				if ( plan.pdf_stamping_allowed ) {
					$( '#govalid-feature-pdf' ).show();
				}
				if ( plan.custom_forms_allowed ) {
					$( '#govalid-feature-forms' ).show();
				}

				// Expiry date
				var expiryDate = sub.expires_date || d.expires_date || null;
				if ( expiryDate ) {
					var dt = new Date( expiryDate );
					var formatted = dt.toLocaleDateString( undefined, { year: 'numeric', month: 'short', day: 'numeric' } );
					$( '#govalid-expiry-display' ).text( formatted );
					$( '#govalid-feature-expiry' ).show();
				}

				// Credit info
				if ( bm === 'credit' && d.credit_balance ) {
					var cb = d.credit_balance;
					var balStr = new Intl.NumberFormat( undefined, {
						style: 'currency',
						currency: cb.currency || 'IDR',
						maximumFractionDigits: 0
					} ).format( cb.balance_idr || cb.balance || 0 );

					$( '#govalid-credit-balance' ).text( balStr );
					$( '#govalid-credit-info' ).show();

					if ( cb.bonus_qr_quota > 0 ) {
						$( '#govalid-bonus-qr-count' ).text( cb.bonus_qr_quota );
						$( '#govalid-bonus-qr' ).show();
					}
				}
			},
			error: function ( xhr ) {
				$( '#govalid-sub-loading' ).hide();
				var msg = '';
				try {
					var err = JSON.parse( xhr.responseText );
					msg = err.message || err.data?.message || '';
				} catch ( e ) {
					msg = xhr.statusText || '';
				}
				if ( msg ) {
					$( '#govalid-sub-error-msg' ).text( msg );
				}
				$( '#govalid-sub-error' ).show();
			}
		} );
	}

	/* =========================================================
	   Utility functions
	   ========================================================= */

	function escapeHtml( str ) {
		var div = document.createElement( 'div' );
		div.appendChild( document.createTextNode( str ) );
		return div.innerHTML;
	}

	function escapeAttr( str ) {
		return str.replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' )
			.replace( /'/g, '&#39;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
	}

	/* =========================================================
	   Promo Sidebar – ads loaded server-side via wp_localize_script
	   ========================================================= */

	var defaultPromoAds = [
		{
			id: 0,
			ad_type: 'text_ad',
			theme: 'promo',
			badge: null,
			title: 'Promo Code: WP-55',
			description: '55% OFF Business & Ultimate — includes free NexHub integration! Use code WP-55 at checkout. Hurry up!',
			click_url: 'https://my.govalid.org/subscription/plans/',
			cta: 'Claim 55% OFF'
		},
		{
			id: 0,
			ad_type: 'text_ad',
			theme: 'nexhub',
			badge: 'FREE ACCESS',
			title: 'Free NexHub Access',
			description: 'Short Links Are Dead. NexHub Turns Links Into Hubs People Remember.',
			click_url: 'https://nexhub.earth/i/hub/',
			cta: 'Join NexHub Free'
		},
		{
			id: 0,
			ad_type: 'text_ad',
			title: 'Need Help?',
			description: 'Check out our FAQ or reach out to our support team for assistance.',
			click_url: 'https://my.govalid.org/pages/faq/'
		}
	];

	( function initPromoSidebar() {
		var $sidebar = $( '#govalid-promo-sidebar' );
		if ( ! $sidebar.length ) {
			return;
		}

		var trackUrl = ( typeof govalidQR !== 'undefined' && govalidQR.restUrl )
			? govalidQR.restUrl + 'ad-track'
			: '';

		// Use server-side pre-loaded ads, fallback to defaults.
		var ads = ( typeof govalidQR !== 'undefined' && govalidQR.promoAds && govalidQR.promoAds.length )
			? govalidQR.promoAds
			: defaultPromoAds;

		// Dynamic promo badge — "THIS WEEK ONLY" in last 7 days of month, else "THIS MONTH ONLY".
		var now = new Date();
		var lastDay = new Date( now.getFullYear(), now.getMonth() + 1, 0 ).getDate();
		var promoBadge = ( lastDay - now.getDate() ) < 7 ? 'THIS WEEK ONLY' : 'THIS MONTH ONLY';

		// Auto-detect theme from title when not set (API ads won't have theme/badge/cta).
		for ( var k = 0; k < ads.length; k++ ) {
			var t = ( ads[ k ].title || '' ).toLowerCase();
			if ( ! ads[ k ].theme ) {
				if ( t.indexOf( 'promo' ) !== -1 || t.indexOf( 'wp-55' ) !== -1 || t.indexOf( 'discount' ) !== -1 || t.indexOf( '% off' ) !== -1 ) {
					ads[ k ].theme = 'promo';
					ads[ k ].badge = ads[ k ].badge || promoBadge;
					ads[ k ].cta   = ads[ k ].cta   || 'Claim 55% OFF';
				} else if ( t.indexOf( 'nexhub' ) !== -1 ) {
					ads[ k ].theme = 'nexhub';
					ads[ k ].badge = ads[ k ].badge || 'FREE ACCESS';
					ads[ k ].cta   = ads[ k ].cta   || 'Join NexHub Free';
				}
			}
		}

		renderAds( $sidebar, ads, trackUrl );
	} )();

	function renderAds( $sidebar, ads, trackUrl ) {
		var html = '';
		for ( var i = 0; i < ads.length; i++ ) {
			html += buildAdBanner( ads[ i ] );
		}
		$sidebar.html( html );

		// Track impressions for remote ads (id > 0).
		for ( var j = 0; j < ads.length; j++ ) {
			if ( ads[ j ].id ) {
				trackAdEvent( trackUrl, ads[ j ].id, 'impression' );
			}
		}

		// Track clicks.
		$sidebar.on( 'click', '.govalid-promo-btn[data-ad-id]', function () {
			var adId = $( this ).data( 'ad-id' );
			if ( adId ) {
				trackAdEvent( trackUrl, adId, 'click' );
			}
		} );

		// NexHub image carousel — auto-rotate every 3s.
		$sidebar.find( '.govalid-nexhub-carousel' ).each( function () {
			var $carousel = $( this );
			var $slides = $carousel.find( '.govalid-nexhub-slide' );
			var $dots   = $carousel.find( '.govalid-nexhub-dot' );
			var current = 0;
			var total   = $slides.length;

			if ( total < 2 ) return;

			function goTo( idx ) {
				$slides.removeClass( 'active' );
				$dots.removeClass( 'active' );
				$slides.eq( idx ).addClass( 'active' );
				$dots.eq( idx ).addClass( 'active' );
				current = idx;
			}

			// Auto-rotate.
			setInterval( function () {
				goTo( ( current + 1 ) % total );
			}, 3000 );

			// Click on dots.
			$dots.on( 'click', function () {
				goTo( $dots.index( this ) );
			} );
		} );
	}

	function buildAdBanner( ad ) {
		var out = '';

		if ( ad.ad_type === 'banner_image' ) {
			out += '<div class="govalid-promo-banner">';
			if ( ad.click_url ) {
				out += '<a href="' + escapeAttr( ad.click_url ) + '" target="_blank" rel="noopener" class="govalid-promo-btn" data-ad-id="' + ( ad.id || '' ) + '" style="display:block;padding:0;border:none;">';
			}
			out += '<img src="' + escapeAttr( ad.image_url || '' ) + '" alt="' + escapeAttr( ad.alt_text || ad.name || '' ) + '" style="width:100%;border-radius:var(--gv-radius);display:block;" />';
			if ( ad.click_url ) {
				out += '</a>';
			}
			out += '</div>';

		} else if ( ad.ad_type === 'text_ad' ) {
			var themeClass = ad.theme ? ' govalid-promo-' + ad.theme : '';
			out += '<div class="govalid-promo-banner' + themeClass + '">';

			if ( ad.theme === 'promo' ) {
				out += '<div class="govalid-promo-icon-circle govalid-promo-icon-promo"><span class="dashicons dashicons-tag"></span></div>';
			} else if ( ad.theme === 'nexhub' ) {
				var nexhubBase = ( typeof govalidQR !== 'undefined' && govalidQR.pluginUrl )
					? govalidQR.pluginUrl + 'assets/images/nexhub/'
					: '';
				out += '<div class="govalid-nexhub-carousel">';
				out += '<div class="govalid-nexhub-slides">';
				out += '<img class="govalid-nexhub-slide active" src="' + escapeAttr( nexhubBase + 'photo_canvas2.png' ) + '" alt="NexHub Photo Canvas" />';
				out += '<img class="govalid-nexhub-slide" src="' + escapeAttr( nexhubBase + 'lavender_curves_preview.png' ) + '" alt="NexHub Lavender Curves" />';
				out += '<img class="govalid-nexhub-slide" src="' + escapeAttr( nexhubBase + 'forest_diagonal_preview_2.png' ) + '" alt="NexHub Forest Diagonal" />';
				out += '</div>';
				out += '<div class="govalid-nexhub-dots"><span class="govalid-nexhub-dot active"></span><span class="govalid-nexhub-dot"></span><span class="govalid-nexhub-dot"></span></div>';
				out += '</div>';
			}

			if ( ad.badge ) {
				out += '<span class="govalid-promo-badge' + ( ad.theme ? ' govalid-promo-badge-' + ad.theme : '' ) + '">' + escapeHtml( ad.badge ) + '</span>';
			}
			if ( ad.title ) {
				out += '<h3>' + escapeHtml( ad.title ) + '</h3>';
			}
			if ( ad.theme === 'promo' ) {
				out += '<div class="govalid-promo-code-box">WP-55</div>';
			}
			if ( ad.description ) {
				out += '<p>' + escapeHtml( ad.description ) + '</p>';
			}
			if ( ad.click_url ) {
				var ctaText = ad.cta || ( ad.title ? 'Learn More' : ad.name || 'Learn More' );
				var btnClass = 'govalid-promo-btn' + ( ad.theme ? ' govalid-promo-btn-' + ad.theme : ' govalid-promo-btn-primary' );
				out += '<a href="' + escapeAttr( ad.click_url ) + '" target="_blank" rel="noopener" class="' + btnClass + '" data-ad-id="' + ( ad.id || '' ) + '">';
				out += escapeHtml( ctaText );
				out += ' <span class="dashicons dashicons-arrow-right-alt"></span></a>';
			}
			out += '</div>';

		} else if ( ad.ad_type === 'custom_html' ) {
			out += '<div class="govalid-promo-banner">';
			out += ad.html_content || '';
			out += '</div>';

		} else if ( ad.ad_type === 'google_adsense' && ad.adsense_client_id && ad.adsense_slot_id ) {
			out += '<div class="govalid-promo-banner" style="padding:0;overflow:hidden;">';
			out += '<ins class="adsbygoogle" style="display:block" data-ad-client="' + escapeAttr( ad.adsense_client_id ) + '" data-ad-slot="' + escapeAttr( ad.adsense_slot_id ) + '" data-ad-format="auto" data-full-width-responsive="true"></ins>';
			out += '</div>';
		}

		return out;
	}

	function trackAdEvent( trackUrl, adId, action ) {
		if ( ! adId || ! trackUrl ) {
			return;
		}
		// Fire-and-forget via WP REST proxy to avoid CORS.
		$.ajax( {
			url: trackUrl,
			method: 'POST',
			contentType: 'application/json',
			data: JSON.stringify( { ad_id: adId, action: action } ),
			beforeSend: function ( xhr ) {
				if ( typeof govalidQR !== 'undefined' && govalidQR.nonce ) {
					xhr.setRequestHeader( 'X-WP-Nonce', govalidQR.nonce );
				}
			},
			timeout: 5000
		} );
	}

	/* =========================================================
	   QR Image Preview Modal
	   ========================================================= */

	// Create modal markup once.
	var $qrModal = $( '<div class="govalid-qr-modal-overlay" style="display:none;">' +
		'<div class="govalid-qr-modal">' +
			'<button type="button" class="govalid-qr-modal-close">&times;</button>' +
			'<div class="govalid-qr-modal-body">' +
				'<img src="" alt="" class="govalid-qr-modal-img" />' +
			'</div>' +
			'<div class="govalid-qr-modal-footer">' +
				'<span class="govalid-qr-modal-name"></span>' +
				'<a href="" download class="button button-primary govalid-qr-modal-download">' +
					'<span class="dashicons dashicons-download"></span> ' +
					( govalidQR.i18n.download || 'Download' ) +
				'</a>' +
			'</div>' +
		'</div>' +
	'</div>' );
	$( 'body' ).append( $qrModal );

	// Open modal on QR image click.
	$( document ).on( 'click', '.govalid-qr-preview-trigger', function ( e ) {
		e.preventDefault();
		e.stopPropagation();
		var src  = $( this ).data( 'full-src' );
		var name = $( this ).data( 'qr-name' ) || 'QR Code';
		$qrModal.find( '.govalid-qr-modal-img' ).attr( 'src', src ).attr( 'alt', name );
		$qrModal.find( '.govalid-qr-modal-name' ).text( name );
		$qrModal.find( '.govalid-qr-modal-download' ).attr( 'href', src );
		$qrModal.fadeIn( 200 );
	} );

	// Close modal.
	$qrModal.on( 'click', '.govalid-qr-modal-close', function () {
		$qrModal.fadeOut( 200 );
	} );
	$qrModal.on( 'click', function ( e ) {
		if ( $( e.target ).hasClass( 'govalid-qr-modal-overlay' ) ) {
			$qrModal.fadeOut( 200 );
		}
	} );
	$( document ).on( 'keydown', function ( e ) {
		if ( e.keyCode === 27 && $qrModal.is( ':visible' ) ) {
			$qrModal.fadeOut( 200 );
		}
	} );

} )( jQuery );
