(function( $ ) {
	'use strict';

	const template = document.createElement( 'template' );

	template.innerHTML = `
		<style>
			:host {                             
                display: block;  
                width: 100%;      
            }

			:host([hidden]) {
				display: none;
			}

			:host([is-video]) {
				max-width: var( --plyr-video-width, 100% );
			}

			:host([is-video]) #root {
				position: relative;
				padding-top: var( --plyr-video-ratio, 56.25% );
				width: 100%;
				height: 0;
			}

			:host([is-video]) #root .plyr {
				position: absolute;
				inset: 0;
				border-radius: 3px;
				width: 100%;
				height: 100%;
			}

			:host([is-video]) #root .plyr__control--overlaid {
				--plyr-control-spacing: 15px;
			}

			:host([is-video]) #root .plyr__control--overlaid svg {
				--plyr-control-icon-size: 27px;
			}

			:host([stacked]) .plyr__controls {
				flex-wrap: wrap;
				justify-content: flex-start;
			}

			:host([stacked]) .plyr__controls .plyr__controls__item.plyr__progress__container {
				position: relative;
				order: -1;
				flex: 1 0 100%;
				margin-left: 0;
				padding-left: 0;
			}

			.plyr--audio .plyr__controls {
				padding: 0 !important;
			}

			.plyr .plyr__controls .plyr__spacer {
				flex: 1 1 auto;
  				height: 100%;
			}		

			.plyr .plyr__controls [data-plyr="speed-control"] {
				padding: 0 calc( var( --plyr-control-spacing, 10px ) * 0.7 );
				line-height: 32px;
				font-size: var( --plyr-font-size-small, 13px );
			}

			.plyr .plyr__controls [data-plyr="chapters"] {
				padding: 4px;
			}

			.plyr .plyr__controls [data-plyr="chapters"] svg {
				width: 24px;
				height: 24px;
			}

			.plyr .plyr__controls [data-plyr="chapters"][aria-expanded=true] svg {
				transform: none;
			}	

			.plyr .plyr__menu .plyr__menu__container {
				max-width: 320px;
			}
				
			.plyr .plyr__menu__container .plyr__control > span {
				min-width: 0;
			}

			.plyr .plyr__menu__container .plyr__control .plyr__menu__label {
				display: inline-block;
				overflow: hidden;
				text-overflow: ellipsis;
				white-space: nowrap;
			}

			.plyr .plyr__menu__container .plyr__control .plyr__menu__value {
				overflow: visible;
			}

			.plyr .plyr__menu__container div[role="menu"] {
				margin: calc( var( --plyr-control-spacing, 10px ) * 0.7 );
				padding: 0;
				max-height: 200px;
				overflow-y: auto;

				/* Firefox scrollbar styling */
				scrollbar-width: thin;
				scrollbar-color: #999 #e5e5e5;
			}			

			.plyr .plyr__menu__container div[role="menu"]::-webkit-scrollbar {
				width: 6px;
			}

			.plyr .plyr__menu__container div[role="menu"]::-webkit-scrollbar-track {
				background: #e5e5e5;
			}

			.plyr .plyr__menu__container div[role="menu"]::-webkit-scrollbar-thumb {
				background: #999;
				border-radius: 3px;
			}

			.plyr .plyr__menu__container div[role="menu"]::-webkit-scrollbar-thumb:hover {
				background: #777;
			}
		</style>
		<div id="root"></div>
	`;

	/**
	 * Load css files.
	 */
	const stylePromises = {};

	const loadStyle = ( file ) => {
		if ( stylePromises[ file.id ] ) {
			return stylePromises[ file.id ];
		}

		stylePromises[ file.id ] = new Promise(( resolve, reject ) => {
			const existing = document.getElementById( file.id );
			if ( existing ) {
				resolve();
				return;
			}

			const link = document.createElement( 'link' );

			link.id   = file.id;
			link.rel  = 'stylesheet';
			link.href = file.href;

			link.onload  = () => resolve();
			link.onerror = () => reject();

			document.head.appendChild( link );
		});

		return stylePromises[ file.id ];
	};

	/**
	 * Load script files.
	 */
	const scriptPromises = {};

	const loadScript = ( file ) => {
		if ( scriptPromises[ file.id ] ) {
			return scriptPromises[ file.id ];
		}

		scriptPromises[ file.id ] = new Promise(( resolve, reject ) => {
			const existing = document.getElementById( file.id );
			if ( existing ) {
				resolve();
				return;
			}

			const script = document.createElement( 'script' );
			script.id    = file.id;
			script.src   = file.src;

			script.onload  = () => resolve();
			script.onerror = () => reject();

			document.body.appendChild( script );
		});

		return scriptPromises[ file.id ];
	};

	/**
	 * Minimal silent WAV data URI used as a placeholder source when a video
	 * episode is loaded on the free tier. Gives Plyr a valid audio element to
	 * initialise against so the interactive player wires up correctly, while
	 * playing nothing audible.
	 */
	const AIOPOD_SILENT_STUB = 'data:audio/wav;base64,UklGRlIAAABXQVZFZm10IBAAAAABAAEARKwAAESsAAABAAgAZGF0YRIAAAAAAAA=';

	/**
	 * Returns true for source URLs that can only be played as video and cannot
	 * be meaningfully downgraded to audio (YouTube, Vimeo, and direct video
	 * file extensions). HLS and DASH are deliberately excluded because they
	 * carry audio tracks and play fine through an <audio> element.
	 *
	 * @param  {string} src Media source URL.
	 * @return {boolean}
	 */
	const isVideoOnlySrc = ( src ) => {
		if ( /youtube\.com|youtu\.be|vimeo\.com/i.test( src ) ) return true;
		return /\.(mp4|m4v|webm|ogv|mov)(\?|$)/i.test( src.split( '?' )[0] );
	};
	
	/**
	 * Define a custom HTML element class for <aiopod-media>
	 */
	class AIOPODMedia extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Attach Shadow DOM to the component
			const shadowDom = this.attachShadow({ mode: 'open' });
			this.shadowRoot.appendChild( template.content.cloneNode( true ) ); 

			// Set references to the DOM elements from the component's template
            this.rootEl   = shadowDom.querySelector( '#root' );
			this.controls = null;

			// Set references to the public properties used by the component
			this.player = null;

			// Set references to the private properties used by the component
			this._params       = {};
			this._currentSrc   = '';
			this._canPlayVideo = !! aiopod.can_play_video;
			this._canAutoplay  = false;
			this._playTracked  = false;
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this.player ) return; // Prevent multiple initializations

			if ( this.dataset.params ) {
				try {
					this._params = JSON.parse( this.dataset.params );
					this._canAutoplay = this._params.autoplay ? true : false;
				} catch ( error ) {
					console.error( 'Invalid params JSON:', error );
					this._params = {};
				}
			}

			// Lazy-load dependencies and initialize when ready
			const plyrPromise =	this._loadStyle( aiopod.plugin_url + 'vendor/plyr/plyr.css?ver=3.7.8' )
				.then( () => {
					return window.Plyr ? Promise.resolve() : loadScript({
						id: 'aio-podcast-studio-plyr-js',
						src: aiopod.plugin_url + 'vendor/plyr/plyr.polyfilled.js?ver=3.7.8'
					});
				});

			plyrPromise.then( () => {
				this._initPlayer();
			}).catch( error => {
				console.error( 'Failed to initialize Plyr:', error );
			});
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			document.removeEventListener( 'click', this._closeChaptersMenu );

			// Destroy HLS | Dash
			this._destroyStreamingProviders();

			// Destroy Plyr
			if ( this.player ) {
				this.player.destroy();
				this.player = null;
			}
		}

		/**
         * Define getters and setters for attributes.
         */
		get instance() {
			if ( this.player ) {
				return this.player;
			}

			return null;
		}

		set src( value ) {
			return this.setAttribute( 'src', value );
		}

		get src() {
			return this.getAttribute( 'src' ) || '';
		}

		set mediaType( value ) {
			const isVideo = ( value === 'video' );
			isVideo ? this.setAttribute( 'is-video', '' ) : this.removeAttribute( 'is-video' );
		}

		/**
		 * Define private methods.
		 */
		_initPlayer() {
			const src = this.src || '';

			if ( ! src ) {
				console.error( 'Media source not found.' );
				return;
			}

			this._currentSrc = src;

			const mediaType = ( this._canPlayVideo && this.hasAttribute( 'is-video' ) ) ? 'video' : 'audio';

			let media;

			if ( this._canPlayVideo && /youtube\.com|youtu\.be/.test( src ) ) { // YouTube (premium only)
				media = document.createElement( 'div' );
				media.setAttribute( 'data-plyr-provider', 'youtube' );
				media.setAttribute( 'data-plyr-embed-id', src );
			} else if ( this._canPlayVideo && /vimeo\.com/.test( src ) ) { // Vimeo (premium only)
				media = document.createElement( 'div' );
				media.setAttribute( 'data-plyr-provider', 'vimeo' );
				media.setAttribute( 'data-plyr-embed-id', src );
			} else {
				media = document.createElement( mediaType );
				media.setAttribute( 'controls', '' );

				// Free tier: load a silent stub so Plyr initialises cleanly and
				// the interactive player can still handle audio episodes in the
				// same list. The real source is intentionally withheld.
				if ( ! this._canPlayVideo && isVideoOnlySrc( src ) ) {
					media.src = AIOPOD_SILENT_STUB;
				} else {
					if ( this._canAutoplay ) media.setAttribute( 'autoplay', '' );
					media.src = src;
				}
			}

			this.rootEl.appendChild( media );

			const { speed, markers, ...options } = this._params || {};
			options.iconUrl = aiopod.plugin_url + 'vendor/plyr/plyr.svg';

			// When re-created for a user-selected episode, resume playback
			// regardless of the autoplay setting (embeds ignore the attribute)
			if ( this._canAutoplay ) {
				options.autoplay = true;
			}

			const player = new Plyr( media, options );
			this.player  = player;

			// HLS
			if ( /\.m3u8(\?|$)/i.test( src ) ) {
				this._initHls( media, src );
			}

			// Dash
			if ( /\.mpd(\?|$)/i.test( src ) ) {
				this._initDash( media, src );
			}	

			// OnReady
			player.on( 'ready', () => {
				this.controls = player.elements.container.querySelector( '.plyr__controls' );

				if ( this.controls ) {
					this._addSpacer();
					this._addSpeedControl();
					this._addChapters();
				}

				// Use the tracking URL for the download button; fall back to the raw src.
				player.download = this.dataset.download || src;

				// Hide the download button for streams and embeds
				this._toggleDownloadButton();
			});

			// Track play — fire AJAX once per play session (resets on source change).
			player.on( 'play', () => {
				if ( this._playTracked ) return;
				this._playTracked = true;

				const playerEl = this.closest( '[data-post_id]' );
				const postId   = playerEl ? parseInt( playerEl.dataset.post_id, 10 ) : 0;

				if ( ! postId ) return;

				$.ajax({
					url: aiopod.ajax_url,
					type: 'POST',
					data: {
						action: 'aiopod_track_play',
						security: aiopod.ajax_nonce,
						episode_id: postId
					}
				});
			});

			// Custom method to change source dynamically
			player.setSource = ( source ) => {
				const src = source.src || '';
				if ( ! src ) return; // Invalid source

				const type = source.type || 'audio/mpeg';

				// Free tier: silently ignore video-only sources. The current
				// source (silent stub or a previous audio episode) is left
				// unchanged so the player remains in a usable state.
				if ( ! this._canPlayVideo && 0 === type.indexOf( 'video/' ) ) {
					return;
				}

				const mediaType = ( this._canPlayVideo && type.indexOf( 'audio' ) === -1 ) ? 'video' : 'audio';
				this.mediaType  = mediaType;

				this._currentSrc  = src;
				this._canAutoplay = true;
				this._playTracked = false; // Reset so the new episode is tracked.

				// Update the chapter markers up front so any rebuilt player or
				// controls use the new episode's chapters, not the previous one's.
				if ( this._params.markers ) {
					this._params.markers.points = source.chapters || [];
				}

				let downloadURL = '';

				// Pause old media
				player.pause();

				// Destroy HLS / Dash
				this._destroyStreamingProviders();

				// Fully tear down and re-create the player. Needed when the new
				// source must attach hls.js / dash.js to a fresh <video> element
				// and the current media (an audio element or an embed) cannot be reused.
				const rebuildPlayer = () => {
					this.src = src;

					if ( source.download ) {
						this.dataset.download = source.download;
					} else {
						delete this.dataset.download;
					}

					player.destroy();
					this.player = null;
					this.rootEl.innerHTML = '';

					this._initPlayer();
				};

				// Whether the current media is an HTML5 <video> that hls.js / dash.js can attach to
				const canReuseVideo = ( 'html5' === player.provider && 'video' === player.type );

				// Plyr cannot reliably swap an embed for HTML5 media in place — the
				// embed's asynchronous teardown races the rebuild (Vimeo invokes the
				// rebuild callback twice) — so fully re-create the player instead.
				const isEmbedSrc = /youtube\.com|youtu\.be|vimeo\.com/.test( src );

				if ( 'vimeo' === player.provider || ( ! isEmbedSrc && 'html5' !== player.provider ) ) {
					rebuildPlayer();
					return;
				}

				// Update source
				if ( /youtube\.com|youtu\.be/.test( src ) ) { // YouTube
					player.source = {
						type: 'video',
						sources: [{	src: src, provider: 'youtube' }]
					};
				} else if ( /vimeo\.com/.test( src ) ) { // Vimeo
					player.source = {
						type: 'video',
						sources: [{	src: src, provider: 'vimeo'	}]
					};
				} else if ( /\.m3u8(\?|$)/i.test( src ) ) { // HLS
					const isSafari = /^((?!chrome|android).)*safari/i.test( navigator.userAgent );

					if ( isSafari ) {
						player.source = {
							type: 'video',
							sources: [{ src, type: 'application/x-mpegURL' }]
						};
					} else if ( canReuseVideo ) {
						// Attach to player.media (the live element) — Plyr replaces the
						// media element on every source change, so the element captured
						// at init time may no longer be in the DOM.
						this._initHls( player.media, src );
					} else {
						rebuildPlayer();
						return;
					}
				} else if ( /\.mpd(\?|$)/i.test( src ) ) { // Dash
					if ( canReuseVideo ) {
						this._initDash( player.media, src );
					} else {
						rebuildPlayer();
						return;
					}
				} else if ( /\.mov(\?|$)/i.test( src ) ) { // MOV
					player.source = {
						type: mediaType,
						sources: [{	src: src }]
					};	
					
					downloadURL = src;
				} else {
					player.source = {
						type: mediaType,
						sources: [{	src: src, type: type }]
					};	
					
					downloadURL = src;
				}				
				
				// Safe play video
				const playVideo = () => {
					if ( ! this.player || this.player !== player ) return;

					if ( player.paused || player.ended ) {
						// play() returns a Promise for HTML5/Vimeo but undefined for YouTube
						const playPromise = player.play();

						if ( playPromise && typeof playPromise.catch === 'function' ) {
							playPromise.catch( () => {} );
						}
					}
				};

				// Update poster, download, chapters, and playback (after UI rebuild)
				setTimeout( () => {
					if ( ! this.player || this.player !== player ) return;

					// Update poster
					player.poster = source.poster || '';

					// Update download button — prefer tracking URL, fall back to raw src.
					player.download = source.download || downloadURL || '';

					// Hide the download button for streams and embeds
					this._toggleDownloadButton();

					// Update chapters
					if ( source.chapters ) {
						this._updateChapters( source.chapters );
					}

					// Explicitly call play()
					playVideo();
				}, 50 );

				// Auto-play after metadata loads
				player.once( 'loadedmetadata', () => {
					playVideo();
				});

				// Plyr rebuilds the media element asynchronously after a source change
				// and "loadedmetadata" is not reliable (embeds never emit it; HTML5
				// media skips it when preload is disabled), so also resume playback
				// on "ready", which fires once the rebuild completes.
				player.once( 'ready', () => {
					playVideo();
				});
			};

			// Dispatch a custom event to notify that the player is initialized
			this._triggerEvent( 'init', { player: player } );
		}

		_initHls( media, src ) {
			const hlsPromise = window.Hls ? Promise.resolve() : loadScript({
				id: 'aio-podcast-studio-hls-js',
				src: aiopod.plugin_url + 'vendor/plyr/hls.min.js?v=1.5.17'
			});

			hlsPromise.then( () => {
				if ( ! this.player ) return;
				const player = this.player;

				this._hls = new Hls();
				this._hls.loadSource( src );
				this._hls.attachMedia( media );

				this._onLanguageChange = () => {
					setTimeout( () => {
						if ( this._hls ) {
							this._hls.subtitleTrack = player.currentTrack;
						}
					}, 50 );
				};

				player.on( 'languagechange', this._onLanguageChange );
			}).catch( error => {
				console.error( 'Failed to initialize HLS:', error );
			});
		}

		_initDash( media, src ) {
			const dashPromise = window.dashjs ? Promise.resolve() : loadScript({
				id: 'aio-podcast-studio-dash-js',
				src: aiopod.plugin_url + 'vendor/plyr/dash.all.min.js?v=4.7.4'
			});

			dashPromise.then( () => {
				if ( ! this.player ) return;

				this._dash = dashjs.MediaPlayer().create();
  				this._dash.initialize( media, src, this._canAutoplay );
			}).catch( error => {
				console.error( 'Failed to initialize Dash:', error );
			});
		}

		_toggleDownloadButton() {
			if ( ! this.player || ! this.player.elements || ! this.player.elements.container ) {
				return;
			}

			const button = this.player.elements.container.querySelector( '[data-plyr="download"]' );

			if ( ! button ) {
				return;
			}

			// Streaming manifests (HLS/DASH) and embeds (YouTube/Vimeo) are not downloadable files
			const src = this._currentSrc || '';
			const isDownloadable = ! ( /youtube\.com|youtu\.be|vimeo\.com/.test( src ) || /\.(m3u8|mpd)(\?|$)/i.test( src ) );

			button.hidden = ! isDownloadable;
		}

		_addSpacer() {
			if ( ! this.controls || this.controls.querySelector( '.plyr__spacer' ) ) {
				return;
			}

			const items = this.controls.querySelectorAll( '.plyr__controls__item' );

			let insertAfter = null;

			items.forEach( item => {
				if (
					item.dataset.plyr === 'rewind' ||
					item.dataset.plyr === 'play' ||
					item.dataset.plyr === 'fast-forward' ||
					item.classList.contains( 'plyr__progress__container' ) || 
					item.classList.contains( 'plyr__time--current' ) ||
					item.classList.contains( 'plyr__time--duration' )
				) {
					insertAfter = item;
				}
			});

			// Create the spacer element
			const spacer = document.createElement( 'div' );
			spacer.className = 'plyr__controls__item plyr__spacer';
			spacer.setAttribute( 'aria-hidden', 'true' );

			// Insert into controls
			if ( insertAfter && insertAfter.parentNode ) {
				insertAfter.parentNode.insertBefore( spacer, insertAfter.nextSibling );
			}
		}

		_addSpeedControl() {
			if ( ! this.player || ! this.controls || this.controls.querySelector( '[data-plyr="speed-control"]' ) ) {
				return;
			}

			if ( ! this._params.speed || ! this._params.speed.selected ) {
				return;
			}

			const player = this.player;

			const speedOptions = this._params.speed.options || [ 0.5, 0.75, 1, 1.5, 2 ];
			const items = this.controls.querySelectorAll( '.plyr__controls__item' );

			let insertAfter = null;

			items.forEach( item => {
				if (
					item.dataset.plyr === 'rewind' ||
					item.dataset.plyr === 'play' ||
					item.dataset.plyr === 'fast-forward' || 
					item.classList.contains( 'plyr__progress__container' ) ||
					item.classList.contains( 'plyr__time--current' ) ||
					item.classList.contains( 'plyr__time--duration' ) ||
					item.classList.contains( 'plyr__spacer' ) ||
					item.classList.contains( 'plyr__volume' )
				) {
					insertAfter = item;
				}
			});

			// Create the speed element
			const speedBtn = document.createElement( 'button' );						
			speedBtn.className = 'plyr__controls__item plyr__control';
			speedBtn.setAttribute( 'type', 'button' );
			speedBtn.setAttribute( 'data-plyr', 'speed-control' );
			speedBtn.setAttribute( 'aria-pressed', 'false' );
			speedBtn.setAttribute( 'aria-label', 'Speed' );
			speedBtn.textContent = this._formatSpeed( player.speed );

			// Insert into controls
			if ( insertAfter && insertAfter.parentNode ) {
				insertAfter.parentNode.insertBefore( speedBtn, insertAfter.nextSibling );
			} else {
				this.controls.insertBefore( speedBtn, items[0] || null );
			}

			// Toggle speed on click
			speedBtn.addEventListener( 'click', () => {
				const currentSpeed = player.speed;
				const currentIndex = speedOptions.indexOf( currentSpeed );

				// Get next index (loop back to 0)
				const nextIndex = ( currentIndex + 1 ) % speedOptions.length;
				const nextSpeed = speedOptions[ nextIndex ];

				player.speed = nextSpeed;
				speedBtn.textContent = this._formatSpeed( nextSpeed );
			});
		}		

		_addChapters() {
			if ( ! this.player || ! this.controls || this.controls.querySelector( '[data-plyr="chapters"]' ) ) {
				return;
			}

			if ( ! this._params.markers || ! this._params.markers.enabled ) {
				return;
			}

			const chaptersList = this._params.markers.points || [];
			if ( chaptersList.length === 0 ) return;	
			
			const player = this.player;
			const items  = this.controls.querySelectorAll( '.plyr__controls__item' );

			let insertAfter = null;

			items.forEach( item => {
				if (
					item.dataset.plyr === 'rewind' ||
					item.dataset.plyr === 'play' ||
					item.dataset.plyr === 'fast-forward' || 
					item.dataset.plyr === 'speed-control' || 
					item.classList.contains( 'plyr__progress__container' ) || 
					item.classList.contains( 'plyr__time--current' ) ||
					item.classList.contains( 'plyr__time--duration' ) || 
					item.classList.contains( 'plyr__spacer' ) ||
					item.classList.contains( 'plyr__volume' )
				) {
					insertAfter = item;
				}
			});

			// Create the chapters element
			const chapters = document.createElement( 'div' );
			chapters.className = 'plyr__controls__item plyr__menu';	

			const chaptersBtn = document.createElement( 'button' );						
			chaptersBtn.className = 'plyr__control';
			chaptersBtn.setAttribute( 'type', 'button' );
			chaptersBtn.setAttribute( 'data-plyr', 'chapters' );
			chaptersBtn.setAttribute( 'aria-haspopup', 'true' );
			chaptersBtn.setAttribute( 'aria-controls', 'plyr-chapters-' + player.id );
			chaptersBtn.setAttribute( 'aria-expanded', 'false' );
			chaptersBtn.setAttribute( 'aria-pressed', 'false' );
			chaptersBtn.innerHTML = `
				<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48" aria-hidden="true" focusable="false">
					<path d="M0 0h48v48H0z" fill="none"></path>
					<g fill="currentColor">
						<rect x="16.002" y="22.002" width="25.997" height="4"></rect>
						<rect x="16.002" y="11.995" width="25.997" height="4"></rect>
						<rect x="16.002" y="32.008" width="25.997" height="4"></rect>
						<rect x="6" y="12" width="4" height="4"></rect>
						<rect x="6" y="22.006" width="4" height="4"></rect>
						<rect x="6" y="32.013" width="4" height="4"></rect>
					</g>
				</svg>
				<span class="plyr__sr-only">Chapters</span>
			`;

			const chaptersMenuContainer = document.createElement( 'div' );
			chaptersMenuContainer.className = 'plyr__menu__container';
			chaptersMenuContainer.id = 'plyr-chapters-' + player.id;
			chaptersMenuContainer.hidden = true;	

			const chaptersMenu = document.createElement( 'div' );
			chaptersMenu.setAttribute( 'role', 'menu' );

			chaptersList.forEach( chapter => {	
				const totalSeconds = chapter.time;

				let hours   = Math.floor( totalSeconds / 3600 );
				let minutes = Math.floor( ( totalSeconds % 3600 ) / 60 );
				let seconds = totalSeconds % 60;

				// Always double digits
				const duration = Number( player.duration );
				const mm = String( minutes ).padStart( 2, '0' );
				const ss = String( seconds ).padStart( 2, '0' );

				let timeStr;

				if ( duration && duration >= 3600 ) { // Show hours if video is longer than 1 hour
					const hh = String( hours ).padStart( 2, '0' );
					timeStr = hh + ':' + mm + ':' + ss;
				} else {
					timeStr = mm + ':' + ss;
				}

				const chapterBtn = document.createElement( 'button' );						
				chapterBtn.className = 'plyr__control';
				chapterBtn.setAttribute( 'type', 'button' );
				chapterBtn.setAttribute( 'role', 'menuitemradio' );
				chapterBtn.setAttribute( 'data-plyr', 'chapter' );
				chapterBtn.setAttribute( 'aria-checked', 'false' );
				chapterBtn.setAttribute( 'value', chapter.time );

				// Build with textContent — chapter labels originate from imported
				// RSS feeds and must not be parsed as HTML.
				const chapterWrap  = document.createElement( 'span' );
				const chapterLabel = document.createElement( 'span' );
				const chapterValue = document.createElement( 'span' );

				chapterLabel.className   = 'plyr__menu__label';
				chapterLabel.textContent = chapter.label;

				chapterValue.className   = 'plyr__menu__value';
				chapterValue.textContent = timeStr;

				chapterWrap.appendChild( chapterLabel );
				chapterWrap.appendChild( chapterValue );
				chapterBtn.appendChild( chapterWrap );

				chaptersMenu.appendChild( chapterBtn );
			});

			chaptersMenuContainer.appendChild( chaptersMenu );

			chapters.appendChild( chaptersBtn );
			chapters.appendChild( chaptersMenuContainer );

			// Store references for later use
			this._chaptersBtn = chaptersBtn;
			this._chaptersMenuContainer = chaptersMenuContainer;

			// Insert into controls
			if ( insertAfter && insertAfter.parentNode ) {								
				insertAfter.parentNode.insertBefore( chapters, insertAfter.nextSibling );
			} else {
				this.controls.insertBefore( chapters, items[0] || null );
			}	
			
			// Toggle chapters menu visibility
			chaptersBtn.addEventListener( 'click', ( e ) => {
				e.stopPropagation(); // Prevent immediate outside-click close

				const expanded = chaptersBtn.getAttribute( 'aria-expanded' ) === 'true';
				chaptersBtn.setAttribute( 'aria-expanded', String( ! expanded ) );
				chaptersMenuContainer.hidden = expanded;

				if ( ! expanded ) {
					// Menu just opened → start listening for outside clicks
					document.addEventListener( 'click', this._closeChaptersMenu );
				} else {
					// Menu just closed → stop listening
					document.removeEventListener( 'click', this._closeChaptersMenu );
				}
			});

			// Handle chapter selection
			chaptersMenu.querySelectorAll( '[data-plyr="chapter"]' ).forEach( chapterBtn => {
				chapterBtn.addEventListener( 'click', ( e ) => {
					const time = parseFloat( chapterBtn.value );
					player.currentTime = time;

					chaptersMenu.querySelectorAll( '[data-plyr="chapter"]' ).forEach( btn => {	
						btn.setAttribute( 'aria-checked', 'false' );
					});

					chapterBtn.setAttribute( 'aria-checked', 'true' );
					
					// Close the menu
					chaptersMenuContainer.hidden = true;
					chaptersBtn.setAttribute( 'aria-expanded', 'false' );
					document.removeEventListener( 'click', this._closeChaptersMenu );
				});
			});

			// Update chapter selection on time update
			player.on( 'timeupdate', this._chapterTimeUpdateHandler );
		}

		_updateChapters( markers ) {
			if ( ! this.player || ! this.controls || ! this._params.markers || ! this._params.markers.enabled ) {
				return;
			}			

			const player = this.player;

			const chaptersBtn = this.controls.querySelector( '[data-plyr="chapters"]' );

			if ( chaptersBtn ) {
				player.off( 'timeupdate', this._chapterTimeUpdateHandler );
				document.removeEventListener( 'click', this._closeChaptersMenu );

				this._chaptersBtn = null;
				this._chaptersMenuContainer = null;

				// Remove the chapters wrapper specifically — Plyr's own settings
				// menu shares the .plyr__menu class and must not be removed.
				chaptersBtn.closest( '.plyr__menu' ).remove();
			}

			this._params.markers.points = markers;
			this._addChapters();
		}

		_chapterTimeUpdateHandler = () => {
			if ( ! this.player || ! this._chaptersMenuContainer ) return;

			const player = this.player;

			let buttons = this._chaptersMenuContainer.querySelectorAll( '[data-plyr="chapter"]' );
			let active  = null;

			buttons.forEach( btn => {
				const t = parseFloat( btn.value );
				if ( player.currentTime >= t ) active = btn;					
			});

			buttons.forEach( btn => btn.setAttribute( 'aria-checked', 'false' ) );
			if ( active ) active.setAttribute( 'aria-checked', 'true' );
		}

		_closeChaptersMenu = ( e ) => {
			if ( ! this._chaptersBtn || ! this._chaptersMenuContainer ) {
				return;
			}

			const path = e.composedPath ? e.composedPath() : ( e.path || [] );

			// If user clicked the button → do nothing			
			if ( path.includes( this._chaptersBtn ) ) return;

			// If user clicked inside the menu → do nothing
			if ( path.includes( this._chaptersMenuContainer ) ) return;

			// Otherwise → close the menu
			this._chaptersMenuContainer.hidden = true;
			this._chaptersBtn.setAttribute( 'aria-expanded', 'false' );
			document.removeEventListener( 'click', this._closeChaptersMenu );
		}

		_destroyStreamingProviders() {
			// Remove Plyr events
			if ( this.player && this._onLanguageChange ) {
				this.player.off( 'languagechange', this._onLanguageChange );
				this._onLanguageChange = null;
			}

			// Destroy HLS
			if ( this._hls ) {
				this._hls.destroy();
				this._hls = null;
			}

			// Destroy DASH
			if ( this._dash ) {
				this._dash.reset();
				this._dash = null;
			}
		}

		_formatSpeed( speed ) {
			const n = Number( speed );
			return ( Number.isInteger( n ) ? n.toFixed( 1 ) : String( n ) ) + 'x';
		}

		_triggerEvent( event, data ) {
			const customEvent = new CustomEvent( event, {					
				bubbles: true,
				composed: true,
				detail: data
			});

			this.dispatchEvent( customEvent );
		}

		/**
         * Define private async methods.
         */
		async _loadStyle( url ) {
			if ( ! AIOPODMedia.plyrCssText ) {
				const res = await fetch( url );
				if ( ! res.ok ) throw new Error( res.status );
				
				AIOPODMedia.plyrCssText = await res.text();
			}

			if ( 'adoptedStyleSheets' in Document.prototype && 'replaceSync' in CSSStyleSheet.prototype ) {
				const sheet = new CSSStyleSheet();
				sheet.replaceSync( AIOPODMedia.plyrCssText );
				this.shadowRoot.adoptedStyleSheets = [ sheet ];
			} else {
				const style = document.createElement( 'style' );
				style.textContent = AIOPODMedia.plyrCssText;
				this.shadowRoot.appendChild( style );
			}
		}

		/**
		 * Define public methods.
		 */
		seekTo( seconds ) {
			if ( ! this.player ) return;

			this.player.currentTime = seconds;

			if ( this.player.paused || this.player.ended ) {
				// play() returns a Promise for HTML5/Vimeo but undefined for YouTube
				const playPromise = this.player.play();

				if ( playPromise && typeof playPromise.catch === 'function' ) {
					playPromise.catch( () => {} );
				}
			}
		}

	}

	AIOPODMedia.plyrCssText = null;

	/**
	 * Define a custom HTML element class for <aiopod-player>
	 */
	class AIOPODPlayer extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Set references to the DOM elements used by the component
			this.$playButton = null;

			// Set references to the public properties used by the component
			this.player = null;

			// Set references to the private properties used by the component
			this._hasInitialized = false;	

			// Bind the event handlers to ensure the reference remains stable
			this._onPlay          = this._onPlay.bind( this );
			this._onPause         = this._onPause.bind( this );
			this._togglePlayPause = this._togglePlayPause.bind( this );
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this._hasInitialized ) return;			

			const player = this.querySelector( 'aiopod-media' );
			if ( ! player ) return;

			this._hasInitialized = true;

			this.$playButton = $( this ).find( '.aiopod-button--play' );

			this.player = player.instance;

			if ( this.player ) {
				this._initEvents();
			}

			// The player is re-created on some source switches — re-bind to each new instance
			player.addEventListener( 'init', ( event ) => {
				this.player = event.detail.player;
				this._initEvents();
			});
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			if ( this.player ) {
				this.player.off( 'play', this._onPlay );
				this.player.off( 'pause', this._onPause );

				this.$playButton.off( 'click', this._togglePlayPause );
			}
		}

		/**
		 * Define private methods.
		 */
		_initEvents() {
			this.player.on( 'play', this._onPlay );
			this.player.on( 'pause', this._onPause );

			// Drop any previous binding first — this method runs again for each re-created player
			this.$playButton.off( 'click', this._togglePlayPause );
			this.$playButton.on( 'click', this._togglePlayPause );
		}

		_onPlay() {
			this.$playButton.addClass( 'aiopod-button--playing' );
		}

		_onPause() {
			this.$playButton.removeClass( 'aiopod-button--playing' );
		}

		_togglePlayPause() {
			if ( this.player.paused || this.player.ended ) {
				this._onPlay();
				this.player.play();				
			} else {
				this._onPause();
				this.player.pause();
			}
		}

	}

	/**
	 * Define a custom HTML element class for <aiopod-episodes>
	 */
	class AIOPODEpisodes extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Set references to the DOM elements used by the component
			this.$root       = null;
			this.$player     = null;
			this.$episodes   = null;
			this.$activeItem = null;
			this.$moreButton = null;

			// Set references to the public properties used by the component
			this.player = null;

			// Set references to the private properties used by the component
			this._hasInitialized     = false;
			this._canPlayVideo       = !! aiopod.can_play_video;
			this._hasPlaybackStarted = false;

			// Bind the event handlers to ensure the reference remains stable
			this._onPlay          = this._onPlay.bind( this );
			this._onPause         = this._onPause.bind( this );
			this._onEnded         = this._onEnded.bind( this );
			this._togglePlayPause = this._togglePlayPause.bind( this );
			this._updateMedia     = this._updateMedia.bind( this );
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this._hasInitialized ) return;			

			let root = this.closest( '.aiopod-single--podcast' ) || this.closest( '.aiopod-episodes--interactive' );
			if ( ! root ) return;

			let player = root.querySelector( 'aiopod-media' );
			if ( ! player ) return;

			this._hasInitialized = true;

			this.$root       = $( root );
			this.$player     = this.$root.find( '.aiopod-player' );
			this.$episodes   = $( this ).find( '.aiopod-episodes__list' );
			this.$moreButton = $( this ).find( 'aiopod-pagination[is-ajax] button' );

			const postId = parseInt( this.$player.data( 'post_id' ) );
			if ( postId ) {
				this.$activeItem = $( this ).find( '.aiopod-episode[data-post_id="' +  postId + '"]' ).addClass( 'aiopod-is-active' );
			}

			this.player = player.instance;

			if ( this.player ) {
				this._initEvents();
			}

			// The player is re-created on some source switches — re-bind to each new instance
			player.addEventListener( 'init', ( event ) => {
				this.player = event.detail.player;
				this._initEvents();
			});
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			if ( this.player ) {
				this.player.off( 'play', this._onPlay );
				this.player.off( 'pause', this._onPause );
				this.player.off( 'ended', this._onEnded );

				this.$episodes.off( 'click', '.aiopod-button--play', this._updateMedia );
			}
		}

		/**
         * Define getters and setters for attributes.
         */
		get autoadvance() {
            return this.hasAttribute( 'autoadvance' );
        }

		get loop() {
            return this.hasAttribute( 'loop' );
        }

		/**
		 * Define private methods.
		 */
		_initEvents() {
			// Set initial state of the active element
			if ( this.$activeItem ) {
				if ( this.player.playing ) {
					this.$activeItem.find( '.aiopod-button--play' ).addClass( 'aiopod-button--playing' );
				} else {
					this.$activeItem.find( '.aiopod-button--play' ).removeClass( 'aiopod-button--playing' );
				}
			}

			// Init events
			this.player.on( 'play', this._onPlay );
			this.player.on( 'pause', this._onPause );
			this.player.on( 'ended', this._onEnded );

			// Drop any previous binding first — this method runs again for each re-created player
			this.$episodes.off( 'click', '.aiopod-button--play', this._updateMedia );
			this.$episodes.on( 'click', '.aiopod-button--play', this._updateMedia );
		}

		_onPlay() {
			this._hasPlaybackStarted = true;

			this.$activeItem = this.$episodes.find( '.aiopod-is-active' );

			if ( this.$activeItem ) {
				this.$activeItem.find( '.aiopod-button--play' ).addClass( 'aiopod-button--playing' );
			}
		}

		_onPause() {
			this.$activeItem = this.$episodes.find( '.aiopod-is-active' );

			if ( this.$activeItem ) {
				this.$activeItem.find( '.aiopod-button--play' ).removeClass( 'aiopod-button--playing' );
			}
		}

		_onEnded() {
			if ( ! this.autoadvance ) {
				this._onPause();
				return;
			}

			this.$activeItem = this.$episodes.find( '.aiopod-is-active' );

			if ( this.$activeItem.length === 0 ) {
				this._onPause();
				return;
			}
			
			if ( this.$activeItem.is( ':last-child' ) ) {
				if ( this.loop ) {
					this.$episodes.find( '.aiopod-episode:first .aiopod-button--play' ).trigger( 'click' );
				} else {
					this._onPause();
				}
			} else {
				this.$activeItem.next( '.aiopod-episode' ).find( '.aiopod-button--play' ).trigger( 'click' );
			}
		}

		_togglePlayPause() {
			if ( this.player.paused || this.player.ended ) {
				this.player.play();
			} else {
				this.player.pause();
			}
		}

		_scrollToPlayer() {
			const scroll_offset = parseInt( window.aiopod.scroll_offset );

			if ( scroll_offset > 0 ) {
				$( 'html, body' ).animate({
					scrollTop: this.$player.offset().top - scroll_offset
				}, 500 );
			}
		}

		_updateMedia( event ) {
			event.preventDefault();
			
			const $this = $( event.currentTarget ).closest( '.aiopod-episode' );

			const $tmpl = $this.find( '.aiopod-template' );
			if ( ! $tmpl.length || ! $tmpl[0].content ) {
				return; // Nothing to do
			}

			const content  = $tmpl[0].content;
			const $content = $( content );

			const jsonEl = content.querySelector( '.aiopod-template__data' );
			if ( ! jsonEl ) {
    			return; // No JSON data
			}

			let data = {};

			try {
				data = JSON.parse( ( jsonEl.textContent || '{}' ).trim() );
			} catch ( error ) {
				console.error( 'Invalid JSON in template:', error );
				return;
			}		

			const src  = data.src || '';
			const type = data.type || 'audio/mpeg';

			if ( ! src ) {
				console.error( 'No media source found for this episode.' );
				return;
			}

			// Free tier: silently ignore video-only episodes. No UI changes are
			// made so the current active episode stays highlighted and playing.
			if ( ! this._canPlayVideo && 0 === type.indexOf( 'video/' ) ) {
				return;
			}

			this.$activeItem = $this;

			if ( $this.hasClass( 'aiopod-is-active' ) ) {
				const willPlay    = this.player.paused || this.player.ended;
				const isFirstPlay = ! this._hasPlaybackStarted;

				this._togglePlayPause();

				// Scroll only the very first time playback starts — play/pause
				// toggles on an already-started episode must not move the viewport
				if ( willPlay && isFirstPlay ) {
					this._scrollToPlayer();
				}

				return; // Already selected
			}

			this.$root.find( '.aiopod-episode.aiopod-is-active' ).removeClass( 'aiopod-is-active' );
			$this.addClass( 'aiopod-is-active' );

			// Update the player & source
			this.$player.attr( 'data-post_id', parseInt( data.id ) );

			this.player.setSource({
				src: src,
				type: type,
				download: data.download || '',
				poster: data.image || '',
				chapters: data.chapters || []
			});
			
			// Update the play button state
			this.$root.find( '.aiopod-button--playing' ).removeClass( 'aiopod-button--playing' );
			$this.find( '.aiopod-button--play' ).addClass( 'aiopod-button--playing' );
			
			// Update the player image
			const $imageWrapper = this.$player.find( '.aiopod-player__image-wrapper' );			

			if ( $imageWrapper.length > 0 ) {
				const $image = $imageWrapper.find( 'img' );

				if ( $image.length > 0 && type.indexOf( 'audio' ) !== -1 ) {
					if ( data.image ) {
						$image.attr({ 
							src: data.image, 
							alt: data.image_alt || ''
						});
						
						$imageWrapper.show();
					} else {
						$imageWrapper.hide();
					}
				} else {	
					$imageWrapper.hide();
				}			
			}			

			// Update the player title
			const $title = this.$player.find( '.aiopod-player__title' );			

			if ( $title.length > 0 ) {
				const $__title = $content.find( '.aiopod-template__title' ).html();

				if ( $__title && $__title.trim().length > 0 ) {
					$title.html( $__title ).show();
				} else {
					$title.html( '' ).hide();
				}
			}			

			// Update the primary metadata
			const $primaryMeta = this.$player.find( '.aiopod-player__meta--primary' );

			if ( $primaryMeta.length > 0 ) {
				const $__primaryMeta = $content.find( '.aiopod-template__meta--primary' ).html();

				if ( $__primaryMeta && $__primaryMeta.trim().length > 0 ) {
					$primaryMeta.html( $__primaryMeta ).show();
				} else {
					$primaryMeta.html( '' ).hide();
				}
			}

			// Update the secondary metadata
			const $secondaryMeta = this.$player.find( '.aiopod-player__meta--secondary' );

			if ( $secondaryMeta.length > 0 ) {
				const $__secondaryMeta = $content.find( '.aiopod-template__meta--secondary' ).html();

				if ( $__secondaryMeta && $__secondaryMeta.trim().length > 0 ) {
					$secondaryMeta.html( $__secondaryMeta ).show();
				} else {
					$secondaryMeta.html( '' ).hide();
				}
			}

			// Update the social buttons			
			const $socialButtons = this.$player.find( '.aiopod-player__meta--buttons' );

			if ( $socialButtons.length > 0 ) {
				const $__socialButtons = $content.find( '.aiopod-template__meta--buttons' ).html();

				if ( $__socialButtons && $__socialButtons.trim().length > 0 ) {
					$socialButtons.html( $__socialButtons ).show();
				} else {
					$socialButtons.html( '' ).hide();
				}
			}

			// Scroll to Top
			this._scrollToPlayer();

			// Load More
			if ( this.autoadvance && this.$activeItem.is( ':last-child' ) ) {
				if ( this.$moreButton.length > 0 && this.$moreButton.is( ':visible' ) ) {
					this.$moreButton.trigger( 'click' );
				}
			}
		}

	}

	/**
	 * Define a custom HTML element class for <aiopod-slider>
	 */
	class AIOPODSlider extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Set references to the public properties used by the component
			this.slider    = null;
			this.container = null;
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this.slider ) return; // Prevent multiple initializations	
			
			this.container = this.querySelector( '.swiper' );
			if ( ! this.container ) return;	

			// Lazy-load dependencies and initialize when ready
			const swiperPromise = window.Swiper ? Promise.resolve() : loadStyle({
				id: 'aio-podcast-studio-swiper-css',
				href: aiopod.plugin_url + 'vendor/swiper/swiper-bundle.min.css?ver=12.0.3'
			}).then( () => {
				return loadScript({
					id: 'aio-podcast-studio-swiper-js',
					src: aiopod.plugin_url + 'vendor/swiper/swiper-bundle.min.js?ver=12.0.3'
				});
			});

			swiperPromise.then( () => {
				this._initSwiper();
			}).catch( error => {
				console.error( 'Failed to initialize Swiper:', error );
			});
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			if ( this.slider ) {
				this.slider.destroy( true, true );
			}
		}

		/**
         * Define getters and setters for attributes.
         */
		get slidesPerView() {
			const value = this.getAttribute( 'slides-per-view' );

			if ( value === null ) {
				return 3;
			}

			if ( value === 'auto' ) {
				return 'auto';
			}

			const parsed = parseInt( value, 10 );
			return isNaN( parsed ) ? 3 : parsed;
		}

		get spaceBetween() {
			const value = this.getAttribute( 'space-between' );

			if ( value === null ) {
				return 24;
			}

			const parsed = parseInt( value, 10 );
			return isNaN( parsed ) ? 24 : parsed;
		}

		get dots() {
			return this.hasAttribute( 'dots' );
		}

		get arrows() {
			return this.hasAttribute( 'arrows' );
		}

		/**
		 * Define private methods.
		 */
		_initSwiper() {
			const maxSlides = this.slidesPerView;

			const options = {
				slidesPerView: 1,
				spaceBetween: this.spaceBetween,
				observer: true,
				observeParents: true,
			};

			if ( maxSlides !== 'auto' ) {
				options.breakpoints = {
					420: {
						slidesPerView: Math.min( 2, maxSlides ),
					},
					600: {
						slidesPerView: Math.min( 3, maxSlides ),
					},
					768: {
						slidesPerView: maxSlides,
					},
				};
			} else {
				options.slidesPerView = 'auto';
			}

			if ( this.dots ) {
				options.pagination = {
					el: this.querySelector( '.swiper-pagination' ),
					type: 'bullets',
					clickable: true,
				};
			}

			if ( this.arrows ) {
				options.navigation = {
					nextEl: this.querySelector( '.swiper-button-next' ),
					prevEl: this.querySelector( '.swiper-button-prev' ),
				};
			}

			this.slider = new Swiper( this.container, options );
		}

	}

	/**
	 * Define a custom HTML element class for <aiopod-modal>
	 */
	class AIOPODModal extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Set references to the DOM elements used by the component
			this.button = null;
			this.modal  = null;

			// Set references to the private properties used by the component
			this._hasInitialized = false;

			// Bind the event handlers to ensure the reference remains stable
			this._onClick          = this._onClick.bind( this );
			this._onBackdropClick  = this._onBackdropClick.bind( this );
			this._onClose          = this._onClose.bind( this );
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this._hasInitialized ) return; // Prevent multiple initializations

			this.button = this.querySelector( '.aiopod-button--modal' );
			if ( ! this.button ) return;

			this.modal = this.querySelector( '.aiopod-modal' );
			if ( ! this.modal ) return;

			this.closeButton = this.modal.querySelector( '.aiopod-modal__close' );

			this._hasInitialized = true;

			this.button.addEventListener( 'click', this._onClick );
			this.modal.addEventListener( 'click', this._onBackdropClick );

			if ( this.closeButton ) {
				this.closeButton.addEventListener( 'click', this._onClose );
			}
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			if ( ! this._hasInitialized ) return false;
			
			this.button.removeEventListener( 'click', this._onClick );
			this.modal.removeEventListener( 'click', this._onBackdropClick );

			if ( this.closeButton ) {
				this.closeButton.removeEventListener( 'click', this._onClose );
			}
		}

		/**
		 * Define private methods.
		 */
		_onClick( event ) {
			event.preventDefault();
			this.modal.showModal();
		}

		_onBackdropClick( event ) {
			if ( event.target === this.modal ) {
				this.modal.close();
			}
		}

		_onClose() {
			this.modal.close();
		}
	}

	/**
	 * Define a custom HTML element class for <aiopod-tabs>
	 */
	class AIOPODTabs extends HTMLElement {

		/**
		 * Element created.
		 */
		constructor() {
			super();

			// Set references to the DOM elements used by the component
			this.$tabs   = null;
			this.$panels = null;

			// Set references to the private properties used by the component
			this._hasInitialized = false;

			// Bind the event handlers to ensure the reference remains stable
			this._onChange = this._onChange.bind( this );
		}

		/**
		 * Called when the element is added to the document.
		 */
		connectedCallback() {
			if ( this._hasInitialized ) return; // Prevent multiple initializations	

			this.$tabs   = $( this ).find( '.aiopod-tabs__tab[data-tab]' );
			this.$panels = $( this ).find( '.aiopod-tabs__panel' );

			if ( this.$tabs.length === 0 ) return;

			this._hasInitialized = true;

			// Handle click and keyboard events
			this.$tabs.on( 'click keydown', this._onChange );
		}

		/**
		 * Called when the element is removed from the document.
		 */
		disconnectedCallback() {
			if ( ! this._hasInitialized ) return false;
			this.$tabs.off( 'click keydown', this._onChange );
		}

		/**
		 * Define private methods.
		 */
		_onChange( event ) {
			const $tab         = $( event.currentTarget );
			const currentIndex = this.$tabs.index( event.currentTarget );

			if ( event.type === 'keydown' ) {
				const key = event.which;

				// Left arrow
				if ( key === 37 ) {
					event.preventDefault();
					const prevIndex = ( currentIndex - 1 + this.$tabs.length ) % this.$tabs.length;
					this.$tabs.eq( prevIndex ).focus();
					return;
				}

				// Right arrow
				if ( key === 39 ) {
					event.preventDefault();
					const nextIndex = ( currentIndex + 1 ) % this.$tabs.length;
					this.$tabs.eq( nextIndex ).focus();
					return;
				}

				// Enter or Space
				if ( key === 13 || key === 32 ) {
					event.preventDefault();
					$tab.trigger( 'click' );
					return;
				}
			}

			// Click handler
			event.preventDefault();

			if ( $tab.hasClass( 'aiopod-tabs__tab--active' ) ) {
				return;
			}

			// Deactivate all tabs
			this.$tabs.removeClass( 'aiopod-tabs__tab--active' ).attr({
				tabindex: -1,
				'aria-selected': 'false'
			});

			// Hide all panels
			this.$panels.attr( 'aria-hidden', 'true' ).hide();

			// Activate current tab
			$tab.addClass( 'aiopod-tabs__tab--active' ).attr({
				tabindex: 0,
				'aria-selected': 'true'
			});

			// Show associated panel
			const target = $tab.data( 'tab' );
			$( this ).find( target ).attr( 'aria-hidden', 'false' ).show();
		}

	}

	/**
	 * Define a custom HTML element class for <aiopod-pagination>
	 */
    class AIOPODPagination extends HTMLElement {

        /**
         * Constructor
         */
        constructor() {
            super();

            // Set references to the DOM elements used by the component
            this.$el             = null;
            this.$dynamicWrapper = null;

            // Set references to the private properties used by the component
            this._params     = {};
            this._paged      = 1;
            this._totalPages = 1;

            // Bind the event handlers to ensure the reference remains stable
            this._onMoreButtonClicked = this._onMoreButtonClicked.bind( this );
        }

        /**
         * Called when element is added to the DOM
         */
        connectedCallback() {
            if ( ! this.isAjax ) return;

            this.$el = $( this );

            this._params = this.$el.data( 'params' ) || {};
            this._params.action   = 'aiopod_load_' + ( this._params.source || 'episodes' );
            this._params.security = window.aiopod.ajax_nonce;

            this._paged = parseInt( this._params.paged || 1, 10 );
            this._totalPages = parseInt( this._params.max_num_pages || 1, 10 );

            this.$dynamicWrapper = this.$el.closest( '.aiopod' ).find( '.aiopod-dynamic-wrapper' );

            // Attach click handler
            this.$el.on( 'click', 'button', this._onMoreButtonClicked );
        }

        /**
         * Called when element is removed from the DOM
         */
        disconnectedCallback() {
            if ( ! this.isAjax ) return;

            this.$el.off( 'click', 'button', this._onMoreButtonClicked );
        }

        /**
         * Returns true if AJAX pagination is enabled
         */
        get isAjax() {
            return this.hasAttribute( 'is-ajax' );
        }

        /**
         * Handle "Load More" button click
         */
        _onMoreButtonClicked( event ) {
            event.preventDefault();

            const $button = $( event.currentTarget );

            // Prevent multiple clicks while loading
			if ( $button.prop( 'disabled' ) ) return;
            $button.prop( 'disabled', true );

            this.$el.addClass( 'aiopod-spinner' );

            // Increment page
            this._paged++;
            this._params.paged = this._paged;

            // Fetch new items
            this._fetch( this._params, ( response ) => {
                this.$el.removeClass( 'aiopod-spinner' );
				$button.prop( 'disabled', false );

                if ( response.success ) {
                    const html = $( response.data.html ).find( '.aiopod-dynamic-wrapper' ).html();
                    this.$dynamicWrapper.append( html );

                    if ( this._paged >= this._totalPages ) {
                        this.$el.closest( '.aiopod-pagination' ).hide();
                    }

                    this.$el.trigger( 'aiopod:layout-updated' );
                } else {
					this.$el.closest( '.aiopod-pagination' ).hide();
                    console.warn( 'AIOPOD Pagination AJAX failed:', response );
                }
            }).fail( ( jqXHR, textStatus, errorThrown ) => {
               	this.$el.closest( '.aiopod-pagination' ).hide();
                console.error( 'AIOPOD Pagination AJAX error:', textStatus, errorThrown );
            });
        }

        /**
         * AJAX request helper
         */
        _fetch( data, callback ) {
            return $.post( window.aiopod.ajax_url, data, callback, 'json' );
        }

    }

	/**
	 * Init Dropdown.
	 */
	function initDropdown( el ) {
		const $this             = $( el );
		const $inputEl          = $this.find( '.aiopod-dropdown__input' );
		const $dropdownListEl   = $this.find( '.aiopod-dropdown__list' );		
		const $dropdownItemsEl  = $this.find( '.aiopod-dropdown__items' );
		const $dropdownSearchEl = $this.find( '.aiopod-dropdown__search' );		
		const $searchEmptyEl    = $this.find( '.aiopod-dropdown__no-items' ); 
		const $searchInputEl    = $dropdownSearchEl.find( 'input[type="text"]' );     
        const $searchResetBtn   = $dropdownSearchEl.find( 'button' ); 

		const totalItems = $dropdownItemsEl.find( '.aiopod-dropdown__item' ).length;
		const stayOpen = parseInt( $dropdownSearchEl.data( 'stay_open' ) );
		const showSearchThreshold = parseInt( $dropdownSearchEl.data( 'show_search_threshold' ) );

		let currentFocus = -1;

		if ( totalItems == 0 ) {
			return false;
		}

		if ( totalItems >= showSearchThreshold ) {
			$dropdownSearchEl.prop( 'hidden', false );
		}

		// Helpers
		function setSelectedValues() {
			const labels = [];

			$dropdownItemsEl.find( '.aiopod-is-selected' ).each(function() {
				const label = $( this ).find( '.aiopod-dropdown__item-label' ).html().trim();
				labels.push( label );
			});

			$inputEl.val( labels.join( ', ' ) );
		}
		
		function setActive( index ) {
			const $visible = $dropdownItemsEl.find( '.aiopod-dropdown__item:not([hidden])' );

			if ( $visible.length === 0 ) return;

			// Up arrow at top → return to search box
			if ( index < 0 ) {
				if ( ! $dropdownSearchEl.prop( 'hidden' ) ) {
					$searchInputEl.focus();
				}

				$dropdownItemsEl.find( '.aiopod-is-active' ).removeClass( 'aiopod-is-active' );
				currentFocus = -1;
				return;
			}

			if ( index >= $visible.length ) index = 0;
			currentFocus = index;

			$dropdownItemsEl.find( '.aiopod-is-active' ).removeClass( 'aiopod-is-active' );
			
			const $item = $visible.eq( currentFocus );
			$item.addClass( 'aiopod-is-active' );

			// Auto-scroll
			const container = $dropdownListEl.get( 0 );
			const el = $item.get( 0 );

			if ( container && el ) {
				const top    = el.offsetTop;
				const bottom = top + el.offsetHeight;

				if ( top < container.scrollTop ) {
					container.scrollTop = top;
				} else if ( bottom > container.scrollTop + container.clientHeight ) {
					container.scrollTop = bottom - container.clientHeight;
				}
			}
		}

		// Toggle Dropdown
		$inputEl.on( 'click', function() {
			$dropdownListEl.toggle();				
		});	

		// Filter Items
		$searchInputEl.on( 'input', function( event ) {
			$this.addClass( 'aiopod-is-searching' );	

			$searchResetBtn.prop( 'hidden', false );
			$searchEmptyEl.prop( 'hidden', true );			

			const value = this.value.trim().toLowerCase();

			if ( value ) {
				let matchesFound = false;

				$dropdownItemsEl.find( '.aiopod-dropdown__item' ).each(function() {
					const label = $( this ).find( '.aiopod-dropdown__item-label' ).html().trim();

					if ( label.toLowerCase().indexOf( value.toString() ) === -1 ) {
						this.hidden = true;
					} else {
						this.hidden = false;
						matchesFound = true;
					}   
				});

				if ( ! matchesFound ) {
					$searchEmptyEl.prop( 'hidden', false );
				}
			} else {
				$this.removeClass( 'aiopod-is-searching' );

				$searchResetBtn.prop( 'hidden', true );
				$dropdownItemsEl.find( '.aiopod-dropdown__item' ).prop( 'hidden', false );
			}
		});

		// Clear Search
		$searchResetBtn.on( 'click', function( event ) {
			event.preventDefault();

			$this.removeClass( 'aiopod-is-searching' );

			$searchInputEl.val( '' );
			$searchResetBtn.prop( 'hidden', true );

			$searchEmptyEl.prop( 'hidden', true );
			$dropdownItemsEl.find( '.aiopod-dropdown__item' ).prop( 'hidden', false );									
		});

		// Toggle Checkbox
		$dropdownItemsEl.find( '.aiopod-dropdown__item' ).on( 'click', function( event ) {
			const tagName = event.target.tagName.toLowerCase();

			if ( tagName !== 'input' ) {
				$( this ).find( 'input[type="checkbox"]' ).trigger( 'click' );
			}			
		});

		// Set selected values in the input field
		$dropdownItemsEl.find( 'input[type="checkbox"]' ).on( 'change', function() {
			const isChecked = $( this ).is( ':checked' );
			
			if ( isChecked ) {
				$( this ).closest( '.aiopod-dropdown__item' ).addClass( 'aiopod-is-selected' );
			} else {
				$( this ).closest( '.aiopod-dropdown__item' ).removeClass( 'aiopod-is-selected' );
			}

			setSelectedValues();

			if ( ! stayOpen ) {
				$dropdownListEl.hide();
			}
		});	
		
		setSelectedValues();

		// Auto-focus search when typing
		$inputEl.on( 'keydown', function( event ) {
			if ( ! $dropdownSearchEl.prop( 'hidden' ) ) {
				if ( /^[a-zA-Z0-9]$/.test( event.key ) ) {
					$dropdownListEl.show();
					$searchInputEl.focus();
					return;
				}
			}
		});

		// Keyboard navigation
		$inputEl.on( 'keydown', function( event ) {
			// TAB → close dropdown
			if ( event.keyCode === 9 ) {
				$dropdownListEl.hide();
				currentFocus = -1;
				return;
			}

			// Open on down arrow
			if ( $dropdownListEl.is( ':hidden' ) ) {
				if ( event.keyCode === 40 || event.keyCode === 13 || event.keyCode === 32 ) {
					event.preventDefault(); // Prevent page scroll on space
					$dropdownListEl.show();
				} else {
					return;
				}
			}

			const key = event.keyCode;

			if ( key === 40 ) { // ↓
				event.preventDefault();
				setActive( currentFocus + 1 );
			} else if ( key === 38 ) { // ↑
				event.preventDefault();
				setActive( currentFocus - 1 );
			} else if ( key === 13 ) { // ENTER
				event.preventDefault();
				const $visible = $dropdownItemsEl.find( '.aiopod-dropdown__item:not([hidden])' );
				if ( currentFocus > -1 && $visible.length > 0 ) {
					$visible.eq( currentFocus ).trigger( 'click' );
				}
			}
		});

		// ESC inside search field
		$searchInputEl.on( 'keydown', function( event ) {
			// TAB → close dropdown
			if ( event.keyCode === 9 ) {
				$dropdownListEl.hide();
				currentFocus = -1;
				return;
			}

			// ESC → close dropdown
			if ( event.keyCode === 27 ) {
				$dropdownListEl.hide();
				$inputEl.focus();
				currentFocus = -1;
			}

			// Down key → move from search → first item
			if ( event.keyCode === 40 ) {
				event.preventDefault();
				
				const $visible = $dropdownItemsEl.find( '.aiopod-dropdown__item:not([hidden])' );
				if ( $visible.length > 0 ) {
					currentFocus = 0;
					setActive( 0 );
					$inputEl.focus();
				}
			}
		});
	}

	/**
	 * Hide other Dropdowns.
	 */
	function hideOtherDropdowns( self = null ) {
		document.querySelectorAll( '.aiopod-dropdown' ).forEach(( el ) => {
			if ( el !== self ) { 
				el.querySelector( '.aiopod-dropdown__list' ).style.display = 'none';
			}	
		});	
	}

	/**
	 * Progress Bar Utility (Function-based)
	 */
	function createProgressBar() {
		let bar      = null;
		let timer    = null;
		let progress = 0;

		// Initialize the progress bar
		function init() {
			bar = document.querySelector( '.aiopod-progressbar' );
			progress = 0;

			const wrapper = document.querySelector( '.aiopod-progress' );
			if ( ! bar || ! wrapper ) return;

			wrapper.style.display = 'block';
			bar.style.width = '0%';

			simulateProgress();
		}

		// Simulate progress until real completion
		function simulateProgress() {
			timer = setInterval(function() {
				progress += Math.floor( Math.random() * 10 ) + 5; // 5%–15%

				if ( progress >= 90 ) {
					progress = 90; // cap until real completion
					clearInterval( timer );
				}

				bar.style.width = progress + '%';
			}, 200 );
		}

		// Complete the progress bar
		function complete() {
			if ( timer ) {
				clearInterval( timer );
			}

			progress = 100;
			if ( bar ) {
				bar.style.width = '100%';
			}

			setTimeout(function() {
				const wrapper = document.querySelector( '.aiopod-progress' );
				if ( wrapper ) {
					wrapper.style.display = 'none';
				}
			}, 500 );
		}

		// Append the global progress bar element to the bottom of <body> if it doesn't already exist
		if ( ! $( '.aiopod-progress' ).length ) {
			$( 'body' ).append(	'<div class="aiopod aiopod-progress" style="display: none;"><div class="aiopod-progressbar"></div></div>' );
		}

		// Return public methods
		return { init, complete	};
	}

	/**
	 * Called when the page has loaded.
	 */
	$(function() {

		// Register custom elements.
		if ( ! customElements.get( 'aiopod-media' ) ) {
			customElements.define( 'aiopod-media', AIOPODMedia );
		}

		if ( ! customElements.get( 'aiopod-player' ) ) {
			customElements.define( 'aiopod-player', AIOPODPlayer );
		}

		if ( ! customElements.get( 'aiopod-episodes' ) ) {
			customElements.define( 'aiopod-episodes', AIOPODEpisodes );
		}
		
		if ( ! customElements.get( 'aiopod-slider' ) ) {
			customElements.define( 'aiopod-slider', AIOPODSlider );
		}

		if ( ! customElements.get( 'aiopod-modal' ) ) {
			customElements.define( 'aiopod-modal', AIOPODModal );
		}

		if ( ! customElements.get( 'aiopod-tabs' ) ) {
			customElements.define( 'aiopod-tabs', AIOPODTabs );
		}

		if ( ! customElements.get( 'aiopod-pagination' ) ) {
            customElements.define( 'aiopod-pagination', AIOPODPagination );
        }

		// Init Dropdown
		$( '.aiopod-dropdown' ).each(function() {
			initDropdown( this );
		});

		// Hide dropdowns when the user clicks on the document outside the select element.
		document.addEventListener( 'click', ( event ) => {
			const self = event.target.closest( '.aiopod-dropdown' );
			hideOtherDropdowns( self );		
        });

		// Hide dropdowns when the user clicks the ESC key.
		document.addEventListener( 'keydown', ( event ) => {
			if (  event.keyCode == 27 ) {
				hideOtherDropdowns();	
			}
		});
		
		// Episode: Chapters
		$( '.aiopod-single--episode .aiopod-chapter-timestamp' ).on( 'click', function( event ) {
			event.preventDefault();

			const seconds = parseInt( event.currentTarget.dataset.time );
			const player  = document.querySelector( '.aiopod-single--episode aiopod-media' );
					
			if ( player !== null ) {
				player.seekTo( seconds );

				// Scroll to Top
				const scroll_offset = parseInt( window.aiopod.scroll_offset );
				
				if ( scroll_offset > 0 ) {
					$( 'html, body' ).animate({
						scrollTop: $( '.aiopod-single--episode' ).offset().top - scroll_offset
					}, 500 );
				}
			}			
		});

		// Categories Dropdown
		$( '.aiopod-categories--layout-dropdown select' ).on( 'change', function() {
			const selectedEl = this.options[ this.selectedIndex ];

			if ( parseInt( selectedEl.value ) === 0 ) {
				window.location.href = $( this ).closest( '.aiopod-categories--layout-dropdown' ).data( 'uri' );
			} else {
				window.location.href = selectedEl.getAttribute( 'data-uri' );
			}
		});

		// Tags Dropdown
		$( '.aiopod-tags--layout-dropdown select' ).on( 'change', function() {
			const selectedEl = this.options[ this.selectedIndex ];

			if ( parseInt( selectedEl.value ) === 0 ) {
				window.location.href = $( this ).closest( '.aiopod-tags--layout-dropdown' ).data( 'uri' );
			} else {
				window.location.href = selectedEl.getAttribute( 'data-uri' );
			}
		});

		// Filters Form: Live
		$( '.aiopod-filters--mode-live' ).each(function() {
			const $this = $( this );
			const $form = $this.find( 'form' );

			// Submit the form
            const submitForm = function() {
				$form.submit();
			};

			// Attach events to inputs and selects
			$form.find( 'input[name="q"]' ).on( 'keydown', function( event ) {
				if ( event.keyCode === 13 ) {
					submitForm();
				}
			}).on( 'blur', submitForm );

			$form.find( 'input[type="checkbox"]' ).on( 'change', submitForm );

			$form.find( 'select' ).on( 'change', submitForm );
		});

		// Filters Form: Ajax
		const progressBar = createProgressBar();

        $( '.aiopod-filters--mode-ajax' ).each(function() {
            const $this  = $( this );
			const $root  = $this.closest( '.aiopod-episodes' );
            const $form  = $this.find( 'form' );
			const layout = $this.hasClass( 'aiopod-filters--layout-compact' ) ? 'compact' : null;			

            // Base parameters to send with request
            const params = $this.data( 'params' ) || {};

            params.action = 'aiopod_load_episodes';
            params.security = window.aiopod.ajax_nonce;		
			
			// Track the last AJAX request for aborting
			let xhr = null;

			// Debounce timer
			let debounce = null;

			// Check if the form values has been changed
			const getSortedSerializedArray = function( form ) {
				return $form.serializeArray()
					.map( field => [ field.name, field.value ] )
					.sort( ( a, b ) => {
						if ( a[0] === b[0] ) return a[1].localeCompare( b[1] );
						return a[0].localeCompare( b[0] );
					});
			}

			const defaultData = getSortedSerializedArray( $form );

			const isFormChanged = function() {
				const currentData = getSortedSerializedArray( $form );

				if ( defaultData.length !== currentData.length ) return true;

				for ( let i = 0; i < defaultData.length; i++ ) {
					if ( defaultData[i][0] !== currentData[i][0] ||	defaultData[i][1] !== currentData[i][1]	) {
						return true;
					}
				}

				return false;
			}

            // Fetch episodes via AJAX
            const fetchItems = function( event ) {
				if ( event ) {
					event.preventDefault();
				}

				clearTimeout( debounce );

				debounce = setTimeout( () => {
					progressBar.init();
					$this.trigger( 'aiopod:filters-loading' );

					// Clone params into requestData
					const requestData = $.extend( {}, params );

					// Extract and transform form data
					const formData = $form.serializeArray();

					formData.forEach(function( item ) {
						const name  = item.name;
						const value = item.value;

						switch ( name ) {
							case 'q':
								requestData.search_query = value;
								break;
							case 'season':
								requestData.season = value;
								break;
							case 'sort':
								const sortParts = value.split( '-' );

								requestData.orderby = sortParts[0] || params.orderby;
								requestData.order   = sortParts[1] || params.order;
								break;
						}
					});

					// Tags
					if ( $form.find( '.aiopod-filters__tags' ).length > 0 ) {
						const tags = [];

						$form.find( '.aiopod-filters__tags input[type="checkbox"]:checked' ).each(function() {
							tags.push( $( this ).val() );
						});                

						requestData.tags = tags;
					}

					// Toggle buttons visibility
					const showResetButton = isFormChanged();

					if ( 'compact' === layout ) {
						$form.find( '.aiopod-button--submit' ).prop( 'hidden', showResetButton );
						$form.find( '.aiopod-button--reset' ).prop( 'hidden', ! showResetButton );
					} else {
						$form.find( '.aiopod-button--reset' ).prop( 'hidden', ! showResetButton );
					}

					// ABORT previous request before making a new one
					if ( xhr !== null ) {
						xhr.abort();
					}

					// Perform the AJAX request
					xhr = $.post( window.aiopod.ajax_url, requestData, function( response ) {
						xhr = null;

						if ( response && response.data && response.data.html ) {
							const html = $( response.data.html ).find( 'aiopod-episodes' );
							$root.find( 'aiopod-episodes' ).replaceWith( html );
						}

						progressBar.complete();
						$this.trigger( 'aiopod:filters-completed' );
					}).fail(function( response ) {
						xhr = null;							

						if ( response.status === 0 ) {
							// Aborted – ignore
						} else if ( response.status === 429 ) {
							console.warn( 'Too many requests (429). Request throttled.' );
						}

						progressBar.complete();
						$this.trigger( 'aiopod:filters-failed' );
					});
				}, 400 );
            };

            // Attach events to inputs and selects
			$form.find( 'input[name="q"]' ).on( 'keydown', function( event ) {
				if ( event.keyCode === 13 ) {
					fetchItems();
				}
			}).on( 'blur', fetchItems );

            $form.find( 'select' ).on( 'change', fetchItems );	

			$form.on( 'submit', fetchItems );

			const $checkboxes = $form.find( 'input[type="checkbox"]' );			
			$checkboxes.on( 'change', fetchItems );

			$form.find( '.aiopod-button--reset' ).on( 'click', function( event ) {
                event.preventDefault();

                $form[0].reset();
				
				$checkboxes.off( 'change', fetchItems );
				$checkboxes.trigger( 'change' );
				$checkboxes.on( 'change', fetchItems );

				fetchItems();
            }).removeAttr( 'onclick' );
        });
		
	});

})( jQuery );
