<?php

if (!defined('ABSPATH')) {
	exit;
}

if (!function_exists('anchorkit_extract_elementor_content')) {
	/**
	 * Extract content from Elementor document data for preview
	 *
	 * Recursively walks through Elementor's element tree to extract heading widgets
	 * and other text content for TOC generation in the editor preview.
	 *
	 * @param array $elements Elementor document elements
	 * @return string Extracted HTML content
	 */
	function anchorkit_extract_elementor_content($elements)
	{
		if (!is_array($elements)) {
			return '';
		}

		$content = '';

		foreach ($elements as $element) {
			if (!is_array($element)) {
				continue;
			}

			$widget_type = $element['widgetType'] ?? ($element['elType'] ?? '');
			$settings = $element['settings'] ?? array();

			// Extract heading widgets
			if ($widget_type === 'heading') {
				$title = $settings['title'] ?? '';
				$header_size = $settings['header_size'] ?? 'h2';
				// Ensure valid heading tag
				if (!preg_match('/^h[1-6]$/', $header_size)) {
					$header_size = 'h2';
				}
				if ($title) {
					$content .= "<{$header_size}>" . wp_kses_post($title) . "</{$header_size}>\n";
				}
			}
			// Extract text editor widgets
			elseif ($widget_type === 'text-editor') {
				$editor_content = $settings['editor'] ?? '';
				if ($editor_content) {
					// Text editor may already contain HTML with headings
					$content .= wp_kses_post($editor_content) . "\n";
				}
			}
			// Extract HTML widget
			elseif ($widget_type === 'html') {
				$html_content = $settings['html'] ?? '';
				if ($html_content) {
					$content .= wp_kses_post($html_content) . "\n";
				}
			}

			// Recursively process child elements (sections, columns, etc.)
			if (!empty($element['elements']) && is_array($element['elements'])) {
				$content .= anchorkit_extract_elementor_content($element['elements']);
			}
		}

		return $content;
	}
}

if (!function_exists('anchorkit_generate_elementor_schema_markup')) {
	/**
	 * Generate Schema.org JSON-LD data for Elementor TOC
	 * Similar to anchorkit_generate_schema_markup but accepts settings as parameters
	 *
	 * @param array  $headings Array of headings (with 'id' key)
	 * @param string $schema_type Schema type (Article, BlogPosting, etc.)
	 * @return array|null Schema data array or null if disabled
	 */
	function anchorkit_generate_elementor_schema_markup($headings, $schema_type = 'Article')
	{
		if (empty($headings) || !is_array($headings)) {
			return null;
		}

		global $post;
		if (!$post) {
			return null;
		}

		// Build sanitized TOC items for JSON-LD
		$toc_items = array();
		$post_url = get_permalink($post->ID);

		foreach ($headings as $index => $heading) {
			if (empty($heading['text'])) {
				continue;
			}

			// Elementor headings use 'id' not 'anchor'
			$anchor = isset($heading['id']) ? $heading['id'] : (isset($heading['anchor']) ? $heading['anchor'] : '');
			if (empty($anchor)) {
				continue;
			}

			// Build URL with sanitized anchor
			$item_url = $post_url . '#' . sanitize_title($anchor);

			$toc_items[] = array(
				'@type' => 'ListItem',
				'position' => $index + 1,
				'name' => wp_strip_all_tags($heading['text']),
				'url' => esc_url_raw($item_url),
			);
		}

		// Validate we have items
		if (empty($toc_items)) {
			return null;
		}

		// Build the schema with sanitized values
		$schema = array(
			'@context' => 'https://schema.org',
			'@type' => sanitize_text_field($schema_type),
			'headline' => wp_strip_all_tags(get_the_title($post->ID)),
			'url' => esc_url_raw(get_permalink($post->ID)),
			'datePublished' => get_the_date('c', $post->ID),
			'dateModified' => get_the_modified_date('c', $post->ID),
			'author' => array(
				'@type' => 'Person',
				'name' => sanitize_text_field(get_the_author_meta('display_name', $post->post_author)),
			),
			'publisher' => array(
				'@type' => 'Organization',
				'name' => sanitize_text_field(get_bloginfo('name')),
				'url' => esc_url_raw(home_url()),
			),
			'mainEntity' => array(
				'@type' => 'ItemList',
				'name' => 'Table of Contents',
				'itemListElement' => $toc_items,
			),
		);

		// Add featured image if available
		if (has_post_thumbnail($post->ID)) {
			$schema['image'] = esc_url_raw(get_the_post_thumbnail_url($post->ID, 'full'));
		}

		return $schema;
	}
}

if (!function_exists('anchorkit_queue_elementor_schema_markup')) {
	/**
	 * Output Elementor schema markup in wp_head to avoid rendering script tags in content.
	 *
	 * @param array $schema Schema data array.
	 * @return void
	 */
	function anchorkit_queue_elementor_schema_markup($schema)
	{
		if (empty($schema) || !is_array($schema)) {
			return;
		}

		static $queued = false;
		static $queued_schema = array();

		if (!$queued) {
			add_action(
				'wp_head',
				function () use (&$queued_schema) {
					if (empty($queued_schema) || !is_array($queued_schema)) {
						return;
					}

					$json = wp_json_encode($queued_schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
					if (!$json) {
						return;
					}

					wp_print_inline_script_tag($json, array('type' => 'application/ld+json'));
				},
				20
			);
			$queued = true;
		}

		$queued_schema = $schema;
	}
}

if (!function_exists('anchorkit_add_ids_to_content_headings')) {
	/**
	 * Add IDs to headings in content HTML (for word count calculation)
	 * Similar to anchorkit_add_heading_ids_for_elementor but callable directly
	 *
	 * @param string $content HTML content
	 * @param array  $heading_levels Array of heading levels to process (e.g., ['h2', 'h3'])
	 * @return string Modified content with heading IDs
	 */
	function anchorkit_add_ids_to_content_headings($content, $heading_levels = array())
	{
		if (empty($content)) {
			return $content;
		}

		// Default to configured heading levels if none specified
		if (empty($heading_levels)) {
			$runtime_anchor_settings = anchorkit_get_runtime_anchor_settings();
			if ($runtime_anchor_settings && !empty($runtime_anchor_settings['heading_levels'])) {
				$heading_levels = (array) $runtime_anchor_settings['heading_levels'];
			}
		}
		if (empty($heading_levels)) {
			$filtered_levels = apply_filters('anchorkit_inject_heading_levels', null);
			if (is_array($filtered_levels) && !empty($filtered_levels)) {
				$heading_levels = $filtered_levels;
			}
		}
		if (empty($heading_levels)) {
			$heading_levels = anchorkit_get_global_heading_levels();
		}
		$heading_levels = array_values(
			array_unique(
				array_filter(
					array_map(
						function ($level) {
							$level = strtolower(trim((string) $level));
							return preg_match('/^h[1-6]$/', $level) ? $level : null;
						},
						(array) $heading_levels
					)
				)
			)
		);
		if (empty($heading_levels)) {
			$heading_levels = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
		}

		// Build pattern for specified heading levels
		$level_pattern = implode('|', array_map('preg_quote', $heading_levels));
		$pattern = '/<(' . $level_pattern . ')([^>]*)>(.*?)<\/\1>/is';

		$used_ids = array();
		$anchor_counter = 0;
		$runtime_anchor_settings = anchorkit_get_runtime_anchor_settings();
		if ($runtime_anchor_settings && isset($runtime_anchor_settings['anchor_format'])) {
			$anchor_format = sanitize_key($runtime_anchor_settings['anchor_format']);
		} else {
			$anchor_format = anchorkit_get_option('anchorkit_toc_anchor_format', 'auto');
		}
		if (!in_array($anchor_format, array('auto', 'sequential', 'prefixed'), true)) {
			$anchor_format = 'auto';
		}

		$content = preg_replace_callback(
			$pattern,
			function ($matches) use (&$used_ids, &$anchor_counter, $anchor_format) {
				$tag = $matches[1]; // h2, h3, etc.
				$attrs = $matches[2]; // existing attributes
				$text = $matches[3]; // heading text
	
				// Check if heading already has an ID
				$has_existing_id = preg_match('/id=["\']([^"\']+)["\']/i', $attrs, $existing_id_match);

				// IMPORTANT: For sequential and prefixed formats, always regenerate IDs to ensure consistency
				// Only preserve existing IDs when using auto format
				$should_regenerate_id = ($anchor_format !== 'auto') || !$has_existing_id;

				if (!$should_regenerate_id) {
					// Keep existing ID (auto format only)
					return $matches[0];
				}

				// Generate ID from text using the same anchor generation logic as parse_headings
				$clean_text = wp_strip_all_tags($text);

				$id = function_exists('anchorkit_toc_generate_anchor')
					? anchorkit_toc_generate_anchor($clean_text)
					: sanitize_title($clean_text);

				// Handle sequential format
				if ($anchor_format === 'sequential') {
					$anchor_counter++;
					$id = $id . '-' . $anchor_counter;
				}

				// Skip if empty
				if (empty($id)) {
					return $matches[0];
				}

				// Ensure unique IDs (but not for sequential which already has counter)
				if ($anchor_format !== 'sequential') {
					$base_id = $id;
					$counter = 1;
					while (isset($used_ids[$id])) {
						$id = $base_id . '-' . $counter;
						$counter++;
					}
				}
				$used_ids[$id] = true;

				// Remove existing ID if present, then add new one
				if ($has_existing_id) {
					$attrs = preg_replace('/\s*id=["\']([^"\']+)["\']/i', '', $attrs);
				}

				// Add ID to heading
				return '<' . $tag . $attrs . ' id="' . esc_attr($id) . '">' . $text . '</' . $tag . '>';
			},
			$content
		);

		return $content;
	}
}

// Define the auto-insert function in the global scope to ensure it's always available
if (!function_exists('anchorkit_elementor_elements_contain_toc_widget')) {
	/**
	 * Recursively inspect Elementor element arrays for our TOC widget.
	 *
	 * @param mixed $elements
	 * @return bool
	 */
	function anchorkit_elementor_elements_contain_toc_widget($elements)
	{
		if (!is_array($elements)) {
			return false;
		}

		foreach ($elements as $element) {
			if (!is_array($element)) {
				continue;
			}

			$widget_type = $element['widgetType'] ?? ($element['widget_type'] ?? '');
			$widget_type = is_string($widget_type) ? strtolower($widget_type) : '';
			if ($widget_type !== '' && strpos($widget_type, 'anchorkit-toc') === 0) {
				return true;
			}

			if (!empty($element['elements']) && anchorkit_elementor_elements_contain_toc_widget($element['elements'])) {
				return true;
			}
		}

		return false;
	}
}

if (!function_exists('anchorkit_post_has_elementor_toc_widget')) {
	/**
	 * Detect whether an Elementor-built post contains the AnchorKit widget.
	 *
	 * @param WP_Post $post_obj
	 * @return bool
	 */
	function anchorkit_post_has_elementor_toc_widget($post_obj)
	{
		if (!$post_obj || !isset($post_obj->ID)) {
			return false;
		}

		static $cache = array();
		$post_id = (int) $post_obj->ID;
		if (isset($cache[$post_id])) {
			return $cache[$post_id];
		}

		$elementor_data = get_post_meta($post_id, '_elementor_data', true);
		if (empty($elementor_data)) {
			$cache[$post_id] = false;
			return false;
		}

		if (is_string($elementor_data)) {
			$elementor_data = wp_unslash($elementor_data);
			$decoded = json_decode($elementor_data, true);
			$elementor_data = (json_last_error() === JSON_ERROR_NONE) ? $decoded : null;
		}

		if (!is_array($elementor_data)) {
			$cache[$post_id] = false;
			return false;
		}

		// Elementor stores elements either as a flat array or inside an "elements" key.
		$elements = isset($elementor_data['elements']) && is_array($elementor_data['elements'])
			? $elementor_data['elements']
			: $elementor_data;

		$found = anchorkit_elementor_elements_contain_toc_widget($elements);
		$cache[$post_id] = $found;

		return $found;
	}
}

if (!function_exists('anchorkit_elementor_runtime_has_toc_widget')) {
	/**
	 * Detect if the currently rendered Elementor document (page or template) contains the TOC widget.
	 *
	 * @return bool
	 */
	function anchorkit_elementor_runtime_has_toc_widget()
	{
		if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) {
			return false;
		}

		static $document_scan_cache = array();

		$elementor = \Elementor\Plugin::$instance;
		if (!$elementor || !isset($elementor->documents)) {
			return false;
		}

		$documents_to_check = array();

		$current_document = $elementor->documents->get_current();
		if ($current_document && is_callable(array($current_document, 'get_post'))) {
			$doc_post = $current_document->get_post();
			if ($doc_post && isset($doc_post->ID)) {
				$documents_to_check[(int) $doc_post->ID] = $current_document;
			}
		}

		if (empty($documents_to_check) && is_singular() && function_exists('get_the_ID')) {
			$post_id = get_the_ID();
			if ($post_id && method_exists($elementor->documents, 'get_doc_for_frontend')) {
				$page_document = $elementor->documents->get_doc_for_frontend($post_id);
				if ($page_document && is_callable(array($page_document, 'get_post'))) {
					$doc_post = $page_document->get_post();
					if ($doc_post && isset($doc_post->ID)) {
						$documents_to_check[(int) $doc_post->ID] = $page_document;
					}
				}
			}
		}

		if (empty($documents_to_check)) {
			return false;
		}

		foreach ($documents_to_check as $doc_id => $document) {
			// Check if this post is actually being built with Elementor
			// If _elementor_edit_mode is not 'builder', the post was previously edited with Elementor
			// but has been reverted to Gutenberg/classic editor, so we should ignore old Elementor data
			$edit_mode = get_post_meta($doc_id, '_elementor_edit_mode', true);
			if ($edit_mode !== 'builder') {
				$document_scan_cache[$doc_id] = false;
				continue;
			}

			if (isset($document_scan_cache[$doc_id])) {
				if ($document_scan_cache[$doc_id]) {
					return true;
				}
				continue;
			}

			$elements = is_callable(array($document, 'get_elements_data'))
				? $document->get_elements_data()
				: array();

			$document_scan_cache[$doc_id] = anchorkit_elementor_elements_contain_toc_widget($elements);

			if ($document_scan_cache[$doc_id]) {
				return true;
			}
		}

		return false;
	}
}

if (!function_exists('anchorkit_post_contains_manual_toc')) {
	/**
	 * Detect if the current post already contains a manually inserted TOC block or shortcode.
	 *
	 * @param WP_Post|null $post_obj
	 * @return bool
	 */
	function anchorkit_post_contains_manual_toc($post_obj = null)
	{
		if ($post_obj === null) {
			global $post;
			$post_obj = $post;
		}

		if (!$post_obj || !isset($post_obj->ID)) {
			return false;
		}

		global $anchorkit_manual_toc_detected_via_elementor_widget;
		if (!empty($anchorkit_manual_toc_detected_via_elementor_widget)) {
			return true;
		}

		if (anchorkit_elementor_runtime_has_toc_widget()) {
			return true;
		}

		$content = isset($post_obj->post_content) ? (string) $post_obj->post_content : '';

		if ($content !== '') {
			if (function_exists('has_block') && has_block('anchorkit/table-of-contents', $content)) {
				return true;
			}

			if (function_exists('has_shortcode') && has_shortcode($content, 'anchorkit_toc')) {
				return true;
			}
		}

		// Elementor stores widget data in post meta instead of post_content.
		if (anchorkit_post_has_elementor_toc_widget($post_obj)) {
			return true;
		}

		return false;
	}
}

if (!function_exists('anchorkit_extract_elementor_toc_settings_from_elements')) {
	/**
	 * Extract AnchorKit Elementor widget settings from an Elementor elements array.
	 *
	 * @param array $elements
	 * @return array|null
	 */
	function anchorkit_extract_elementor_toc_settings_from_elements($elements)
	{
		if (!is_array($elements)) {
			return null;
		}

		foreach ($elements as $element) {
			if (!is_array($element)) {
				continue;
			}

			$widget_type = $element['widgetType'] ?? ($element['widget_type'] ?? '');
			$widget_type = is_string($widget_type) ? strtolower($widget_type) : '';
			if ($widget_type !== '' && strpos($widget_type, 'anchorkit-toc') === 0) {
				return isset($element['settings']) && is_array($element['settings']) ? $element['settings'] : array();
			}

			if (!empty($element['elements'])) {
				$nested = anchorkit_extract_elementor_toc_settings_from_elements($element['elements']);
				if ($nested !== null) {
					return $nested;
				}
			}
		}

		return null;
	}
}

if (!function_exists('anchorkit_maybe_apply_elementor_runtime_anchor_settings')) {
	/**
	 * For Elementor pages, apply widget-specific anchor settings early so sequential/prefixed IDs
	 * match between TOC generation and heading ID injection.
	 */
	function anchorkit_maybe_apply_elementor_runtime_anchor_settings()
	{
		if (is_admin() || !is_singular()) {
			return;
		}

		static $did_apply = false;
		if ($did_apply) {
			return;
		}

		global $post;
		if (!$post || !isset($post->ID)) {
			return;
		}

		// Only proceed if Elementor is available and this post is built with it
		if (!did_action('elementor/loaded') || !class_exists('\Elementor\Plugin')) {
			return;
		}

		$elementor = \Elementor\Plugin::$instance;
		if (!$elementor || !isset($elementor->documents)) {
			return;
		}

		// Get the document for this post (handles draft/preview)
		$document = method_exists($elementor->documents, 'get_doc_for_frontend')
			? $elementor->documents->get_doc_for_frontend($post->ID)
			: null;

		// Fallback: check post meta directly if no document is available
		$elementor_data = null;
		if ($document && is_callable(array($document, 'get_elements_data'))) {
			$elementor_data = $document->get_elements_data();
		} else {
			$raw = get_post_meta($post->ID, '_elementor_data', true);
			if (is_string($raw)) {
				$raw = wp_unslash($raw);
				$decoded = json_decode($raw, true);
				$elementor_data = (json_last_error() === JSON_ERROR_NONE) ? $decoded : null;
			} elseif (is_array($raw)) {
				$elementor_data = $raw;
			}
		}

		if (empty($elementor_data) || !is_array($elementor_data)) {
			return;
		}

		$elements = isset($elementor_data['elements']) && is_array($elementor_data['elements'])
			? $elementor_data['elements']
			: $elementor_data;

		$settings = anchorkit_extract_elementor_toc_settings_from_elements($elements);
		if ($settings === null) {
			return;
		}

		$anchor_format = isset($settings['anchor_format']) ? sanitize_key($settings['anchor_format']) : null;
		$anchor_prefix = isset($settings['anchor_prefix']) ? $settings['anchor_prefix'] : null;
		$heading_levels = isset($settings['heading_levels']) ? $settings['heading_levels'] : null;

		if (!empty($heading_levels) && is_array($heading_levels)) {
			$heading_levels = array_values(
				array_filter(
					array_map(
						function ($level) {
							$level = strtolower(trim((string) $level));
							return preg_match('/^h[1-6]$/', $level) ? $level : null;
						},
						$heading_levels
					)
				)
			);
		}

		// CRITICAL FIX: Always provide heading levels, using widget defaults when not specified
		// This matches the Gutenberg fix approach and ensures ID injection uses the same
		// heading levels as the widget (see SEQUENTIAL_NUMBER_FIX.md and ELEMENTOR_SEQUENTIAL_FIX.md)
		if (empty($heading_levels)) {
			$heading_levels = array('h2', 'h3', 'h4'); // Widget default from elementor-widget-class.php line 161
		}

		anchorkit_set_runtime_anchor_settings(
			$anchor_format,
			$anchor_prefix,
			$heading_levels,
			true
		);

		// Always add the filter since we now always have heading levels
		add_filter(
			'anchorkit_inject_heading_levels',
			function () use ($heading_levels) {
				return $heading_levels;
			}
		);

		$did_apply = true;
	}
}
add_action('wp', 'anchorkit_maybe_apply_elementor_runtime_anchor_settings', 6);


if (!function_exists('anchorkit_generate_elementor_toc')) {
	/**
	 * Generate TOC for Elementor Widget
	 *
	 * This function generates a Table of Contents specifically for Elementor widgets,
	 * using per-instance settings rather than global plugin settings.
	 *
	 * @param array $settings Widget settings from Elementor
	 * @return string HTML output of the TOC
	 */
	function anchorkit_generate_elementor_toc($settings)
	{
		// Recursion guard - prevent infinite loops during block rendering
		static $is_rendering = false;
		if ($is_rendering) {
			if (defined('WP_DEBUG') && WP_DEBUG) {
				anchorkit_debug_log('AnchorKit: Recursion detected in anchorkit_generate_elementor_toc, returning empty');
			}
			return '';
		}
		$is_rendering = true;

		// Mark that an Elementor instance of the TOC is present so auto-insert skips this request.
		global $anchorkit_manual_toc_detected_via_elementor_widget;
		$anchorkit_manual_toc_detected_via_elementor_widget = true;

		// Detect if we're in Elementor preview/editor mode to force fresh ACF extraction
		$is_elementor_preview = false;
		if (class_exists('\Elementor\Plugin')) {
			$elementor = \Elementor\Plugin::$instance;
			if ($elementor) {
				if (isset($elementor->preview) && is_object($elementor->preview) && method_exists($elementor->preview, 'is_preview_mode')) {
					$is_elementor_preview = $elementor->preview->is_preview_mode();
				}
				if (!$is_elementor_preview && isset($elementor->editor) && is_object($elementor->editor) && method_exists($elementor->editor, 'is_edit_mode')) {
					$is_elementor_preview = $elementor->editor->is_edit_mode();
				}
			}
		}

		// Get the current post content
		global $post;

		// In Elementor editor/preview, try to get the document's post
		if ((!$post || !$post->post_content) && $is_elementor_preview) {
			if (class_exists('\Elementor\Plugin')) {
				$document = \Elementor\Plugin::$instance->documents->get_current();
				if ($document) {
					$post = get_post($document->get_main_id());

					// If we still don't have content, try to get it from Elementor's document data
					if ($post && (!$post->post_content || empty(trim($post->post_content)))) {
						// Try to get content from the Elementor document's elements
						$document_data = $document->get_elements_data();
						if (!empty($document_data)) {
							// Build content from Elementor widgets
							$extracted_content = anchorkit_extract_elementor_content($document_data);
							if ($extracted_content) {
								// Temporarily set post content for preview
								$post->post_content = $extracted_content;
							}
						}
					}
				}
			}
		}

		if (!$post || !$post->post_content) {
			// In Elementor editor, show a helpful preview message instead of error
			if ($is_elementor_preview) {
				$is_rendering = false;
				return '<div class="anchorkit-toc-preview">
                <div class="anchorkit-toc-container" data-theme="light">
                    <div class="toc-title">Table of Contents (Preview)</div>
                    <ul>
                        <li>→ Sample Heading 1</li>
                        <li data-indent="1">→ Sample Sub-heading 1.1</li>
                        <li data-indent="1">→ Sample Sub-heading 1.2</li>
                        <li>→ Sample Heading 2</li>
                        <li data-indent="1">→ Sample Sub-heading 2.1</li>
                    </ul>
                    <p>
                        <em>Preview: Add headings to your content to populate the Table of Contents. View on the frontend to see the actual TOC.</em>
                    </p>
                </div>
            </div>';
			}
			$is_rendering = false;
			return '<div class="anchorkit-toc-error">No content available to generate Table of Contents.</div>';
		}



		// Extract settings with defaults
		$title = $settings['title'] ?? 'Table of Contents';
		$show_title = $settings['show_title'] ?? true;
		$heading_levels = $settings['heading_levels'] ?? array('h2', 'h3', 'h4');
		$min_headings = $settings['min_headings'] ?? 3;
		$exclude_selectors = $settings['exclude_selectors'] ?? '';
		$aria_option = anchorkit_get_option('anchorkit_toc_aria_label', '');
		$global_aria_label = is_string($aria_option) ? trim($aria_option) : '';
		$aria_label = $global_aria_label !== '' ? $global_aria_label : $title;
		if (array_key_exists('aria_label', $settings) && is_string($settings['aria_label']) && $settings['aria_label'] !== '') {
			$aria_label = $settings['aria_label'];
		}
		$custom_css_class = $settings['custom_css_class'] ?? '';
		$design_override = !empty($settings['design_override']);
		$instance_id = $settings['instance_id'] ?? '';

		// Get custom ID from settings if provided
		$custom_id_override = isset($settings['custom_id']) && !empty($settings['custom_id']) ? sanitize_html_class(trim($settings['custom_id'])) : '';

		// Use custom ID if provided, otherwise use instance_id or generate one
		$toc_instance_id = !empty($custom_id_override) ? $custom_id_override : (!empty($instance_id) ? $instance_id : ('anchorkit-toc-' . uniqid()));

		$hierarchical = $settings['hierarchical'] ?? true;
		$collapsible = $settings['collapsible'] ?? true;
		$initial_state = $settings['initial_state'] ?? 'expanded';
		$smooth_scroll = $settings['smooth_scroll'] ?? true;
		$scroll_offset = $settings['scroll_offset'] ?? 0;
		$scroll_easing = $settings['scroll_easing'] ?? 'ease-in-out';
		$scroll_duration = $settings['scroll_duration'] ?? 500;
		$scroll_spy = $settings['scroll_spy'] ?? true;
		$context = isset($settings['context']) ? $settings['context'] : '';

		$context_source = ($context === 'gutenberg') ? 'gutenberg_block' : 'elementor_widget';
		if (function_exists('anchorkit_prepare_toc_context')) {
			$toc_context = anchorkit_prepare_toc_context(
				array(
					'source' => $context_source,
					'post_id' => ($post && isset($post->ID)) ? (int) $post->ID : 0,
					'data' => array(
						'elementor_settings' => $settings,
						'instance_id' => $toc_instance_id,
					),
				)
			);
		} else {
			$toc_context = array(
				'source' => $context_source,
				'post_id' => ($post && isset($post->ID)) ? (int) $post->ID : 0,
				'data' => array(
					'elementor_settings' => $settings,
					'instance_id' => $toc_instance_id,
				),
				'render_settings' => array(),
			);
		}
		$theme = $settings['theme'] ?? 'system';
		// Validate theme value
		$valid_themes = array('system', 'light', 'dark');
		if (!in_array($theme, $valid_themes, true)) {
			$theme = 'system';
		}

		// Override theme for editor preview mode (for preview only, doesn't affect saved data)
		if ($is_elementor_preview && !empty($settings['design_override']) && $settings['design_override'] === 'yes') {
			$editor_mode_theme = $settings['design_editor_mode'] ?? '';
			if ($editor_mode_theme === 'light' || $editor_mode_theme === 'dark') {
				$theme = $editor_mode_theme;
			}
		}
		$hide_on_mobile = $settings['hide_on_mobile'] ?? false;
		$mobile_breakpoint = $settings['mobile_breakpoint'] ?? 782;
		$style_preset = $settings['style_preset'] ?? 'minimal';
		$toc_width = $settings['toc_width'] ?? 100;
		$effective_toc_width = (isset($toc_width) && $toc_width > 0 && $toc_width <= 100) ? (int) $toc_width : null;
		$alignment_setting = $settings['alignment'] ?? null;
		$float_setting = $settings['float'] ?? null;

		$valid_alignment_values = array('left', 'center', 'right');
		$alignment_preference = in_array($alignment_setting, $valid_alignment_values, true)
			? $alignment_setting
			: anchorkit_get_option('anchorkit_toc_alignment', 'center');

		$valid_float_values = array('none', 'left', 'right');
		$float_preference = in_array($float_setting, $valid_float_values, true)
			? $float_setting
			: anchorkit_get_option('anchorkit_toc_float', 'none');

		// Normalise heading levels for consistent ID injection
		$normalized_heading_levels = array();
		if (is_array($heading_levels)) {
			foreach ($heading_levels as $level) {
				$level = strtolower(trim((string) $level));
				if (preg_match('/^h[1-6]$/', $level)) {
					$normalized_heading_levels[] = $level;
				}
			}
		}
		if (empty($normalized_heading_levels)) {
			$normalized_heading_levels = array('h2', 'h3', 'h4');
		}
		$heading_levels = $normalized_heading_levels;

		// Persist anchor runtime settings (format + prefix + levels) so ID injection matches this widget
		$anchor_format_setting = $settings['anchor_format'] ?? 'auto';
		$anchor_prefix_setting = $settings['anchor_prefix'] ?? 'section';
		anchorkit_set_runtime_anchor_settings(
			$anchor_format_setting,
			$anchor_prefix_setting,
			$normalized_heading_levels,
			true
		);

		// Pro features - Sticky (initialized with safe defaults)
		$sticky = false;
		$sticky_position = 'content';
		$sticky_offset = 20;

		// Pro features - View More
		$view_more_enabled = false;
		$initial_count = 5;
		$view_more_text = 'View More';
		$view_less_text = 'View Less';

		// Pro features - Animations
		$entrance_animation = false;
		$animation_type = 'fade';

		// Premium feature overrides
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$sticky = isset($settings['sticky']) ? (bool) $settings['sticky'] : false;
				$sticky_position = $settings['sticky_position'] ?? 'content';
				$sticky_offset = $settings['sticky_offset'] ?? 20;

				$view_more_enabled = $settings['view_more_enabled'] ?? false;
				$initial_count = $settings['initial_count'] ?? 5;
				$view_more_text = $settings['view_more_text'] ?? 'View More';
				$view_less_text = $settings['view_less_text'] ?? 'View Less';

				$entrance_animation = $settings['entrance_animation'] ?? false;
				$animation_type = $settings['animation_type'] ?? 'fade';
			}
		}

		// Custom heading labels - use per-instance JSON when provided, fallback to global option
		$custom_labels = array();
		$custom_labels_json = '';
		if (isset($settings['custom_labels']) && $settings['custom_labels'] !== '') {
			if (is_array($settings['custom_labels'])) {
				foreach ($settings['custom_labels'] as $key => $value) {
					$custom_labels[trim((string) $key)] = trim((string) $value);
				}
			} else {
				$custom_labels_json = $settings['custom_labels'];
			}
		} else {
			$custom_labels_json = anchorkit_get_option('anchorkit_toc_custom_labels', '');
		}

		if (!empty($custom_labels_json) && empty($custom_labels)) {
			$custom_labels_json = wp_unslash($custom_labels_json);
			$decoded = json_decode($custom_labels_json, true);
			if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) {
				foreach ($decoded as $key => $value) {
					$custom_labels[trim((string) $key)] = trim((string) $value);
				}
			} elseif (defined('WP_DEBUG') && WP_DEBUG) {
				anchorkit_debug_log('AnchorKit JSON Error: ' . json_last_error_msg() . ' | Raw JSON: ' . $custom_labels_json);
			}
		}

		// Pro features - Bullets & Numbering
		$bullet_style = $settings['bullet_style'] ?? 'disc';
		$bullet_character = $settings['bullet_character'] ?? '•';
		$bullet_color_override = '';
		if (isset($settings['bullet_color']) && !empty($settings['bullet_color'])) {
			// Ensure color has # prefix if it's missing
			$color = trim($settings['bullet_color']);
			if (strpos($color, '#') !== 0) {
				$color = '#' . ltrim($color, '#');
			}
			// Sanitize hex color
			$sanitized = sanitize_hex_color($color);
			if ($sanitized) {
				$bullet_color_override = $sanitized;
			} elseif (preg_match('/^#[0-9A-Fa-f]{6}$/', $color) || preg_match('/^#[0-9A-Fa-f]{3}$/', $color)) {
				// Fallback: if sanitize_hex_color fails but it's a valid hex format (6 or 3 digits), use it
				$bullet_color_override = $color;
			}
		}
		// Note: Bullet colors use global settings from admin panel (handled via CSS)
		$show_numerals = $settings['show_numerals'] ?? false;
		$numbering_style = $settings['numbering_style'] ?? 'hierarchical';
		$numbering_format = $settings['numbering_format'] ?? 'decimal';
		$numbering_sep = $settings['numbering_separator'] ?? '.';

		// Pro features - Advanced Filtering
		$exclude_keywords = $settings['exclude_keywords'] ?? '';
		$exclude_regex = $settings['exclude_regex'] ?? '';
		$min_heading_depth = $settings['min_heading_depth'] ?? 2;
		$max_heading_depth = $settings['max_heading_depth'] ?? 6;

		// Check if H5 or H6 are explicitly included in heading_levels
		// If so, we need to adjust the max depth filter to allow them
		$has_h5_or_h6 = in_array('h5', $heading_levels, true) || in_array('h6', $heading_levels, true);
		if ($has_h5_or_h6) {
			if (in_array('h6', $heading_levels, true)) {
				$max_heading_depth = max($max_heading_depth, 6);
			} elseif (in_array('h5', $heading_levels, true)) {
				$max_heading_depth = max($max_heading_depth, 5);
			}
		}

		// Pro features - Reading Time
		$show_reading_time = $settings['show_reading_time'] ?? false;
		$reading_speed = $settings['reading_speed'] ?? 200;
		$show_word_count = $settings['show_word_count'] ?? false;
		$time_format = $settings['time_format'] ?? 'min_read';

		// Pro features - Advanced Typography
		$advanced_typography_override = $settings['advanced_typography_override'] ?? false;
		// Handle both slider and number formats
		$get_font_size = function ($value, $default) {
			if (is_array($value) && isset($value['size']) && $value['size'] !== '') {
				return is_numeric($value['size']) ? (int) $value['size'] : $default;
			}
			if (is_numeric($value)) {
				return (int) $value;
			}
			return $default;
		};
		$title_font_size = $get_font_size($settings['title_font_size'] ?? null, 20);
		$h2_font_size = $get_font_size($settings['h2_font_size'] ?? null, 18);
		$h3_font_size = $get_font_size($settings['h3_font_size'] ?? null, 16);
		$h4_font_size = $get_font_size($settings['h4_font_size'] ?? null, 14);
		$h5_font_size = $get_font_size($settings['h5_font_size'] ?? null, 13);
		$h6_font_size = $get_font_size($settings['h6_font_size'] ?? null, 12);

		// Pro features - Back to Top
		// PRO: Back to Top - default from global settings
		$back_to_top_link_default = false;
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$back_to_top_link_default = (bool) anchorkit_get_option('anchorkit_toc_back_to_top_link', false);
			}
		}
		$back_to_top_link = isset($settings['back_to_top_link'])
			? (bool) $settings['back_to_top_link']
			: $back_to_top_link_default;
		$back_to_top_text = $settings['back_to_top_text'] ?? anchorkit_get_option('anchorkit_toc_back_to_top_text', 'Back to Top');
		$back_to_top_font_size = $settings['back_to_top_font_size'] ?? anchorkit_get_option('anchorkit_toc_back_to_top_font_size', 14);
		if (is_array($back_to_top_font_size)) {
			$back_to_top_font_size = $back_to_top_font_size['size'] ?? 14;
		}
		$back_to_top_font_size = is_numeric($back_to_top_font_size) ? (int) $back_to_top_font_size : 14;

		// Pro features - Schema
		$valid_schema_types = array('Article', 'BlogPosting', 'WebPage', 'HowTo', 'FAQPage', 'NewsArticle', 'TechArticle', 'Course');
		$schema_type_option = anchorkit_get_option('anchorkit_toc_schema_type', 'Article');
		$per_post_type_schema = anchorkit_get_option('anchorkit_toc_schema_type_per_post_type', array());
		if (isset($post->post_type) && isset($per_post_type_schema[$post->post_type]) && !empty($per_post_type_schema[$post->post_type])) {
			$schema_type_option = $per_post_type_schema[$post->post_type];
		}
		if (!in_array($schema_type_option, $valid_schema_types, true)) {
			$schema_type_option = 'Article';
		}
		// PRO: Schema enabled
		$schema_enabled = false;
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$schema_enabled = (bool) anchorkit_get_option('anchorkit_toc_schema_enabled', false);
			}
		}
		if (array_key_exists('schema_enabled', $settings) && $settings['schema_enabled'] !== null) {
			$schema_enabled = (bool) $settings['schema_enabled'];
		}
		$schema_type = $schema_type_option;
		if (!empty($settings['schema_type']) && in_array($settings['schema_type'], $valid_schema_types, true)) {
			$schema_type = $settings['schema_type'];
		}

		if (!isset($toc_context['render_settings']) || !is_array($toc_context['render_settings'])) {
			$toc_context['render_settings'] = array();
		}
		$toc_context['render_settings'] = array_merge(
			$toc_context['render_settings'],
			array(
				'title' => $title,
				'show_title' => $show_title,
				'collapsible' => $collapsible,
				'hierarchical' => $hierarchical,
				'sticky' => $sticky,
				'theme' => $theme,
				'style_preset' => $style_preset,
				'view_more_enabled' => $view_more_enabled,
				'show_numerals' => $show_numerals,
				'numbering_style' => $numbering_style,
				'bullet_style' => $bullet_style,
			)
		);

		// Parse headings from content
		// Use DOMDocument to parse the actual rendered content from the page
		// This works better than trying to get Elementor content during widget render
		$content = $post->post_content;

		// PRO: ACF Integration - extract headings from ACF fields if enabled
		$acf_enabled = false;
		$acf_merge_mode = 'after';
		$acf_content = '';
		$acf_extraction_succeeded = false;

		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$acf_enabled = (bool) anchorkit_get_option('anchorkit_toc_acf_enabled', false);
				if (array_key_exists('acf_enabled', $settings) && $settings['acf_enabled'] !== null) {
					$acf_enabled = (bool) $settings['acf_enabled'];
				}

				$acf_merge_mode = anchorkit_get_option('anchorkit_toc_acf_merge_mode', 'after');
				if (array_key_exists('acf_merge_mode', $settings) && !empty($settings['acf_merge_mode'])) {
					$acf_merge_mode = sanitize_key($settings['acf_merge_mode']);
				}
				if (!in_array($acf_merge_mode, array('before', 'after', 'replace'), true)) {
					$acf_merge_mode = 'after';
				}

				$acf_field_names_override = array_key_exists('acf_field_names', $settings) && $settings['acf_field_names'] !== null ? $settings['acf_field_names'] : null;

				if ($acf_enabled && $post->ID) {
					// For Gutenberg REST preview, check if ACF functions are available
					// ACF might not be fully initialized during REST requests
					$can_extract_acf = true;
					if ($context === 'gutenberg' && defined('REST_REQUEST') && REST_REQUEST) {
						if (!function_exists('get_field') || !function_exists('get_fields')) {
							$can_extract_acf = false;
						}
					}

					if ($can_extract_acf) {
						$acf_content = anchorkit_extract_acf_content($post->ID, $acf_field_names_override);
						$acf_extraction_succeeded = true;
					}
				}
			}
		}

		// Merge ACF content with post content based on merge mode
		$heading_source = $content;
		if ($acf_enabled && $acf_extraction_succeeded && !empty($acf_content)) {
			if ($acf_merge_mode === 'before') {
				$heading_source = $acf_content . "\n\n" . $content;
			} elseif ($acf_merge_mode === 'after') {
				$heading_source = $content . "\n\n" . $acf_content;
			} elseif ($acf_merge_mode === 'replace') {
				$heading_source = $acf_content;
			}
		}

		// Apply WordPress content filters to get rendered HTML unless called from Gutenberg block
		// Prevent recursive filtering when rendering dynamic blocks in editor/preview

		// IMPORTANT: For Gutenberg, we need to carefully handle content filtering
		if ($context === 'gutenberg') {
			// When ACF replace mode is active AND we have actual ACF content,
			// ACF content doesn't contain Gutenberg blocks so skip do_blocks() to avoid potential issues
			$skip_do_blocks = $acf_enabled && $acf_extraction_succeeded && !empty($acf_content) && $acf_merge_mode === 'replace';

			if ($skip_do_blocks) {
				// ACF content doesn't need block rendering - just apply text filters
				$heading_source = wptexturize($heading_source);
				$heading_source = wpautop($heading_source);
				$heading_source = shortcode_unautop($heading_source);
				if (function_exists('do_shortcode')) {
					$heading_source = do_shortcode($heading_source);
				}
			} else {
				// Remove TOC blocks from content to avoid infinite recursion, then render other blocks
				// This allows word counting on rendered content without causing loops
				// Handle both self-closing (<!-- wp:block /--> ) and regular block syntax (<!-- wp:block -->...<!-- /wp:block -->)
				$content_without_toc = preg_replace(
					'/<!--\s*wp:anchorkit\/table-of-contents\b[^>]*?(?:\/-->|-->.*?<!--\s*\/wp:anchorkit\/table-of-contents\s*-->)/s',
					'',
					$heading_source
				);

				// Only render blocks if we have content to prevent infinite loops
				if (!empty(trim($content_without_toc)) && function_exists('do_blocks')) {
					$heading_source = do_blocks($content_without_toc);
				} else {
					$heading_source = $content_without_toc;
				}

				// Apply other essential filters for proper rendering
				$heading_source = wptexturize($heading_source);
				$heading_source = wpautop($heading_source);
				$heading_source = shortcode_unautop($heading_source);
				if (function_exists('do_shortcode')) {
					$heading_source = do_shortcode($heading_source);
				}
			}

			// CRITICAL: Add IDs to headings BEFORE extracting them
			// This is essential for word count calculation which uses anchor IDs to find sections
			// Now using Gutenberg-compatible ID generation (removes underscores)
			$heading_source = anchorkit_add_ids_to_content_headings($heading_source, $heading_levels);
		} else {
			// For non-Gutenberg contexts, use fully filtered content
			$heading_source = apply_filters('the_content', $heading_source); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core hook.

			// CRITICAL: Add IDs to headings in content BEFORE extracting them
			// This ensures TOC links match the actual heading IDs in the rendered content
			$heading_source = anchorkit_add_ids_to_content_headings($heading_source, $heading_levels);
		}

		// Extract headings from the heading source (now with IDs)
		$headings = anchorkit_extract_headings($heading_source, $heading_levels, $exclude_selectors);

		// Register filter to inject the same IDs into the actual page content (skip for Gutenberg; blocks register their own filter)
		if ($context !== 'gutenberg' && !has_filter('the_content', 'anchorkit_add_heading_ids_for_elementor')) {
			add_filter('the_content', 'anchorkit_add_heading_ids_for_elementor', 5); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core hook.
		}

		// Filter by exclude keywords (available for all users, matching Gutenberg block behavior)
		if (!empty($headings) && !empty($exclude_keywords)) {
			$keywords = array_filter(array_map('trim', explode(',', $exclude_keywords)));
			if (!empty($keywords)) {
				$headings = array_values(
					array_filter(
						$headings,
						function ($heading) use ($keywords) {
							foreach ($keywords as $keyword) {
								if ($keyword !== '' && stripos($heading['text'], $keyword) !== false) {
									return false; // Exclude this heading
								}
							}
							return true; // Keep this heading
						}
					)
				);
			}
		}

		// PRO: Filter headings by regex and depth
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && !empty($headings)) {

				// Filter by min/max heading depth
				if ($min_heading_depth > 1 || $max_heading_depth < 6) {
					$headings = array_filter(
						$headings,
						function ($heading) use ($min_heading_depth, $max_heading_depth) {
							$level = is_numeric($heading['level']) ? (int) $heading['level'] : (int) str_replace('h', '', $heading['level']);
							return $level >= $min_heading_depth && $level <= $max_heading_depth;
						}
					);
					// Re-index array after filtering
					$headings = array_values($headings);
				}
			}
		}

		// If running inside the block editor preview, avoid processing shortcodes/blocks again
		// to prevent duplication or recursion
		if ($context === 'gutenberg' && defined('REST_REQUEST') && REST_REQUEST) {
			$min_headings = max(1, (int) $min_headings);
		}

		// PRO: Compute word counts for reading time/word count metadata in Elementor
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && ($show_reading_time || $show_word_count) && !empty($heading_source) && !empty($headings)) {
				// Convert to the structure expected by the calculator (uses 'anchor' keys)
				$calc_headings = array();
				foreach ($headings as $h) {
					$calc_headings[] = array(
						'level' => is_numeric($h['level']) ? (int) $h['level'] : (int) str_replace('h', '', $h['level']),
						'text' => $h['text'],
						'anchor' => $h['id'],
					);
				}
				// Calculate word counts using the same heading source where we extracted headings from
				$counted = anchorkit_toc_calculate_section_word_counts($heading_source, $calc_headings);
				// Merge word_count back into Elementor headings array
				foreach ($counted as $idx => $c) {
					if (isset($headings[$idx])) {
						$headings[$idx]['word_count'] = isset($c['word_count']) ? (int) $c['word_count'] : 0;
					}
				}
			}
		}

		$headings = apply_filters('anchorkit_toc_headings', $headings, $toc_context);

		// Check if we have enough headings
		if (empty($headings) || count($headings) < $min_headings) {
			$is_rendering = false;

			// For Gutenberg preview, show a helpful message instead of empty response
			// This prevents the JavaScript from showing an infinite loading spinner
			if ($context === 'gutenberg' && defined('REST_REQUEST') && REST_REQUEST) {
				return '<div class="anchorkit-block-notice">
                    <p><strong>Table of Contents</strong></p>
                    <p>Not enough headings found. Add at least ' . $min_headings . ' headings to your content to display the Table of Contents.</p>
                </div>';
			}

			return ''; // Don't show TOC on frontend if not enough headings
		}

		// PRO: Check if we're on an AMP page
		$is_amp_page = function_exists('anchorkit_is_amp') && anchorkit_is_amp();
		// PRO: AMP support
		$amp_enabled = false;
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$amp_enabled = (bool) anchorkit_get_option('anchorkit_toc_amp_enabled', false);
			}
		}
		if (array_key_exists('amp_enabled', $settings) && $settings['amp_enabled'] !== null) {
			$amp_enabled = (bool) $settings['amp_enabled'];
		}

		// If on AMP page, check if AMP support is enabled
		if ($is_amp_page) {
			if (!$amp_enabled) {
				// AMP support is disabled - don't render broken TOC on AMP pages
				$is_rendering = false;
				// Check if user has premium version
				$has_premium = false;
				if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
					$has_premium = anchorkit_fs()->can_use_premium_code();
				}
				if (!$has_premium) {
					return '<!-- AnchorKit: AMP Compatibility requires AnchorKit Pro. Visit plugin settings to upgrade. -->';
				}
				return '<!-- AnchorKit: Enable AMP Compatibility in Pro settings (Structure & SEO tab) to display TOC on AMP pages. -->';
			}
			// AMP support enabled - render AMP-compatible version
			if (function_exists('anchorkit_generate_amp_toc')) {
				$amp_headings = anchorkit_prepare_amp_headings($headings);
				$is_rendering = false;
				return anchorkit_generate_amp_toc($amp_headings);
			}
		}

		$global_custom_styling_raw = anchorkit_get_option('anchorkit_toc_custom_styling', false);
		$global_custom_styling = filter_var($global_custom_styling_raw, FILTER_VALIDATE_BOOLEAN);

		// Only add custom-styling class when there are ACTUAL custom overrides to apply
		// This prevents the generic fallback values from overriding preset-specific styles
		$has_custom_overrides = false;
		if ($design_override) {
			// Check if any color tokens, container sizing, or box shadow is set
			$has_custom_overrides = !empty($settings['override_tokens_light'])
				|| !empty($settings['override_tokens_dark'])
				|| !empty($settings['override_tokens'])
				|| isset($settings['container_padding'])
				|| isset($settings['container_border_width'])
				|| isset($settings['container_border_radius'])
				|| (!empty($settings['box_shadow_light']) && trim((string) $settings['box_shadow_light']) !== '')
				|| (!empty($settings['box_shadow_dark']) && trim((string) $settings['box_shadow_dark']) !== '');
		}

		// Custom styling behavior:
		// The .anchorkit-toc-custom-styling class is ONLY needed when there are container styling
		// overrides (padding, border, box-shadow, etc.) that need CSS variables instead of preset's fixed values.
		// Color overrides work WITHOUT this class because presets now use var() with fallbacks for colors.
		//
		// For both Gutenberg and Elementor: use custom-styling ONLY when there are container overrides
		// This allows users to customize colors while keeping preset design (padding, borders, etc.)
		//
		// CRITICAL: Only count as "container override" if the value was EXPLICITLY SET BY USER.
		// When design override toggle is ON but user hasn't touched these controls, Elementor may send
		// empty string values that should NOT trigger custom-styling class.
		//
		// Note: isset() returns true even for 0, which is correct - if user explicitly sets padding to 0,
		// that IS an override. We just need to filter out empty strings and null.
		$has_container_overrides = (isset($settings['container_padding']) && $settings['container_padding'] !== '' && $settings['container_padding'] !== null && is_numeric($settings['container_padding']))
			|| (isset($settings['container_border_width']) && $settings['container_border_width'] !== '' && $settings['container_border_width'] !== null && is_numeric($settings['container_border_width']))
			|| (isset($settings['container_border_radius']) && $settings['container_border_radius'] !== '' && $settings['container_border_radius'] !== null && is_numeric($settings['container_border_radius']))
			|| (!empty($settings['box_shadow_light']) && trim((string) $settings['box_shadow_light']) !== '')
			|| (!empty($settings['box_shadow_dark']) && trim((string) $settings['box_shadow_dark']) !== '');

		// Both Gutenberg and Elementor: use custom-styling class when there are actual customizations
		// (container or colors). This ensures the clean 'Custom' look (no preset-specific borders/accents)
		// is applied when the user explicitly chooses to customize the design.
		$use_custom_styling = $has_container_overrides || ($design_override && $has_custom_overrides);

		// Build TOC HTML
		$classes = array('anchorkit-toc-container', 'anchorkit-toc-elementor');
		$classes[] = 'anchorkit-toc-theme-' . $theme;

		// Use EITHER preset OR custom-styling class based on whether custom styling is needed
		if ($use_custom_styling) {
			$classes[] = 'anchorkit-toc-custom-styling';
		} else {
			$classes[] = 'anchorkit-toc-preset-' . $style_preset;
		}

		if ($collapsible) {
			$classes[] = 'anchorkit-toc-collapsible';
			$classes[] = $initial_state === 'collapsed' ? 'anchorkit-toc-collapsed' : 'anchorkit-toc-expanded';
		}

		if ($hierarchical) {
			$classes[] = 'anchorkit-toc-hierarchical';
		}

		if ($sticky) {
			$classes[] = 'anchorkit-toc-sticky';
			$classes[] = 'anchorkit-toc-sticky-' . $sticky_position;
		}

		if ($entrance_animation) {
			$classes[] = 'anchorkit-toc-entrance-animation';
			$classes[] = 'anchorkit-toc-animate-' . $animation_type;
		}

		// Bullet style classes
		if ($bullet_style) {
			$classes[] = 'anchorkit-toc-bullet-' . $bullet_style;
		}

		// Numerals classes
		if ($show_numerals) {
			$classes[] = 'anchorkit-toc-numerals';
			$classes[] = 'anchorkit-toc-numbering-' . $numbering_style;
		}

		$classes = apply_filters('anchorkit_toc_container_classes', $classes, $toc_context);
		$classes = apply_filters('anchorkit_toc_container_class', $classes);

		// Hide on mobile class
		if ($hide_on_mobile) {
			$classes[] = 'anchorkit-toc-hide-mobile';
		}

		// Custom CSS classes (space-separated, optional . prefix)
		if (!empty($custom_css_class)) {
			$custom_items = explode(' ', $custom_css_class);
			foreach ($custom_items as $item) {
				$item = trim($item);
				if (empty($item)) {
					continue;
				}

				// Remove . prefix if present
				$class_value = strpos($item, '.') === 0 ? substr($item, 1) : $item;
				$sanitized = sanitize_html_class($class_value);
				if (!empty($sanitized)) {
					$classes[] = $sanitized;
				}
			}
		}

		// For Gutenberg blocks with custom styling: populate CSS variables from global admin settings
		// This matches how auto-insert works - global settings provide the base, user can override

		$box_shadow_light_value = isset($settings['box_shadow_light']) ? trim((string) $settings['box_shadow_light']) : '';
		$box_shadow_dark_value = isset($settings['box_shadow_dark']) ? trim((string) $settings['box_shadow_dark']) : '';
		$instance_style_rules = '';

		// Build inline styles
		$inline_styles = array();

		// CRITICAL: When custom styling is enabled for Gutenberg, populate ONLY non-color CSS variables
		// from global admin settings. Color tokens should come from presets, not global settings.
		// This matches how presets work - they define colors, while container/typography can be customized.
		if ($context === 'gutenberg' && $use_custom_styling && function_exists('anchorkit_collect_toc_tokens')) {
			$global_tokens = anchorkit_collect_toc_tokens();
			$base_tokens = isset($global_tokens['light']) ? $global_tokens['light'] : array();

			// Apply ONLY container/typography tokens (padding, border, font-size, font-family)
			// Exclude color tokens so presets can define colors properly
			$allowed_vars = array('--anchorkit-toc-padding', '--anchorkit-toc-border-width', '--anchorkit-toc-border-radius', '--anchorkit-toc-font-size', '--anchorkit-toc-font-family', '--anchorkit-toc-bullet-gap');
			foreach ($base_tokens as $var => $value) {
				if (in_array($var, $allowed_vars, true) && $value !== '' && $value !== null) {
					$inline_styles[] = $var . ': ' . esc_attr($value);
				}
			}
		}

		// Apply per-instance CSS variable overrides if provided
		if (!empty($settings['override_tokens']) && is_array($settings['override_tokens'])) {
			foreach ($settings['override_tokens'] as $var => $value) {
				if (is_string($var) && is_string($value) && strpos($var, '--anchorkit-toc-') === 0) {
					$inline_styles[] = $var . ': ' . esc_attr($value);
				}
			}
		}
		// Note: Bullet colors are handled via global CSS variables in style blocks, not inline styles
		// Inline styles would override media queries for dark mode (system theme)
		if ($bullet_style === 'custom_character' && $bullet_character) {
			$inline_styles[] = '--anchorkit-toc-custom-bullet: "' . esc_attr($bullet_character) . '"';
		}
		if (!empty($bullet_color_override)) {
			$inline_styles[] = '--anchorkit-toc-bullet-color: ' . esc_attr($bullet_color_override) . ' !important';
		}
		// Container sizing overrides (if provided via settings)
		// Apply as CSS variables - will be picked up by .anchorkit-toc-custom-styling CSS
		// These override the global token values set above
		if (isset($settings['container_padding'])) {
			$inline_styles[] = '--anchorkit-toc-padding: ' . intval($settings['container_padding']) . 'px';
		}
		if (isset($settings['container_border_width'])) {
			$inline_styles[] = '--anchorkit-toc-border-width: ' . intval($settings['container_border_width']) . 'px';
		}
		if (isset($settings['container_border_radius'])) {
			$inline_styles[] = '--anchorkit-toc-border-radius: ' . intval($settings['container_border_radius']) . 'px';
		}
		// Apply instance-specific box-shadow values via CSS variables
		// The frontend CSS handles switching these based on theme using media queries.
		// CRITICAL: Only apply shadow CSS variables when design_override is enabled.
		// When design_override is false, the block uses preset styling which handles shadows via CSS.
		// Applying shadow vars without design_override causes preset blocks to get wrong shadows.
		if (!empty($settings['design_override'])) {
			// Fallback: If design override is active but no explicit shadows set, apply default shadows.
			if (empty($box_shadow_light_value)) {
				$box_shadow_light_value = '0px 4px 6px 0px rgba(0, 0, 0, 0.1)';
			}
			if (empty($box_shadow_dark_value)) {
				$box_shadow_dark_value = '0px 2px 8px 0px rgba(0, 0, 0, 0.3)';
			}

			if ($box_shadow_light_value !== '') {
				$inline_styles[] = '--anchorkit-toc-shadow-light: ' . $box_shadow_light_value;
			}
			if ($box_shadow_dark_value !== '') {
				$inline_styles[] = '--anchorkit-toc-shadow-dark: ' . $box_shadow_dark_value;
			}
		}
		if (isset($settings['line_height']) && $settings['line_height'] !== '') {
			$inline_styles[] = '--anchorkit-toc-line-height: ' . floatval($settings['line_height']);
		}
		if (isset($settings['letter_spacing']) && $settings['letter_spacing'] !== '') {
			$inline_styles[] = '--anchorkit-toc-letter-spacing: ' . floatval($settings['letter_spacing']) . 'px';
		}
		if (isset($settings['text_transform']) && $settings['text_transform'] !== '') {
			$allowed_transforms = array('none', 'uppercase', 'lowercase', 'capitalize');
			$transform = in_array($settings['text_transform'], $allowed_transforms, true) ? $settings['text_transform'] : 'none';
			$inline_styles[] = '--anchorkit-toc-text-transform: ' . esc_attr($transform);
		}
		if (isset($settings['link_underline']) && $settings['link_underline'] !== '') {
			$allowed_underlines = array('none', 'always', 'hover', 'hover_none');
			$underline = in_array($settings['link_underline'], $allowed_underlines, true) ? $settings['link_underline'] : 'none';
			$underline_value = ($underline === 'always' || $underline === 'hover_none') ? 'underline' : 'none';
			$underline_hover = ($underline === 'hover') ? 'underline' : (($underline === 'always') ? 'underline' : 'none');
			$inline_styles[] = '--anchorkit-toc-link-decoration: ' . esc_attr($underline_value);
			$inline_styles[] = '--anchorkit-toc-link-decoration-hover: ' . esc_attr($underline_hover);
		}
		// Apply width and positioning with max-width/min-width overrides
		$is_sticky_sidebar = $sticky && ($sticky_position === 'left' || $sticky_position === 'right');

		if (!$is_sticky_sidebar) {
			// Normal positioning: apply width percentage
			if ($effective_toc_width !== null) {
				$inline_styles[] = 'width: ' . $effective_toc_width . '%';

				// If width is 100%, set a reasonable max-width to prevents it from spanning the full page width in wide containers
				// unless explicitly overridden by a max-width setting (future proofing) or if the user wants full width.
				// However, 'none' allows it to grow indefinitely. We'll change this to respect a new setting or default.

				// For now, if width is 100%, we remove the !important override on max-width: none
				// OR we can set it to 100% to fill container but respect parent bounds better?
				// Actually, the user asked for a "reasonable max width".
				// Let's check if 'toc_max_width' is passed in settings, otherwise default to a reasonable value if width is 100%.

				// Handle Max Width Logic
				// Elementor returns arrays for slider controls with units: ['size' => 50, 'unit' => 'px']
				$max_width_setting = isset($settings['toc_max_width']) ? $settings['toc_max_width'] : array();
				$effective_max_width = '';

				if (is_array($max_width_setting) && isset($max_width_setting['size']) && $max_width_setting['size'] !== '') {
					$effective_max_width = $max_width_setting['size'] . (isset($max_width_setting['unit']) ? $max_width_setting['unit'] : 'px');
				} elseif (is_numeric($max_width_setting)) {
					$effective_max_width = $max_width_setting . 'px'; // assume px if just number
				} elseif (is_string($max_width_setting) && !empty($max_width_setting)) {
					$effective_max_width = $max_width_setting;
				}

				if (!empty($effective_max_width)) {
					$inline_styles[] = 'max-width: ' . esc_attr($effective_max_width);
				} else {
					// If no explicit max-width set, and width is 100%, we should enforce a reasonable max-width
					// unless the user intentionally wants full width (which they can achieve by setting max-width to 100% or similar in custom CSS,
					// or we could add a "None" option to the control later).
					// For now, if width IS 100%, we default to '100%' max-width to align with container,
					// BUT we remove the old 'max-width: none !important' which caused unlimited growth.
					// Setting max-width: 100% ensures it doesn't overflow container.
					$inline_styles[] = 'max-width: 100%';
				}

				// Prevent TOC from becoming too small
				$inline_styles[] = 'min-width: 250px';
			}
		}

		// Apply float and alignment (only for non-sticky or sticky-content positions)
		if (!$is_sticky_sidebar) {
			if ($float_preference !== 'none') {
				$inline_styles[] = 'float: ' . esc_attr($float_preference);
				$inline_styles[] = 'margin: ' . ($float_preference === 'left' ? '0 20px 20px 0' : '0 0 20px 20px');
			} elseif ($alignment_preference === 'left') {
				$inline_styles[] = 'margin: 20px auto 20px 0';
			} elseif ($alignment_preference === 'right') {
				$inline_styles[] = 'margin: 20px 0 20px auto';
			} else {
				$inline_styles[] = 'margin: 20px auto';
			}
		}
		$style_attr = !empty($inline_styles) ? ' style="' . implode('; ', $inline_styles) . ';"' : '';

		// Build data attributes for JS settings (per-instance)
		$data_attrs = array(
			'data-collapsible="' . ($collapsible ? '1' : '0') . '"',
			'data-initial-state="' . esc_attr($initial_state) . '"',
			'data-hierarchical="' . ($hierarchical ? '1' : '0') . '"',
			'data-smooth-scroll="' . ($smooth_scroll ? '1' : '0') . '"',
			'data-scroll-offset="' . esc_attr($scroll_offset) . '"',
			'data-scroll-spy="' . ($scroll_spy ? '1' : '0') . '"',
			'data-scroll-easing="' . esc_attr($scroll_easing) . '"',
			'data-scroll-duration="' . esc_attr((int) $scroll_duration) . '"',
			'data-sticky="' . ($sticky ? '1' : '0') . '"',
			'data-sticky-position="' . esc_attr($sticky_position) . '"',
			'data-sticky-offset="' . esc_attr($sticky_offset) . '"',
			'data-alignment="' . esc_attr($alignment_preference) . '"',
			'data-entrance-animation="' . ($entrance_animation ? '1' : '0') . '"',
			'data-animation-type="' . esc_attr($animation_type) . '"',
			'data-exclude-selectors="' . esc_attr($exclude_selectors) . '"',
			'data-exclude-keywords="' . esc_attr($exclude_keywords) . '"',
			'data-min-heading-depth="' . esc_attr($min_heading_depth) . '"',
			'data-max-heading-depth="' . esc_attr($max_heading_depth) . '"',
			'data-show-reading-time="' . ($show_reading_time ? '1' : '0') . '"',
			'data-reading-speed="' . esc_attr($reading_speed) . '"',
			'data-hide-on-mobile="' . ($hide_on_mobile ? '1' : '0') . '"',
			'data-mobile-breakpoint="' . esc_attr($mobile_breakpoint) . '"',
		);

		// Add data-anchorkit-instance for Elementor widgets and Gutenberg blocks
		// ALL Elementor/Gutenberg TOCs get this attribute to isolate them from global styling.
		// This ensures blocks with presets don't inherit global custom styling colors.
		$data_attrs[] = 'data-anchorkit-instance="' . esc_attr($toc_instance_id) . '"';

		if ($effective_toc_width !== null) {
			$data_attrs[] = 'data-toc-width="' . esc_attr($effective_toc_width) . '"';
		}

		/**
		 * COLOR SYSTEM ARCHITECTURE - Instance-Specific TOC Styling
		 * ==========================================================
		 *
		 * Elementor/Gutenberg widgets use INSTANCE-SCOPED styling to isolate them
		 * from global settings and auto-insert TOCs.
		 *
		 * ARCHITECTURE:
		 *   - Each widget gets unique data-anchorkit-instance="<id>" attribute
		 *   - Colors applied via scoped style blocks: [data-anchorkit-instance="<id>"]
		 *   - CSS uses !important to override global rules
		 *   - Supports separate light/dark modes via media queries
		 *
		 * COLOR APPLICATION FLOW:
		 *   1. Extract override_tokens_light and override_tokens_dark from settings
		 *   2. Build CSS rules scoped to this instance's ID
		 *   3. For system theme: apply light as default + in @media (light), dark in @media (dark)
		 *   4. For fixed theme: apply only that theme's colors directly
		 *   5. Inject as inline CSS BEFORE the navigation element
		 *
		 * ISOLATION FROM GLOBAL:
		 *   - Global CSS uses :not(.anchorkit-toc-instance-override) to exclude instances
		 *   - Auto-insert JavaScript checks hasAttribute("data-anchorkit-instance") and skips
		 *   - Instance styles use higher specificity ([data-attr]) + !important
		 *
		 * FALLBACK BEHAVIOR:
		 *   - If no custom colors: uses anchorkit_collect_toc_tokens(true) for base tokens
		 *   - If only light OR dark defined: uses that for both modes (user flexibility)
		 *
		 * See also: includes/features/toc/render.php lines 970-999 for auto-insert color JS
		 */

		// Color token names that need light/dark variants
		$color_token_names = array(
			'--anchorkit-toc-bg',
			'--anchorkit-toc-text-color',
			'--anchorkit-toc-link-color',
			'--anchorkit-toc-link-hover-color',
			'--anchorkit-toc-active-link-color',
			'--anchorkit-toc-border-color',
			'--anchorkit-toc-bullet-color',
		);


		// Helper to add color tokens as inline styles with -light/-dark suffix
		$add_color_tokens_to_inline_styles = function ($tokens, $suffix) use (&$inline_styles, $color_token_names) {
			if (empty($tokens) || !is_array($tokens)) {
				return;
			}
			foreach ($tokens as $var => $value) {
				if ($value === '' || $value === null) {
					continue;
				}
				// Only process color-related tokens
				if (in_array($var, $color_token_names, true)) {
					$inline_styles[] = $var . $suffix . ': ' . esc_attr($value);
				}
			}
		};

		if ($design_override) {
			// User has custom colors - apply their light/dark overrides as inline CSS variables
			if (!empty($settings['override_tokens_light'])) {
				$add_color_tokens_to_inline_styles($settings['override_tokens_light'], '-light');
			}
			if (!empty($settings['override_tokens_dark'])) {
				$add_color_tokens_to_inline_styles($settings['override_tokens_dark'], '-dark');
			}

			// Fallback: if only override_tokens exists (legacy), apply it as both light and dark
			if (empty($settings['override_tokens_light']) && empty($settings['override_tokens_dark']) && !empty($settings['override_tokens'])) {
				$add_color_tokens_to_inline_styles($settings['override_tokens'], '-light');
				$add_color_tokens_to_inline_styles($settings['override_tokens'], '-dark');
			}

			// CRITICAL: For presets with custom colors (when $use_custom_styling is false),
			// also set BASE color variables inline based on current theme, BUT only if theme is not 'system'.
			// For system theme, the CSS mapping rules at the end of anchorkit-frontend.css
			// handle switching between -light and -dark tokens.
			if (!$use_custom_styling && $theme !== 'system') {
				// Determine which color set to use based on theme
				$active_tokens = ($theme === 'dark' && !empty($settings['override_tokens_dark']))
					? $settings['override_tokens_dark']
					: (!empty($settings['override_tokens_light']) ? $settings['override_tokens_light'] : array());

				// Set base color variables (without suffix) for preset CSS to use
				foreach ($active_tokens as $var => $value) {
					if ($value === '' || $value === null) {
						continue;
					}
					if (in_array($var, $color_token_names, true)) {
						$inline_styles[] = $var . ': ' . esc_attr($value);
					}
				}
			}
		}
		// When design_override is false, do NOT inherit global tokens.
		// Blocks/widgets with custom styling OFF should use their preset's CSS rules, not global custom styling.

		if ($hide_on_mobile) {
			$responsive_breakpoint = isset($mobile_breakpoint) && is_numeric($mobile_breakpoint) ? (int) $mobile_breakpoint : 782;
			if ($responsive_breakpoint <= 0) {
				$responsive_breakpoint = 782;
			}
			$instance_selector = '[data-anchorkit-instance="' . esc_attr($toc_instance_id) . '"]';
			$instance_style_rules .= '@media (max-width: ' . $responsive_breakpoint . 'px){' . $instance_selector . '{display:none !important;}}';
		}

		// CRITICAL: Elementor preview-specific fixes
		// wp_add_inline_style() doesn't work in Elementor preview iframe, so we need to
		// use inline styles on the element itself for preview-only fixes.
		if ($context === 'elementor' && $is_elementor_preview) {
			if (!$use_custom_styling) {
				// When using presets, ensure preset padding and font-size are visible
				$preset_padding_map = array(
					'minimal' => '12px 16px',
					'classic' => '20px',
					'modern' => '24px',
				);
				$preset_padding = isset($preset_padding_map[$style_preset]) ? $preset_padding_map[$style_preset] : '16px';
				$inline_styles[] = 'padding: ' . $preset_padding;
				// Reset font-size to prevent Elementor inheritance issues
				$inline_styles[] = 'font-size: 14px';
			} else {
				// When using custom-styling class, ensure CSS variables have inline defaults
				// This prevents Elementor's preview iframe from applying incorrect inherited values

				// Helper to check if a CSS variable is already set in inline_styles
				$has_css_var = function ($var_name) use (&$inline_styles) {
					foreach ($inline_styles as $style) {
						if (strpos($style, $var_name . ':') === 0) {
							return true;
						}
					}
					return false;
				};

				// Set defaults for STRUCTURAL CSS variables only (padding, font-size, border dimensions)
				// DO NOT set color variables here - they are handled by the color token logic above
				// which respects the theme setting (light/dark/system)
				$preview_defaults = array(
					'--anchorkit-toc-padding' => '20px',
					'--anchorkit-toc-font-size' => '14px',
					'--anchorkit-toc-border-width' => '1px',
					'--anchorkit-toc-border-radius' => '8px',
				);

				foreach ($preview_defaults as $var => $default_value) {
					if (!$has_css_var($var)) {
						$inline_styles[] = $var . ': ' . $default_value;
					}
				}
			}
		}

		// Apply typography variables to container style attribute
		// This ensures they work in Elementor preview via inline styles + CSS variables logic
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $advanced_typography_override) {
				if (!empty($title_font_size) && is_numeric($title_font_size)) {
					$inline_styles[] = '--anchorkit-toc-title-font-size: ' . intval($title_font_size) . 'px';
				}
				if (!empty($h2_font_size) && is_numeric($h2_font_size)) {
					$inline_styles[] = '--anchorkit-toc-h2-font-size: ' . intval($h2_font_size) . 'px';
				}
				if (!empty($h3_font_size) && is_numeric($h3_font_size)) {
					$inline_styles[] = '--anchorkit-toc-h3-font-size: ' . intval($h3_font_size) . 'px';
				}
				if (!empty($h4_font_size) && is_numeric($h4_font_size)) {
					$inline_styles[] = '--anchorkit-toc-h4-font-size: ' . intval($h4_font_size) . 'px';
				}
				if (!empty($h5_font_size) && is_numeric($h5_font_size)) {
					$inline_styles[] = '--anchorkit-toc-h5-font-size: ' . intval($h5_font_size) . 'px';
				}
				if (!empty($h6_font_size) && is_numeric($h6_font_size)) {
					$inline_styles[] = '--anchorkit-toc-h6-font-size: ' . intval($h6_font_size) . 'px';
				}
			}
		}
		// PRO: Back-to-top font size styling
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $back_to_top_link) {
				$inline_styles[] = '--anchorkit-toc-back-to-top-font-size: ' . intval($back_to_top_font_size) . 'px';
			}
		}

		// Ensure the container is responsive and list can scroll in Elementor output
		// Combine base responsive styles with width settings from $style_attr
		$combined_styles = 'box-sizing:border-box;';
		if (!empty($inline_styles)) {
			$combined_styles = implode('; ', $inline_styles) . '; box-sizing:border-box;';
		}

		$before_hook_output = '';
		ob_start();
		do_action('anchorkit_toc_before', $toc_context);
		$before_hook_output = ob_get_clean();

		// Start building output
		$output = '';
		if (!empty($before_hook_output)) {
			$output .= $before_hook_output;
		}

		// Note: Preview contexts may not show these styles, but frontend will work correctly
		if ($instance_style_rules !== '') {
			wp_add_inline_style('anchorkit-toc-css', $instance_style_rules);
		}


		// Check if we should return parts for safe late escaping
		$return_parts = isset($settings['return_parts']) && $settings['return_parts'];

		$output = '';
		if (!empty($before_hook_output) && !$return_parts) {
			$output .= $before_hook_output;
		}

		// Prepare parts
		$header_html = '';

		// Title
		if ($show_title && $title) {
			$header_html .= '<div class="anchorkit-toc-title">';
			$header_html .= esc_html($title);

			if ($collapsible) {
				$expanded = $initial_state === 'expanded' ? 'true' : 'false';
				$header_html .= '<button class="anchorkit-toc-toggle-button" aria-expanded="' . esc_attr($expanded) . '" aria-controls="anchorkit-toc-list">';
				$header_html .= '<span class="anchorkit-toc-toggle-icon"></span>';
				$header_html .= '</button>';
			}

			$header_html .= '</div>';
		} elseif ($collapsible) {
			// Collapsible without title (just the button)
			$expanded = $initial_state === 'expanded' ? 'true' : 'false';
			$header_html .= '<button class="anchorkit-toc-toggle-button anchorkit-toc-toggle-no-title" aria-expanded="' . esc_attr($expanded) . '" aria-controls="anchorkit-toc-list">';
			$header_html .= '<span class="anchorkit-toc-toggle-icon"></span>';
			$header_html .= '</button>';
		}

		// TOC List
		$inner_html = '<ul class="anchorkit-toc-list" id="anchorkit-toc-list">';

		// Initialize numbering tracker
		$num_tracker = array(0, 0, 0, 0, 0, 0); // For H1-H6
		$base_level = null;

		// Determine base level from first heading
		if (!empty($headings) && $hierarchical) {
			$first_level = intval(str_replace('h', '', $headings[0]['level']));
			$base_level = $first_level;
		}
		$auto_trim_patterns = anchorkit_toc_get_auto_trim_patterns($show_numerals, $numbering_format, $numbering_sep);

		foreach ($headings as $index => $heading) {
			$level = max(1, min(6, intval(str_replace('h', '', $heading['level']))));
			$depth = 1;
			if ($hierarchical && $base_level !== null) {
				$depth = max(1, min(6, $level - $base_level + 1));
			}
			$item_classes = array(
				'anchorkit-toc-item',
				'anchorkit-toc-level-' . $depth,
				'anchorkit-toc-heading-level-' . $level,
			);

			// Add hidden class for items beyond initial count when View More is enabled
			// Add hidden class for items beyond initial count when View More is enabled
			if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
				if (anchorkit_fs()->can_use_premium_code() && $view_more_enabled && $index >= $initial_count) {
					$item_classes[] = 'anchorkit-toc-hidden-item';
				}
			}

			// Generate number prefix if numerals are enabled
			$prefix = '';
			if ($show_numerals && $level <= 6) {
				$prefix = anchorkit_toc_build_number_prefix(
					$num_tracker,
					$level,
					$base_level,
					$hierarchical,
					$numbering_style,
					$numbering_format,
					$numbering_sep
				);
			}

			$inner_html .= '<li class="' . esc_attr(implode(' ', $item_classes)) . '" data-toc-index="' . esc_attr($index) . '">';

			$link_attributes = array(
				'class' => 'anchorkit-toc-link',
				'href' => '#' . ltrim((string) ($heading['id'] ?? ''), '#'),
				'aria-level' => (string) $depth,
				'aria-setsize' => (string) count($headings),
				'aria-posinset' => (string) ($index + 1),
			);

			$link_attributes = apply_filters('anchorkit_toc_link_attributes', $link_attributes, $heading, $toc_context);

			$inner_html .= '<a';
			foreach ($link_attributes as $attr => $value) {
				if ($value === null || $value === false || $attr === '') {
					continue;
				}
				if ($value === true) {
					$inner_html .= ' ' . esc_attr($attr);
					continue;
				}
				// Security: Use esc_url for URL attributes, esc_attr for everything else.
				$escaped_value = ($attr === 'href' || $attr === 'src') ? esc_url($value) : esc_attr($value);
				$inner_html .= ' ' . esc_attr($attr) . '="' . $escaped_value . '"';
			}
			$inner_html .= '>';

			// Add number prefix if enabled
			if ($prefix) {
				$inner_html .= '<span class="anchorkit-toc-item-number" aria-hidden="true">' . esc_html($prefix) . '</span>';
			}

			$display_text = $heading['text'] ?? '';
			// PRO: Trim heading prefixes
			$display_text = anchorkit_toc_trim_heading_prefixes(
				$display_text,
				$exclude_regex,
				$auto_trim_patterns,
				(anchorkit_fs() && anchorkit_fs()->is__premium_only() && anchorkit_fs()->can_use_premium_code())
			);

			if (!empty($custom_labels) && !empty($heading['text'])) {
				$trimmed_heading = trim($heading['text']);
				$normalized_heading = html_entity_decode($trimmed_heading, ENT_QUOTES | ENT_HTML5, 'UTF-8');
				$normalized_heading = str_replace(
					array('&#039;', '&apos;', "\u{2018}", "\u{2019}", '&#8217;', "\u{201C}", "\u{201D}", '&ldquo;', '&rdquo;'),
					array("'", "'", "'", "'", "'", '"', '"', '"', '"'),
					$normalized_heading
				);
				$normalized_heading = trim($normalized_heading);

				$matched = false;
				foreach ($custom_labels as $key => $value) {
					$normalized_key = html_entity_decode($key, ENT_QUOTES | ENT_HTML5, 'UTF-8');
					$normalized_key = str_replace(
						array('&#039;', '&apos;', "\u{2018}", "\u{2019}", '&#8217;', "\u{201C}", "\u{201D}", '&ldquo;', '&rdquo;'),
						array("'", "'", "'", "'", "'", '"', '"', '"', '"'),
						$normalized_key
					);
					$normalized_key = trim($normalized_key);

					if (strtolower($normalized_heading) === strtolower($normalized_key)) {
						$display_text = $value;
						$matched = true;
						break;
					}
				}
			}

			$inner_html .= '<span class="anchorkit-toc-item-text">' . esc_html($display_text) . '</span>';

			// PRO: Append reading time/word count metadata if enabled
			if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
				if (anchorkit_fs()->can_use_premium_code() && !empty($heading['word_count']) && ($show_reading_time || $show_word_count)) {
					$metadata_parts = array();

					if ($show_reading_time) {
						$minutes = max(1, (int) round(((int) $heading['word_count']) / max(1, (int) $reading_speed)));
						switch ($time_format) {
							case 'minutes':
								$metadata_parts[] = sprintf('~%d minute%s', $minutes, $minutes > 1 ? 's' : '');
								break;
							case 'short':
								$metadata_parts[] = $minutes . 'm';
								break;
							case 'min_read':
							default:
								$metadata_parts[] = sprintf('%d min read', $minutes);
								break;
						}
					}

					if ($show_word_count) {
						$metadata_parts[] = number_format((int) $heading['word_count']) . ' ' . __('words', 'anchorkit-table-of-contents');
					}

					if (!empty($metadata_parts)) {
						$inner_html .= '<span class="anchorkit-toc-metadata">(' . esc_html(implode(' • ', $metadata_parts)) . ')</span>';
					}
				}
			}
			$inner_html .= '</a>';
			$inner_html .= '</li>';
		}

		$inner_html .= '</ul>';


		// Prepare footer parts (View More, Back to Top)
		$footer_html = '';

		// View More button (Pro) - moved outside scrollable list

		// PRO: View More button
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $view_more_enabled && count($headings) > $initial_count) {
				$footer_html .= '<div class="anchorkit-toc-view-more-item">';
				$footer_html .= '<button class="anchorkit-toc-view-more-btn" ';
				$footer_html .= 'data-initial-count="' . esc_attr($initial_count) . '" ';
				$footer_html .= 'data-view-more-text="' . esc_attr($view_more_text) . '" ';
				$footer_html .= 'data-view-less-text="' . esc_attr($view_less_text) . '" ';
				$footer_html .= 'aria-expanded="false" ';
				$footer_html .= 'type="button">';
				$footer_html .= '<span class="anchorkit-toc-view-more-icon"></span>';
				$footer_html .= '<span class="anchorkit-toc-view-more-text">' . esc_html($view_more_text) . '</span>';
				$footer_html .= '</button>';
				$footer_html .= '</div>';
			}
		}

		// PRO: Back-to-top link - Only shown when TOC is sticky
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $sticky && $back_to_top_link) {
				// Prepend arrow if not already present in the text
				$back_to_top_display_text = (strpos($back_to_top_text, '↑') === false) ? '↑ ' . $back_to_top_text : $back_to_top_text;
				$footer_html .= '<div class="anchorkit-toc-back-to-top" style="--anchorkit-toc-back-to-top-font-size: ' . esc_attr($back_to_top_font_size) . 'px;"><a href="#" aria-label="' . esc_attr__('Scroll to the top of the page', 'anchorkit-table-of-contents') . '">' . esc_html($back_to_top_display_text) . '</a></div>';
			}
		}

		if (!$return_parts) {
			$output .= '<nav id="' . esc_attr($toc_instance_id) . '" class="' . esc_attr(implode(' ', $classes)) . '" style="' . esc_attr($combined_styles) . '" ' . implode(' ', $data_attrs) . ' aria-label="' . esc_attr($aria_label) . '">';
			$output .= $header_html;
			$output .= $inner_html;
			$output .= $footer_html;
			$output .= '</nav>';
		}
		// PRO: Generate Schema.org markup if enabled
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $schema_enabled && !empty($headings)) {
				$schema = anchorkit_generate_elementor_schema_markup($headings, $schema_type);
				if (!empty($schema) && is_array($schema)) {
					anchorkit_queue_elementor_schema_markup($schema);
				}
			}
		}

		// Localize settings for JavaScript (ensure handle matches enqueued script)
		wp_localize_script(
			'anchorkit-toc-js',
			'anchorkitTocSettings',
			array(
				'collapsible' => $collapsible,
				'initial_state' => $initial_state,
				'hierarchical' => $hierarchical,
				'smooth_scroll' => $smooth_scroll,
				'scroll_offset' => $scroll_offset,
				'scroll_spy' => $scroll_spy,
				'sticky' => $sticky,
				'sticky_position' => $sticky_position,
				'sticky_offset' => $sticky_offset,
				'entrance_animation' => $entrance_animation,
				'animation_type' => $animation_type,
			)
		);

		// Add notification for Elementor preview if sticky is enabled
		if ($is_elementor_preview && $sticky) {
			$notice = '<div class="anchorkit-toc-sticky-notification" style="background: #f0f0f1; border-left: 4px solid #72aee6; padding: 12px; margin-top: 10px; font-size: 13px; color: #3c434a;">';
			$notice .= '<strong>' . esc_html__('Note:', 'anchorkit-table-of-contents') . '</strong> ' . esc_html__('Sticky positioning may not be fully visible in the Elementor preview. Please preview the page to see the sticky effect.', 'anchorkit-table-of-contents');
			$notice .= '</div>';

			if ($return_parts) {
				$inner_html .= $notice;
			} else {
				$output .= $notice;
			}
		}

		ob_start();
		do_action('anchorkit_toc_after', $toc_context);
		$after_hook_output = ob_get_clean();

		if ($return_parts) {
			// Reset recursion guard before returning
			$is_rendering = false;

			return array(
				'container_tag' => 'nav',
				'container_attributes' => array_merge(
					array('id' => $toc_instance_id, 'class' => implode(' ', $classes), 'style' => $combined_styles, 'aria-label' => $aria_label),
					// Map numeric keys from data_attrs strings is hard. 
					// Ideally $data_attrs should be an assoc array.
					// Reviewing the code, $data_attrs is array('data-foo="bar"', ...).
					// We'll pass it as 'extra_attributes_string' to simple concatenation.
					array()
				),
				'extra_attributes_string' => implode(' ', $data_attrs),
				'header_html' => $header_html,
				'inner_html' => $inner_html . $after_hook_output,
				'footer_html' => $footer_html,
				'before_html' => $before_hook_output,
			);
		}

		if (!empty($after_hook_output)) {
			$output .= $after_hook_output;
		}

		// Reset recursion guard before returning
		$is_rendering = false;
		return apply_filters('anchorkit_toc_html', $output, $toc_context);
	}

	/**
	 * Extract headings from content for Elementor TOC
	 *
	 * @param string $content Post content
	 * @param array  $levels Heading levels to include (h1, h2, etc.)
	 * @param string $exclude_selectors CSS selectors to exclude
	 * @return array Array of heading data
	 */
	function anchorkit_extract_headings($content, $levels, $exclude_selectors)
	{
		$headings = array();
		$used_ids = array();

		// Parse exclude selectors into array
		$exclude_classes = array();
		$exclude_ids = array();
		if (!empty($exclude_selectors)) {
			$selectors = array_map('trim', explode(',', $exclude_selectors));
			foreach ($selectors as $selector) {
				if (strpos($selector, '.') === 0) {
					// Class selector
					$exclude_classes[] = substr($selector, 1);
				} elseif (strpos($selector, '#') === 0) {
					// ID selector
					$exclude_ids[] = substr($selector, 1);
				}
			}
		}

		// Build regex pattern for specified heading levels
		$level_pattern = implode('|', array_map('preg_quote', $levels));
		$pattern = '/<(' . $level_pattern . ')([^>]*)>(.*?)<\/\1>/is';

		if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
			foreach ($matches as $index => $match) {
				$level = strtolower($match[1]);
				$attributes = $match[2];
				$text = wp_strip_all_tags($match[3]);

				// Skip if empty
				if (empty(trim($text))) {
					continue;
				}

				// Check if heading should be excluded based on class or id
				$should_exclude = false;

				// Check for class exclusions
				if (!empty($exclude_classes)) {
					// Extract all classes from the heading (robust to newlines and spacing)
					if (preg_match('/class\s*=\s*(["\'])(.*?)\1/si', $attributes, $class_match)) {
						$class_value = trim($class_match[2]);
						// Normalize whitespace and split
						$heading_classes = preg_split('/\s+/', $class_value);
						$heading_classes = array_filter($heading_classes);

						// Normalize both sides for reliable compare
						$normalized_heading_classes = array_map('strtolower', $heading_classes);
						foreach ($exclude_classes as $exclude_class) {
							$needle = strtolower($exclude_class);
							if (in_array($needle, $normalized_heading_classes, true)) {
								$should_exclude = true;
								break;
							}
						}
					}
				}

				// Check for id exclusions
				if (!$should_exclude && !empty($exclude_ids)) {
					// Extract ID from the heading (robust to newlines and spacing)
					if (preg_match('/id\s*=\s*(["\'])(.*?)\1/si', $attributes, $id_match)) {
						$heading_id = trim($id_match[2]);
						if (in_array($heading_id, $exclude_ids, true)) {
							$should_exclude = true;
						}
					}
				}

				if ($should_exclude) {
					continue;
				}

				// Determine heading ID
				// Prefer existing id attribute when present to match real DOM
				$id = '';
				if (preg_match('/id=["\']([^"\']+)["\']/i', $attributes, $id_match_attr)) {
					$id = trim($id_match_attr[1]);
				}
				if ($id === '') {
					// Generate ID from text if no explicit id is present
					$id = sanitize_title($text);
					// Ensure uniqueness ONLY for generated IDs so we don't mismatch DOM ids
					$base_id = $id;
					$counter = 1;
					while (isset($used_ids[$id])) {
						$id = $base_id . '-' . $counter;
						++$counter;
					}
					$used_ids[$id] = true;
				}

				$headings[] = array(
					'level' => $level,
					'text' => $text,
					'id' => $id,
					'anchor' => $id,
				);
			}
		}

		return $headings;
	}
}


if (!function_exists('anchorkit_add_heading_ids_for_elementor')) {
	/**
	 * Add IDs to headings in content for Elementor widget
	 * This ensures TOC links work properly by adding anchor IDs to headings
	 *
	 * @param string $content Post content
	 * @return string Modified content with heading IDs
	 */
	function anchorkit_add_heading_ids_for_elementor($content)
	{
		// Only run if we're viewing a page (not in admin, feeds, etc.)
		if (!is_singular() || is_admin() || is_feed()) {
			return $content;
		}

		// This function works for both Elementor widgets AND Gutenberg blocks
		// No need to check for Elementor specifically - we want to inject IDs for any widget/block usage

		// Prevent redundant runs for the same anchor settings, but allow reprocessing if format/levels change.
		static $processed_signatures = array();

		// Determine which heading levels to process so numbering matches the widget
		$heading_levels = array();
		$runtime_anchor_settings = anchorkit_get_runtime_anchor_settings();
		if ($runtime_anchor_settings && !empty($runtime_anchor_settings['heading_levels'])) {
			$heading_levels = (array) $runtime_anchor_settings['heading_levels'];
		}
		if (empty($heading_levels)) {
			$filtered_levels = apply_filters('anchorkit_inject_heading_levels', null);
			if (is_array($filtered_levels) && !empty($filtered_levels)) {
				$heading_levels = $filtered_levels;
			}
		}
		if (empty($heading_levels)) {
			$heading_levels = anchorkit_get_global_heading_levels();
		}
		$heading_levels = array_values(
			array_unique(
				array_filter(
					array_map(
						function ($level) {
							$level = strtolower(trim((string) $level));
							return preg_match('/^h[1-6]$/', $level) ? $level : null;
						},
						(array) $heading_levels
					)
				)
			)
		);
		if (empty($heading_levels)) {
			$heading_levels = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
		}

		// Determine anchor format
		$anchor_format = 'auto';
		if ($runtime_anchor_settings && isset($runtime_anchor_settings['anchor_format'])) {
			$anchor_format = sanitize_key($runtime_anchor_settings['anchor_format']);
		} else {
			$anchor_format = anchorkit_get_option('anchorkit_toc_anchor_format', 'auto');
		}
		if (!in_array($anchor_format, array('auto', 'sequential', 'prefixed'), true)) {
			$anchor_format = 'auto';
		}

		// Determine anchor format once so we can include it in the processed key
		$anchor_format = 'auto';
		if ($runtime_anchor_settings && isset($runtime_anchor_settings['anchor_format'])) {
			$anchor_format = sanitize_key($runtime_anchor_settings['anchor_format']);
		} else {
			$anchor_format = anchorkit_get_option('anchorkit_toc_anchor_format', 'auto');
		}
		if (!in_array($anchor_format, array('auto', 'sequential', 'prefixed'), true)) {
			$anchor_format = 'auto';
		}

		// Build a key that reflects current runtime format + levels. If unchanged, skip processing.
		// Track processed combinations of anchor settings + specific content so excerpts/previews don't block full renders.
		$content_string = is_string($content) ? $content : strval($content);
		$content_hash = $content_string !== '' ? md5($content_string) : 'empty';
		$current_key = $anchor_format . '|' . implode(',', $heading_levels) . '|' . $content_hash;
		if (isset($processed_signatures[$current_key])) {
			return $content;
		}
		$processed_signatures[$current_key] = true;

		// Pattern to match only the configured heading levels
		$level_pattern = implode('|', array_map('preg_quote', $heading_levels));
		$pattern = '/<(' . $level_pattern . ')([^>]*)>(.*?)<\/\1>/is';

		$used_ids = array();
		$injected_count = 0;
		$anchor_counter = 0;

		$content = preg_replace_callback(
			$pattern,
			function ($matches) use (&$used_ids, &$injected_count, &$anchor_counter, $anchor_format) {
				$tag = $matches[1]; // h2, h3, etc.
				$attrs = $matches[2]; // existing attributes
				$text = $matches[3]; // heading text
	
				// Check if heading already has an ID
				$has_existing_id = preg_match('/id=["\']([^"\']+)["\']/i', $attrs);

				// IMPORTANT: For sequential and prefixed formats, always regenerate IDs to ensure consistency
				// Only preserve existing IDs when using auto format
				$should_regenerate_id = ($anchor_format !== 'auto') || !$has_existing_id;

				if (!$should_regenerate_id) {
					// Keep existing ID (auto format only)
					return $matches[0];
				}

				// Generate ID from text using the same logic as the core TOC parser
				$clean_text = wp_strip_all_tags($text);

				$id = function_exists('anchorkit_toc_generate_anchor')
					? anchorkit_toc_generate_anchor($clean_text)
					: sanitize_title($clean_text);

				// Handle sequential format
				if ($anchor_format === 'sequential') {
					$anchor_counter++;
					$id = $id . '-' . $anchor_counter;
				}

				// Skip if empty
				if (empty($id)) {
					return $matches[0];
				}

				// Ensure unique IDs (but not for sequential format which already has counter)
				if ($anchor_format !== 'sequential') {
					$base_id = $id;
					$counter = 1;
					while (isset($used_ids[$id])) {
						$id = $base_id . '-' . $counter;
						$counter++;
					}
				}
				$used_ids[$id] = true;
				$injected_count++;

				// Remove existing ID if present, then add new one
				if ($has_existing_id) {
					$attrs = preg_replace('/\s*id=["\']([^"\']+)["\']/i', '', $attrs);
				}

				// Add ID to heading
				return '<' . $tag . $attrs . ' id="' . esc_attr($id) . '">' . $text . '</' . $tag . '>';
			},
			$content
		);

		return $content;
	}
}


// Ensure heading IDs are injected for Elementor-rendered content as well
if (!has_filter('elementor/frontend/the_content', 'anchorkit_add_heading_ids_for_elementor')) {
	add_filter('elementor/frontend/the_content', 'anchorkit_add_heading_ids_for_elementor', 5);
}
