<?php
/**
 * MenuX — Mega Menu System
 *
 * Renders mega menu column panels for first-level nav items.
 * Column data is stored per-item inside menux_menu_items.
 *
 * Supported column item types:
 *   link      — clickable link with icon + optional description
 *   heading   — non-clickable section label
 *   divider   — horizontal rule separator
 *   image     — image block (WP attachment or external URL)
 *   shortcode — arbitrary shortcode / Gutenberg reusable block
 *   dynamic   — auto-populated list of links from a content source
 *               (recent posts or WooCommerce product categories)
 *
 * @package GiuliomaxMenuBuilder
 */

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

class Menux_MegaMenu {

	/**
	 * Raw shortcode/widget output stashed behind plain-text placeholder tokens.
	 * The caller passes each menu item through wp_kses_post() for security, which
	 * would otherwise strip legitimate shortcode output (forms, iframes, inputs).
	 * We swap that output for an opaque token before kses runs, then restore it
	 * afterwards via restore_widgets() — preserving the "trusted admin content is
	 * not filtered" intent without exposing the rest of the markup.
	 *
	 * @var array<string,string>
	 */
	private static $widget_stash = array();

	/** Stashes raw widget HTML and returns a kses-safe placeholder token. */
	private static function stash_widget( $html ) {
		$token = 'MENUXWGT' . wp_generate_password( 24, false ) . 'ENDWGT';
		self::$widget_stash[ $token ] = $html;
		return $token;
	}

	/**
	 * Replaces stashed tokens in a (kses-filtered) string with their raw HTML.
	 * Each token is consumed once. Call after wp_kses_post() on the item markup.
	 */
	public static function restore_widgets( $html ) {
		if ( empty( self::$widget_stash ) || strpos( $html, 'MENUXWGT' ) === false ) {
			return $html;
		}
		foreach ( self::$widget_stash as $token => $raw ) {
			if ( strpos( $html, $token ) !== false ) {
				$html = str_replace( $token, $raw, $html );
				unset( self::$widget_stash[ $token ] );
			}
		}
		return $html;
	}

	// ─────────────────────────────────────────────────────────────────
	// Frontend rendering
	// ─────────────────────────────────────────────────────────────────

	/**
	 * Renders the full mega menu panel HTML for one first-level item.
	 *
	 * @param array $item  The raw menu item array (must have mega_columns key).
	 * @return string
	 */
	public static function render_panel( $item ) {
		$cols       = is_array( $item['mega_columns'] ?? null ) ? $item['mega_columns'] : array();
		$full_width = ( $item['mega_full_width'] ?? '1' ) === '1';

		if ( empty( $cols ) ) {
			return '';
		}

		$panel_class = 'menux-mega-panel';
		if ( $full_width ) {
			$panel_class .= ' menux-mega-full';
		}

		$item_key = $item['item_key'] ?? '';
		$out  = '<div class="' . esc_attr( $panel_class ) . '"'
		      . ( $item_key ? ' data-mega-key="' . esc_attr( $item_key ) . '"' : '' )
		      . ' role="menu" aria-label="' . esc_attr__( 'Submenu', 'giuliomax-menu-builder' ) . '">';
		$out .= '<div class="menux-mega-inner">';

		foreach ( $cols as $col ) {
			$col_items = is_array( $col['items'] ?? null ) ? $col['items'] : array();

			// Column width: explicit % or flex:1 for auto.
			$col_style = '';
			if ( ! empty( $col['width_pct'] ) && is_numeric( $col['width_pct'] ) ) {
				$pct       = min( 100, max( 5, (int) $col['width_pct'] ) );
				$col_style = 'flex:0 0 ' . $pct . '%;max-width:' . $pct . '%;';
			}

			// Column text alignment.
			$align = in_array( $col['align'] ?? 'left', array( 'left', 'center', 'right' ), true ) ? ( $col['align'] ?? 'left' ) : 'left';

			$col_attrs = '';
			if ( $col_style ) {
				$col_attrs .= ' style="' . esc_attr( $col_style ) . '"';
			}
			if ( 'left' !== $align ) {
				$col_attrs .= ' data-align="' . esc_attr( $align ) . '"';
			}

			$out .= '<div class="menux-mega-col"' . $col_attrs . '>';
			foreach ( $col_items as $mega_item ) {
				$out .= self::render_col_item( $mega_item );
			}
			$out .= '</div>';
		}

		$out .= '</div></div>';
		return $out;
	}

	/** Renders a single column item based on its type. */
	private static function render_col_item( $mega_item ) {
		$type = $mega_item['type'] ?? 'link';

		switch ( $type ) {

			case 'heading':
				if ( empty( $mega_item['label'] ) ) return '';
				$icon_h = ! empty( $mega_item['icon'] )
					? '<i class="' . esc_attr( $mega_item['icon'] ) . '" aria-hidden="true" style="margin-right:5px;"></i>' : '';
				$head_style = ! empty( $mega_item['heading_color'] )
					? ' style="color:' . esc_attr( $mega_item['heading_color'] ) . ';"' : '';
				return '<h3 class="menux-mega-heading"' . $head_style . '>' . $icon_h . esc_html( $mega_item['label'] ) . '</h3>';

			case 'divider':
				return '<hr class="menux-mega-divider" aria-hidden="true">';

			case 'image':
				return self::render_col_image( $mega_item );

			case 'shortcode':
				if ( empty( $mega_item['content'] ) ) return '';
				// do_shortcode output is trusted admin content and must not be
				// filtered. Stash it behind a token so the caller's wp_kses_post()
				// can't strip forms/iframes/inputs; restored via restore_widgets().
				return '<div class="menux-mega-widget">' . self::stash_widget( do_shortcode( $mega_item['content'] ) ) . '</div>';

			case 'dynamic':
				return self::render_dynamic( $mega_item );

			default: // 'link'
				return self::render_col_link( $mega_item );
		}
	}

	private static function render_col_link( $item ) {
		$url    = ! empty( $item['url'] ) ? esc_url( $item['url'] ) : '#';
		$label  = esc_html( $item['label'] ?? '' );
		$target = ( ! empty( $item['target'] ) && $item['target'] === '_blank' )
			? ' target="_blank" rel="noopener noreferrer"' : '';
		$icon_html = ! empty( $item['icon'] )
			? '<i class="' . esc_attr( $item['icon'] ) . '" aria-hidden="true"></i>' : '';
		$desc_style = ! empty( $item['desc_color'] )
			? ' style="color:' . esc_attr( $item['desc_color'] ) . ';"' : '';
		$desc_html  = ! empty( $item['desc'] )
			? '<small class="menux-mega-link-desc"' . $desc_style . '>' . esc_html( $item['desc'] ) . '</small>' : '';

		// Per-item link color (applies to the label; the description keeps its own color).
		$label_style = ! empty( $item['link_color'] )
			? ' style="color:' . esc_attr( $item['link_color'] ) . ';"' : '';

		return '<a href="' . $url . '" class="menux-mega-link" role="menuitem"' . $target . '>'
			. $icon_html
			. '<span class="menux-mega-link-inner">'
			.   '<span class="menux-mega-link-label"' . $label_style . '>' . $label . '</span>'
			.   $desc_html
			. '</span>'
			. '</a>';
	}

	private static function render_col_image( $item ) {
		// Optional explicit dimensions (integer percent). Empty = auto/native.
		$dim_style = '';
		if ( ! empty( $item['image_width'] ) ) {
			$dim_style .= 'width:' . (int) $item['image_width'] . '%;';
		}
		if ( ! empty( $item['image_height'] ) ) {
			$dim_style .= 'height:' . (int) $item['image_height'] . '%;';
		}
		$style_attr = $dim_style ? ' style="' . esc_attr( $dim_style ) . '"' : '';

		if ( ! empty( $item['image_id'] ) && (int) $item['image_id'] > 0 ) {
			$attrs = array( 'loading' => 'lazy' );
			if ( $dim_style ) {
				$attrs['style'] = $dim_style;
			}
			$img = wp_get_attachment_image( (int) $item['image_id'], 'medium', false, $attrs );
		} elseif ( ! empty( $item['image_url'] ) ) {
			$img = '<img src="' . esc_url( $item['image_url'] ) . '"'
				. ' alt="' . esc_attr( $item['label'] ?? '' ) . '"' . $style_attr . ' loading="lazy">';
		} else {
			return '';
		}

		if ( ! empty( $item['url'] ) ) {
			$target = ( ! empty( $item['target'] ) && $item['target'] === '_blank' )
				? ' target="_blank" rel="noopener noreferrer"' : '';
			return '<div class="menux-mega-image"><a href="' . esc_url( $item['url'] ) . '"' . $target . '>' . $img . '</a></div>';
		}

		return '<div class="menux-mega-image">' . $img . '</div>';
	}

	// ─────────────────────────────────────────────────────────────────
	// Dynamic content blocks (recent posts / WooCommerce categories)
	// ─────────────────────────────────────────────────────────────────

	/** Allowed dynamic sources and the orderby values each one accepts. */
	const DYN_ORDERBY = array(
		'posts'   => array( 'date', 'title', 'modified' ),
		'wc_cats' => array( 'name', 'count', 'menu_order' ),
	);

	/**
	 * Renders a dynamic block: a list of auto-generated links pulled from a
	 * content source. Output is cached in a transient keyed by the block
	 * config, the current locale and a generation counter that is bumped
	 * whenever relevant content changes (see register_hooks()).
	 *
	 * @param array $item  The column item (type === 'dynamic').
	 * @return string
	 */
	public static function render_dynamic( $item ) {
		$source = in_array( $item['source'] ?? '', array( 'posts', 'wc_cats' ), true )
			? $item['source'] : 'posts';
		$count  = isset( $item['dyn_count'] ) && is_numeric( $item['dyn_count'] )
			? min( 20, max( 1, (int) $item['dyn_count'] ) ) : 5;
		$allowed_ob = self::DYN_ORDERBY[ $source ];
		$orderby = in_array( $item['dyn_orderby'] ?? '', $allowed_ob, true )
			? $item['dyn_orderby'] : $allowed_ob[0];
		$icon      = sanitize_text_field( $item['icon'] ?? '' );
		$post_type = sanitize_key( $item['dyn_post_type'] ?? 'post' ) ?: 'post';

		// Cache key: config + locale + generation counter. Stale entries simply
		// expire; a content change bumps the counter so a fresh key is used.
		$cache_sig = wp_json_encode( array(
			$source, $count, $orderby, $post_type,
			$icon,
			$item['dyn_view_all'] ?? '',
			$item['dyn_view_all_label'] ?? '',
			determine_locale(),
			self::dynamic_gen(),
		) );
		$cache_key = 'menux_dyn_' . md5( (string) $cache_sig );

		$cached = get_transient( $cache_key );
		if ( false !== $cached ) {
			return $cached;
		}

		// Generated links are returned bare (no wrapper) so they are direct
		// children of .menux-mega-col — identical to hand-authored links, which
		// keeps the desktop hover negative-margin and mobile flat-list CSS intact.
		$html = ( 'wc_cats' === $source )
			? self::dynamic_wc_cats( $count, $orderby, $icon, $item )
			: self::dynamic_posts( $post_type, $count, $orderby, $icon, $item );

		set_transient( $cache_key, $html, 6 * HOUR_IN_SECONDS );
		return $html;
	}

	/** Builds the link list for the "recent posts" source. */
	private static function dynamic_posts( $post_type, $count, $orderby, $icon, $item ) {
		if ( ! post_type_exists( $post_type ) ) {
			$post_type = 'post';
		}
		$order = ( 'title' === $orderby ) ? 'ASC' : 'DESC';

		$q = new WP_Query( array(
			'post_type'           => $post_type,
			'posts_per_page'      => $count,
			'post_status'         => 'publish',
			'orderby'             => $orderby,
			'order'               => $order,
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
			'suppress_filters'    => false,
		) );

		$out = '';
		foreach ( $q->posts as $post ) {
			$out .= self::dynamic_link( get_permalink( $post ), get_the_title( $post ), $icon );
		}
		wp_reset_postdata();

		if ( '' === $out ) {
			return '';
		}

		// "View all" → the posts page if set, otherwise the post type archive.
		if ( ! empty( $item['dyn_view_all'] ) && '1' === $item['dyn_view_all'] ) {
			$all_url = '';
			if ( 'post' === $post_type ) {
				$blog_id = (int) get_option( 'page_for_posts' );
				$all_url = $blog_id ? get_permalink( $blog_id ) : home_url( '/' );
			} else {
				$all_url = get_post_type_archive_link( $post_type ) ?: home_url( '/' );
			}
			$out .= self::dynamic_view_all( $all_url, $item['dyn_view_all_label'] ?? '' );
		}

		return $out;
	}

	/** Builds the link list for the "WooCommerce product categories" source. */
	private static function dynamic_wc_cats( $count, $orderby, $icon, $item ) {
		// Silently no-op when WooCommerce (the product_cat taxonomy) is absent.
		if ( ! taxonomy_exists( 'product_cat' ) ) {
			return '';
		}
		$order = ( 'count' === $orderby ) ? 'DESC' : 'ASC';

		$terms = get_terms( array(
			'taxonomy'   => 'product_cat',
			'hide_empty' => true,   // empty categories are always excluded
			'number'     => $count,
			'orderby'    => $orderby,
			'order'      => $order,
		) );

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return '';
		}

		$out = '';
		foreach ( $terms as $term ) {
			$link = get_term_link( $term );
			if ( is_wp_error( $link ) ) {
				continue;
			}
			$out .= self::dynamic_link( $link, $term->name, $icon );
		}

		if ( '' === $out ) {
			return '';
		}

		// "View all" → the WooCommerce shop page, falling back to the product archive.
		if ( ! empty( $item['dyn_view_all'] ) && '1' === $item['dyn_view_all'] ) {
			$shop_id = function_exists( 'wc_get_page_id' ) ? (int) wc_get_page_id( 'shop' ) : 0;
			$all_url = ( $shop_id > 0 ) ? get_permalink( $shop_id ) : '';
			if ( ! $all_url ) {
				$all_url = get_post_type_archive_link( 'product' ) ?: home_url( '/' );
			}
			$out .= self::dynamic_view_all( $all_url, $item['dyn_view_all_label'] ?? '' );
		}

		return $out;
	}

	/** Renders one auto-generated link (label + fixed icon, no description). */
	private static function dynamic_link( $url, $label, $icon ) {
		$icon_html = '' !== $icon
			? '<i class="' . esc_attr( $icon ) . '" aria-hidden="true"></i>' : '';
		return '<a href="' . esc_url( $url ) . '" class="menux-mega-link" role="menuitem">'
			. $icon_html
			. '<span class="menux-mega-link-inner">'
			.   '<span class="menux-mega-link-label">' . esc_html( $label ) . '</span>'
			. '</span>'
			. '</a>';
	}

	/** Renders the optional "View all →" trailing link. */
	private static function dynamic_view_all( $url, $label ) {
		$label = sanitize_text_field( $label );
		if ( '' === $label ) {
			$label = __( 'View all →', 'giuliomax-menu-builder' );
		}
		return '<a href="' . esc_url( $url ) . '" class="menux-mega-link menux-mega-viewall" role="menuitem">'
			. '<span class="menux-mega-link-inner">'
			.   '<span class="menux-mega-link-label">' . esc_html( $label ) . '</span>'
			. '</span>'
			. '</a>';
	}

	// ─────────────────────────────────────────────────────────────────
	// Dynamic-block cache invalidation
	// ─────────────────────────────────────────────────────────────────

	/** Current generation counter; included in every dynamic cache key. */
	private static function dynamic_gen() {
		return (int) get_option( 'menux_dyn_gen', 1 );
	}

	/** Bumps the generation counter so all dynamic blocks re-query on next render. */
	public static function bump_dynamic_gen() {
		// Autoloaded: it is read on every render, so we avoid a per-page query.
		update_option( 'menux_dyn_gen', self::dynamic_gen() + 1, true );
	}

	/**
	 * save_post handler — bumps the cache generation only for real content
	 * changes, skipping the autosave/revision noise that would otherwise
	 * invalidate every dynamic block dozens of times per editing session.
	 *
	 * @param int     $post_id Saved post ID.
	 * @param WP_Post $post    Saved post object.
	 */
	public static function on_save_post( $post_id, $post = null ) {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}
		// Only public post types can ever appear in a dynamic block.
		if ( $post instanceof WP_Post ) {
			$pt = get_post_type_object( $post->post_type );
			if ( $pt && empty( $pt->public ) ) {
				return;
			}
		}
		self::bump_dynamic_gen();
	}

	/**
	 * Registers the hooks that invalidate dynamic-block caches when the
	 * underlying content (posts or product categories) changes.
	 */
	public static function register_hooks() {
		add_action( 'save_post', array( __CLASS__, 'on_save_post' ), 10, 2 );
		foreach ( array( 'deleted_post', 'trashed_post', 'untrashed_post' ) as $hook ) {
			add_action( $hook, array( __CLASS__, 'bump_dynamic_gen' ) );
		}
		foreach ( array( 'created_term', 'edited_term', 'delete_term' ) as $hook ) {
			add_action( $hook, array( __CLASS__, 'bump_dynamic_gen' ) );
		}
	}

	// ─────────────────────────────────────────────────────────────────
	// Save helper — sanitises raw POST column data
	// ─────────────────────────────────────────────────────────────────

	/**
	 * Sanitises and returns a clean mega_columns array from raw JSON input.
	 *
	 * @param string $json_raw  Raw JSON string from a hidden form input.
	 * @return array
	 */
	public static function sanitize_columns( $json_raw ) {
		if ( empty( $json_raw ) ) return array();

		$decoded = json_decode( stripslashes( (string) $json_raw ), true );
		if ( ! is_array( $decoded ) ) return array();

		$allowed_types = array( 'link', 'heading', 'divider', 'image', 'shortcode', 'dynamic' );
		$clean_cols    = array();

		foreach ( $decoded as $col ) {
			if ( ! is_array( $col ) ) continue;

			$clean_col = array(
				'width_pct' => is_numeric( $col['width_pct'] ?? '' ) ? (int) $col['width_pct'] : '',
				'align'     => in_array( $col['align'] ?? 'left', array( 'left', 'center', 'right' ), true ) ? ( $col['align'] ?? 'left' ) : 'left',
				'items'     => array(),
			);

			foreach ( (array) ( $col['items'] ?? array() ) as $mega_item ) {
				if ( ! is_array( $mega_item ) ) continue;
				$item_type = in_array( $mega_item['type'] ?? 'link', $allowed_types, true )
					? $mega_item['type'] : 'link';

				$clean_col['items'][] = array(
					'type'         => $item_type,
					'label'        => sanitize_text_field( $mega_item['label']     ?? '' ),
					'url'          => ( 'link' === $item_type || 'image' === $item_type )
									? esc_url_raw( $mega_item['url'] ?? '' ) : '',
					'icon'         => sanitize_text_field( $mega_item['icon']      ?? '' ),
					'desc'         => sanitize_text_field( $mega_item['desc']      ?? '' ),
					// Per-item color overrides. Empty string means "use the mega menu
					// default / preset color". desc_color applies to link descriptions,
					// link_color to link labels, heading_color to section headings.
					'desc_color'    => sanitize_hex_color( $mega_item['desc_color']    ?? '' ),
					'link_color'    => sanitize_hex_color( $mega_item['link_color']    ?? '' ),
					'heading_color' => sanitize_hex_color( $mega_item['heading_color'] ?? '' ),
					'image_id'     => absint( $mega_item['image_id']               ?? 0 ),
					'image_url'    => esc_url_raw( $mega_item['image_url']         ?? '' ),
					// Image dimensions: integer percent 1-100, empty = auto.
					'image_width'  => self::sanitize_pct( $mega_item['image_width']  ?? '' ),
					'image_height' => self::sanitize_pct( $mega_item['image_height'] ?? '' ),
					// kses strips unsafe HTML in shortcode content.
					'content'      => wp_kses_post( $mega_item['content']          ?? '' ),
					'target'       => in_array( $mega_item['target'] ?? '', array( '', '_blank' ), true )
								   ? ( $mega_item['target'] ?? '' ) : '',
				);

				// Dynamic-block configuration (only meaningful for type 'dynamic').
				if ( 'dynamic' === $item_type ) {
					$src   = in_array( $mega_item['source'] ?? '', array( 'posts', 'wc_cats' ), true )
						? $mega_item['source'] : 'posts';
					$ob_in = $mega_item['dyn_orderby'] ?? '';
					$idx   = count( $clean_col['items'] ) - 1;
					$clean_col['items'][ $idx ]['source']        = $src;
					$clean_col['items'][ $idx ]['dyn_count']     = is_numeric( $mega_item['dyn_count'] ?? '' )
						? (string) min( 20, max( 1, (int) $mega_item['dyn_count'] ) ) : '5';
					$clean_col['items'][ $idx ]['dyn_orderby']   = in_array( $ob_in, self::DYN_ORDERBY[ $src ], true )
						? $ob_in : self::DYN_ORDERBY[ $src ][0];
					$clean_col['items'][ $idx ]['dyn_post_type']      = sanitize_key( $mega_item['dyn_post_type'] ?? 'post' ) ?: 'post';
					$clean_col['items'][ $idx ]['dyn_view_all']       = ( ! empty( $mega_item['dyn_view_all'] ) && '1' === (string) $mega_item['dyn_view_all'] ) ? '1' : '';
					$clean_col['items'][ $idx ]['dyn_view_all_label'] = sanitize_text_field( $mega_item['dyn_view_all_label'] ?? '' );
				}
			}

			$clean_cols[] = $clean_col;
		}

		return $clean_cols;
	}

	public static function sanitize_style( $json_raw ) {
		if ( empty( $json_raw ) ) return array();
		$d = json_decode( stripslashes( (string) $json_raw ), true );
		if ( ! is_array( $d ) ) return array();
		$allowed_div = array( 'solid', 'dashed', 'dotted' );
		return array(
			// bg accepts gradients, so it can't go through sanitize_hex_color();
			// strip tags plus the CSS metacharacters that would allow injecting
			// extra declarations or escaping the rule ( ; } { ).
			'bg'           => ! empty( $d['bg'] )    ? str_replace( array( ';', '{', '}' ), '', wp_strip_all_tags( $d['bg'] ) ) : '',
			'pad_y'        => isset( $d['pad_y'] )   && is_numeric( $d['pad_y'] )  ? (int) $d['pad_y']  : '',
			'pad_x'        => isset( $d['pad_x'] )   && is_numeric( $d['pad_x'] )  ? (int) $d['pad_x']  : '',
			'gap'          => isset( $d['gap'] )      && is_numeric( $d['gap'] )    ? (int) $d['gap']    : '',
			'radius'       => isset( $d['radius'] )   && is_numeric( $d['radius'] ) ? (int) $d['radius'] : '',
			'font_size'    => isset( $d['font_size'] )&& is_numeric( $d['font_size'] ) && (int)$d['font_size'] > 0 ? (int) $d['font_size'] : '',
			'max_w'        => isset( $d['max_w'] )    && is_numeric( $d['max_w'] ) && (int) $d['max_w'] >= 400 ? min( 2400, (int) $d['max_w'] ) : '',
			'link'         => ! empty( $d['link'] )        ? sanitize_hex_color( $d['link'] )        : '',
			'head'         => ! empty( $d['head'] )        ? sanitize_hex_color( $d['head'] )        : '',
			'accent'       => ! empty( $d['accent'] )      ? sanitize_hex_color( $d['accent'] )      : '',
			'hover'        => ! empty( $d['hover'] )       ? sanitize_hex_color( $d['hover'] )       : '',
			'desc'         => ! empty( $d['desc'] )        ? sanitize_hex_color( $d['desc'] )        : '',
			'divider'      => ! empty( $d['divider'] )     ? sanitize_hex_color( $d['divider'] )     : '',
			'divider_style'=> in_array( $d['divider_style'] ?? '', $allowed_div, true ) ? $d['divider_style'] : '',
		);
	}

	/**
	 * Sanitises a percentage value (1-100). Returns '' for empty/invalid input,
	 * which the renderer treats as "auto".
	 *
	 * @param mixed $value Raw percent value.
	 * @return string Integer 1-100 as a string, or '' for auto.
	 */
	private static function sanitize_pct( $value ) {
		if ( '' === $value || ! is_numeric( $value ) ) {
			return '';
		}
		$pct = (int) $value;
		if ( $pct < 1 || $pct > 100 ) {
			return '';
		}
		return (string) $pct;
	}

	// ─────────────────────────────────────────────────────────────────
	// CSS generation — called from css-generator.php
	// ─────────────────────────────────────────────────────────────────

	public static function generate_css( $s = array() ) {
		// Gradient takes priority over solid colour. Gradients can't use
		// sanitize_hex_color(), so strip the CSS metacharacters ( ; } { )
		// that would allow injecting extra declarations.
		if ( ! empty( $s['mega_bg_gradient'] ) ) {
			$bg = str_replace( array( ';', '{', '}' ), '', wp_strip_all_tags( $s['mega_bg_gradient'] ) );
		} elseif ( ! empty( $s['mega_bg'] ) ) {
			$bg = sanitize_hex_color( $s['mega_bg'] ) ?: '#fff';
		} else {
			$bg = '#fff';
		}
		$pad_y      = isset( $s['mega_padding_y'] )        && is_numeric( $s['mega_padding_y'] )    ? (int) $s['mega_padding_y']    : 24;
		$pad_x      = isset( $s['mega_padding_x'] )        && is_numeric( $s['mega_padding_x'] )    ? (int) $s['mega_padding_x']    : 32;
		$col_gap    = isset( $s['mega_col_gap'] )          && is_numeric( $s['mega_col_gap'] )       ? (int) $s['mega_col_gap']      : 16;
		$max_w      = isset( $s['mega_max_width'] )        && is_numeric( $s['mega_max_width'] )     ? (int) $s['mega_max_width']    : 0;
		$radius     = isset( $s['mega_border_radius'] )    && is_numeric( $s['mega_border_radius'] ) ? (int) $s['mega_border_radius'] : 14;
		$font_size  = isset( $s['mega_font_size'] )        && is_numeric( $s['mega_font_size'] )     && (int) $s['mega_font_size'] > 0 ? (int) $s['mega_font_size'] : 0;
		$link_color = ! empty( $s['mega_link_color'] )       ? sanitize_hex_color( $s['mega_link_color'] )       : '#374151';
		$head_color = ! empty( $s['mega_heading_color'] )    ? sanitize_hex_color( $s['mega_heading_color'] )    : '#9ca3af';
		$accent     = ! empty( $s['mega_accent_color'] )     ? sanitize_hex_color( $s['mega_accent_color'] )     : '#667eea';
		$desc_color = ! empty( $s['mega_desc_color'] )       ? sanitize_hex_color( $s['mega_desc_color'] )       : '#9ca3af';
		$hover_color_set = ! empty( $s['mega_link_hover_color'] ) ? sanitize_hex_color( $s['mega_link_hover_color'] ) : '';

		// Improvement 3 — Divider color & style.
		$div_color = ! empty( $s['mega_divider_color'] ) ? sanitize_hex_color( $s['mega_divider_color'] ) : '';
		$div_style = in_array( $s['mega_divider_style'] ?? 'solid', array( 'solid', 'dashed', 'dotted' ), true ) ? ( $s['mega_divider_style'] ?? 'solid' ) : 'solid';

		// Improvement 4 — Open animation duration.
		$open_dur        = isset( $s['mega_open_duration'] ) && is_numeric( $s['mega_open_duration'] ) ? max( 0, min( 800, (int) $s['mega_open_duration'] ) ) : 200;
		$open_dur_s      = round( $open_dur / 1000, 3 ) . 's';
		$div_color_final = $div_color ?: 'rgba(0,0,0,.07)';
		// Mobile layout: list (visible, default) | hide.
		// Back-compat: treat old grid2 value and mega_mobile_disable flag correctly.
		$mob_layout = $s['mega_mobile_layout'] ?? '';
		if ( ! in_array( $mob_layout, array( 'list', 'hide' ), true ) ) {
			$mob_layout = ( ! empty( $s['mega_mobile_disable'] ) && $s['mega_mobile_disable'] === '1' ) ? 'hide' : 'list';
		}
		$breakpoint = isset( $s['mobile_breakpoint'] )     && is_numeric( $s['mobile_breakpoint'] ) ? (int) $s['mobile_breakpoint'] : 768;
		$bp_mode    = $s['mobile_breakpoint_mode'] ?? 'manual';
		$is_auto    = $bp_mode === 'auto';

		$max_w_rule     = $max_w > 0 ? 'max-width:' . $max_w . 'px;margin-left:auto;margin-right:auto;' : '';
		$font_size_rule = $font_size > 0 ? 'font-size:' . $font_size . 'px;' : '';

		// Hover color: dedicated setting wins, otherwise derive from accent.
		$hover_bg    = 'rgba(102,126,234,.08)';
		$hover_color = $hover_color_set !== '' ? $hover_color_set : $accent;

		if ( $mob_layout === 'hide' ) {
			// Completely hidden on mobile.
			$mobile_css = $is_auto
				? '.menux-is-mobile .menux-mega-panel{display:none!important;}'
				: '@media(max-width:' . $breakpoint . 'px){.menux-mega-panel{display:none!important;}}';
		} else {
			// Mobile mega menu: push-navigation sub-screen (3.8.0+).
			// Tapping a mega item slides a full-screen overlay in from the right —
			// exactly like mediaworld.it / euronics on mobile. The inline panel is
			// hidden; JS builds and opens the sub-screen.
			$p  = $is_auto ? '.menux-is-mobile ' : '';
			$wo = $is_auto ? '' : '@media(max-width:' . $breakpoint . 'px){';
			$wc = $is_auto ? '' : '}';

			// Sub-screen background: match the open mobile menu's background.
			$open_style = $s['mobile_open_style'] ?? 'dropdown';
			$ov_bg_hex  = ! empty( $s['mobile_overlay_bg'] ) ? sanitize_hex_color( $s['mobile_overlay_bg'] ) : '#000000';
			if ( $open_style === 'fullscreen' ) {
				$ov_opacity = isset( $s['mobile_overlay_opacity'] ) ? (float) $s['mobile_overlay_opacity'] : 0.9;
				$hex   = ltrim( $ov_bg_hex, '#' );
				$ss_bg = 'rgba(' . hexdec( substr( $hex, 0, 2 ) ) . ',' . hexdec( substr( $hex, 2, 2 ) ) . ',' . hexdec( substr( $hex, 4, 2 ) ) . ',' . round( $ov_opacity, 2 ) . ')';
			} elseif ( $open_style === 'drawer-left' || $open_style === 'drawer-right' ) {
				$ss_bg = $ov_bg_hex;
			} else {
				// dropdown: use the submenu/panel background or white
				$ss_bg = ! empty( $s['submenu_bg'] ) ? sanitize_hex_color( $s['submenu_bg'] ) : '#ffffff';
			}

			// Text color: use the configured mobile link color; on dark overlays
			// (fullscreen/drawer) default to white so text is legible.
			$is_dark_overlay = ( $open_style === 'fullscreen' || $open_style === 'drawer-left' || $open_style === 'drawer-right' );
			$mob_text = ! empty( $s['mobile_link_color'] )
				? sanitize_hex_color( $s['mobile_link_color'] )
				: ( $is_dark_overlay ? '#ffffff' : ( ! empty( $s['submenu_link_color'] ) ? sanitize_hex_color( $s['submenu_link_color'] ) : '#374151' ) );

			// Sub-screen link font/alignment: match regular top-level mobile links.
			$mob_fs_align = $s['mobile_fullscreen_align'] ?? 'center';
			if ( $open_style === 'fullscreen' ) {
				$ss_font = 'font-size:clamp(20px,4vw,32px)!important;font-weight:700!important;';
				$ss_just = 'justify-content:' . esc_attr( $mob_fs_align ) . '!important;';
			} elseif ( $open_style === 'drawer-left' || $open_style === 'drawer-right' ) {
				$ss_font = 'font-size:15px!important;font-weight:inherit!important;';
				$ss_just = 'justify-content:flex-start!important;';
			} else {
				$link_fs = ! empty( $s['font_size'] ) && is_numeric( $s['font_size'] ) ? (int) $s['font_size'] . 'px' : '15px';
				$ss_font = 'font-size:' . $link_fs . '!important;font-weight:inherit!important;';
				$ss_just = 'justify-content:center!important;';
			}

			$mobile_css = $wo
				// Panel never shown inline — JS sub-screen overlay handles mobile display.
				. $p . '.menux-list>li.menux-has-mega>.menux-mega-panel{display:none!important;}'
				// Indicator: right-pointing chevron (push-nav feel), pinned to the right edge.
				// position:relative on the <a> lets ::after use absolute positioning.
				. $p . '.menux-has-mega>a.menux-link{position:relative!important;}'
				. $p . '.menux-has-mega>a.menux-link::after{'
				.   'content:"›"!important;display:inline-block!important;'
				.   'position:absolute!important;right:16px!important;top:50%!important;'
				.   'transform:translateY(-50%)!important;transition:none!important;'
				.   'font-size:20px!important;line-height:1;opacity:.65;margin:0!important;'
				. '}'
				// Sub-screen: hidden globally via static CSS; display:flex + @keyframes when
				// open or closing (avoids desktop visibility and the dark initial-flash bug
				// that plagued the old transform+pointer-events approach).
				. $p . 'li.menux-mega-subscreen-li.menux-ss-open{'
				.   'display:flex!important;flex-direction:column;'
				.   'position:fixed!important;inset:0!important;z-index:100001!important;'
				.   'overflow:hidden;list-style:none!important;margin:0!important;padding:0!important;'
				.   'background:' . $ss_bg . ';'
				.   'animation:menux-ss-in .32s cubic-bezier(.4,0,.2,1) both;'
				. '}'
				. $p . 'li.menux-mega-subscreen-li.menux-ss-closing{'
				.   'display:flex!important;flex-direction:column;'
				.   'position:fixed!important;inset:0!important;z-index:100001!important;'
				.   'overflow:hidden;list-style:none!important;margin:0!important;padding:0!important;'
				.   'background:' . $ss_bg . ';'
				.   'animation:menux-ss-out .28s cubic-bezier(.4,0,.2,1) both;'
				. '}'
				// Sub-screen header row (back button + item title)
				. $p . '.menux-mega-subscreen-head{'
				.   'display:flex;align-items:center;gap:10px;flex-shrink:0;'
				.   'padding:14px 20px;'
				.   'border-bottom:1px solid rgba(128,128,128,.18);'
				. '}'
				. $p . '.menux-mega-subscreen-back{'
				.   'background:none;border:none;cursor:pointer;padding:4px 8px 4px 0;'
				.   'font-size:22px;line-height:1;color:' . $mob_text . ';opacity:.75;flex-shrink:0;'
				. '}'
				. $p . '.menux-mega-subscreen-back:hover,.menux-mega-subscreen-back:focus{opacity:1;outline:none;}'
				. $p . '.menux-mega-subscreen-title{'
				.   'font-size:16px;font-weight:700;color:' . $mob_text . ';'
				.   'cursor:pointer;'
				. '}'
				. $p . '.menux-mega-subscreen-title:hover{opacity:.75;}'
				// Global close button (X) relocated into the header by JS: drop the
				// fixed positioning and pin it to the far right of the flex row.
				. $p . '.menux-close-btn.menux-ss-close{'
				.   'position:static!important;top:auto!important;right:auto!important;'
				.   'margin:0 0 0 auto!important;z-index:auto!important;flex-shrink:0;'
				.   'font-size:28px!important;line-height:1;padding:0 4px!important;'
				.   'color:' . $mob_text . '!important;'
				. '}'
				// Links list inside sub-screen
				. $p . '.menux-mega-subscreen-links{'
				.   'list-style:none;margin:0;padding:0;flex:1;overflow-y:auto;'
				. '}'
				. $p . '.menux-mega-subscreen-links>li{list-style:none;margin:0;}'
				// Individual links: same styling as regular top-level mobile links
				. $p . '.menux-mega-subscreen-links a.menux-mega-link{'
				.   'display:flex!important;align-items:center!important;gap:10px!important;'
				.   'padding:14px 20px!important;margin:0!important;'
				.   'border-radius:0!important;background:transparent!important;'
				.   'color:' . $mob_text . '!important;text-decoration:none;'
				.   'border-bottom:1px solid rgba(128,128,128,.08);'
				.   'width:100%;box-sizing:border-box;'
				.   $ss_font
				.   $ss_just
				. '}'
				. $p . '.menux-mega-subscreen-links a.menux-mega-link .menux-mega-link-inner{align-items:flex-start!important;}'
				. $p . '.menux-mega-subscreen-links a.menux-mega-link .menux-mega-link-label{'
				.   'color:' . $mob_text . '!important;font-size:inherit!important;font-weight:inherit!important;'
				. '}'
				. $p . '.menux-mega-subscreen-links a.menux-mega-link .menux-mega-link-desc{display:none!important;}'
				. $p . '.menux-mega-subscreen-links a.menux-mega-link i{color:' . $mob_text . '!important;font-size:1em;}'
				. $p . '.menux-mega-subscreen-links a.menux-mega-link:hover{background:rgba(128,128,128,.08)!important;}'
				// Section headings inside sub-screen: small caps label, neutral, non-interactive
				. $p . '.menux-mega-subscreen-links .menux-ss-heading{'
				.   'display:block;font-size:11px!important;font-weight:700;'
				.   'color:' . $mob_text . ';opacity:.55;'
				.   'text-transform:uppercase;letter-spacing:.7px;'
				.   'padding:14px 20px 5px;margin:0;'
				. '}'
				. $wc;
		}

		return '
/* ── Mega Menu ─────────────────────────────────────── */
/* Full-width panels: resolve left:0/right:0 against .menux-container, not the <li>. */
.menux-list > li.menux-has-mega:has(> .menux-mega-full){position:static;}
.menux-mega-panel{
  position:absolute;left:0;right:0;top:100%;
  background:' . esc_attr( $bg ) . ';
  ' . $font_size_rule . '
  box-shadow:0 16px 48px -8px rgba(0,0,0,.18),0 2px 8px rgba(0,0,0,.06);
  border-radius:0 0 ' . $radius . 'px ' . $radius . 'px;
  opacity:0;visibility:hidden;pointer-events:none;

  transform:translateY(-10px);
  transition:opacity ' . $open_dur_s . ' ease,visibility ' . $open_dur_s . ' ease,transform ' . $open_dur_s . ' ease;
  z-index:999999;
}
/* Non-full-width panels: positioned relative to the <li>, not the container. */
.menux-mega-panel:not(.menux-mega-full){
  left:0;right:auto;min-width:300px;width:max-content;max-width:min(600px,90vw);
}
/* Flip alignment (set by JS) when a left-aligned panel would overflow the
   right edge of the viewport: anchor it to the right edge of the <li> instead. */
.menux-mega-panel:not(.menux-mega-full).menux-mega-flip{left:auto;right:0;}
.menux-list > li.menux-has-mega.menux-open > .menux-mega-panel{
  opacity:1;visibility:visible;pointer-events:auto;transform:none;
}
/* Invisible bridge: extends the panel hover zone 10px upward so the
   mouse never briefly exits both the <li> and the panel while moving
   from the nav link down to the panel (pointer-events inherited). */
.menux-mega-panel::before{
  content:"";position:absolute;top:-10px;left:0;right:0;height:10px;
}
/* Flatten the container bottom corners when a mega panel is open so
   there is no gap between the curved container edge and the panel. */
.menux-container:has(.menux-has-mega.menux-open){
  border-bottom-left-radius:0!important;
  border-bottom-right-radius:0!important;
}
.menux-mega-inner{
  display:flex;gap:' . $col_gap . 'px;
  padding:' . $pad_y . 'px ' . $pad_x . 'px;
  ' . $max_w_rule . '
}
.menux-mega-col{flex:1;min-width:0;}
.menux-mega-heading{
  font-size:10px;font-weight:700;color:' . esc_attr( $head_color ) . ';
  text-transform:uppercase;letter-spacing:.7px;
  margin:0 0 10px;padding:0 0 8px;
  border-bottom:1px ' . $div_style . ' ' . $div_color_final . ';
}
.menux-mega-link{
  display:flex;align-items:flex-start;gap:10px;
  padding:7px 10px;margin:0 -10px;
  text-decoration:none;color:' . esc_attr( $link_color ) . ';
  border-radius:8px;transition:background .15s,color .15s;
}
.menux-mega-link:hover{background:' . $hover_bg . ';color:' . esc_attr( $hover_color ) . ';}
.menux-mega-link:hover i{color:' . esc_attr( $hover_color ) . ';}
.menux-mega-link:hover .menux-mega-link-label{color:' . esc_attr( $hover_color ) . ';}
.menux-mega-link i{font-size:15px;flex-shrink:0;margin-top:2px;color:' . esc_attr( $accent ) . ';transition:color .15s;}
.menux-mega-heading i{font-size:15px;flex-shrink:0;margin-right:6px;color:' . esc_attr( $accent ) . ';}
.menux-mega-link-inner{display:flex;flex-direction:column;}
.menux-mega-link-label{font-size:13px;font-weight:500;line-height:1.3;}
.menux-mega-link-desc{font-size:11px;color:' . esc_attr( $desc_color ) . ';margin-top:2px;line-height:1.4;}
.menux-mega-viewall .menux-mega-link-label{font-weight:600;color:' . esc_attr( $accent ) . ';}
.menux-mega-viewall{margin-top:4px;}
.menux-mega-divider{border:none;border-top:1px ' . $div_style . ' ' . $div_color_final . ';margin:8px 0;}
.menux-mega-col[data-align="center"] .menux-mega-link{justify-content:center;}
.menux-mega-col[data-align="center"] .menux-mega-link-inner{align-items:center;text-align:center;}
.menux-mega-col[data-align="center"] .menux-mega-heading{text-align:center;}
.menux-mega-col[data-align="right"] .menux-mega-link{justify-content:flex-end;}
.menux-mega-col[data-align="right"] .menux-mega-link-inner{align-items:flex-end;text-align:right;}
.menux-mega-col[data-align="right"] .menux-mega-heading{text-align:right;}
.menux-mega-image img{width:100%;height:auto;border-radius:8px;display:block;}
.menux-mega-widget{font-size:13px;line-height:1.5;}
/* Sub-screen: always hidden; revealed only inside the mobile media block via .menux-ss-open */
li.menux-mega-subscreen-li{display:none!important;}
@keyframes menux-ss-in{from{transform:translateX(100%)}to{transform:translateX(0)}}
@keyframes menux-ss-out{from{transform:translateX(0)}to{transform:translateX(100%)}}
' . $mobile_css . '
';
	}

	/**
	 * Generates per-item scoped CSS for mega menus that have custom appearance overrides.
	 * Called from menux_get_cached_css() in assets.php.
	 *
	 * @param array $menu_items  The menux_menu_items option value.
	 * @param array $global_s    The menux_style option (used as defaults).
	 * @return string
	 */
	public static function generate_items_css( $menu_items, $global_s = array() ) {
		if ( empty( $menu_items ) || ! is_array( $menu_items ) ) return '';
		$css = '';
		foreach ( $menu_items as $item ) {
			$key   = $item['item_key'] ?? '';
			$ms    = $item['mega_style'] ?? array();
			if ( ! $key || empty( $ms ) ) continue;
			// Only generate CSS if at least one override is set
			$has = false;
			foreach ( $ms as $v ) { if ( $v !== '' ) { $has = true; break; } }
			if ( ! $has ) continue;

			$sel = '.menux-mega-panel[data-mega-key="' . esc_attr( $key ) . '"]';

			// Background — strip CSS metacharacters too, so values saved before
			// sanitize_style() hardened them can't inject extra declarations.
			if ( ! empty( $ms['bg'] ) ) {
				$bg = str_replace( array( ';', '{', '}' ), '', wp_strip_all_tags( $ms['bg'] ) );
				$css .= $sel . '{background:' . $bg . ';}';
			}

			// Padding
			$pad_y = ( $ms['pad_y'] !== '' ) ? (int) $ms['pad_y'] : null;
			$pad_x = ( $ms['pad_x'] !== '' ) ? (int) $ms['pad_x'] : null;
			if ( $pad_y !== null || $pad_x !== null ) {
				$py = $pad_y !== null ? $pad_y : ( isset( $global_s['mega_padding_y'] ) ? (int) $global_s['mega_padding_y'] : 24 );
				$px = $pad_x !== null ? $pad_x : ( isset( $global_s['mega_padding_x'] ) ? (int) $global_s['mega_padding_x'] : 32 );
				$css .= $sel . ' .menux-mega-inner{padding:' . $py . 'px ' . $px . 'px;}';
			}

			// Column gap
			if ( $ms['gap'] !== '' ) {
				$gap = (int) $ms['gap'];
				$css .= $sel . ' .menux-mega-inner{gap:' . $gap . 'px;}';
			}

			// Border radius — only bottom corners (top must stay 0 to avoid gap with nav bar)
			// Only emit when > 0; value of 0 means "inherit global default" (browser auto-populates min=0)
			if ( $ms['radius'] !== '' && (int) $ms['radius'] > 0 ) {
				$r = (int) $ms['radius'];
				$css .= $sel . '{border-radius:0 0 ' . $r . 'px ' . $r . 'px;}';
			}

			// Font size
			if ( $ms['font_size'] !== '' ) {
				$css .= $sel . '{font-size:' . (int) $ms['font_size'] . 'px;}';
			}

			// Panel max width — only meaningful for full-width panels.
			// Full-width panels have left:0/right:0 resolved against .menux-container,
			// so margin:auto correctly centers the box within the container.
			// Non-full-width panels have right:auto (positioned relative to the <li>),
			// making margin:auto a no-op — the panel would pin to the left edge instead
			// of centering, silently ignoring the setting.
			if ( isset( $ms['max_w'] ) && $ms['max_w'] !== '' && ! empty( $item['mega_full_width'] ) ) {
				$mw = (int) $ms['max_w'];
				$css .= $sel . '{max-width:' . $mw . 'px;margin-left:auto;margin-right:auto;}';
				// Use 100% (not none) so the inner stays bounded by the panel box.
				// This lets the tighter value win when max_w > global mega_max_width,
				// rather than removing the global cap entirely.
				$css .= $sel . ' .menux-mega-inner{max-width:100%;}';
			}

			// Link color
			if ( ! empty( $ms['link'] ) ) {
				$c = sanitize_hex_color( $ms['link'] );
				$css .= $sel . ' .menux-mega-link-label{color:' . $c . ';}';
			}

			// Heading color
			if ( ! empty( $ms['head'] ) ) {
				$c = sanitize_hex_color( $ms['head'] );
				$css .= $sel . ' .menux-mega-heading{color:' . $c . ';}';
			}

			// Accent color — icons inside links plus the "View all" label,
			// matching the elements the global $accent rule targets.
			if ( ! empty( $ms['accent'] ) ) {
				$c = sanitize_hex_color( $ms['accent'] );
				$css .= $sel . ' .menux-mega-link i{color:' . $c . ';}';
				$css .= $sel . ' .menux-mega-viewall .menux-mega-link-label{color:' . $c . ';}';
			}

			// Link hover color — apply to both label and icon to match the global hover rule
			if ( ! empty( $ms['hover'] ) ) {
				$c = sanitize_hex_color( $ms['hover'] );
				$css .= $sel . ' .menux-mega-link:hover .menux-mega-link-label,' . $sel . ' .menux-mega-link:hover i{color:' . $c . '!important;}';
			}

			// Description color
			if ( ! empty( $ms['desc'] ) ) {
				$c = sanitize_hex_color( $ms['desc'] );
				$css .= $sel . ' .menux-mega-link-desc{color:' . $c . ';}';
			}

			// Divider color + style
			if ( ! empty( $ms['divider'] ) ) {
				$c = sanitize_hex_color( $ms['divider'] );
				$ds = in_array( $ms['divider_style'] ?? '', array( 'solid', 'dashed', 'dotted' ), true )
					? $ms['divider_style'] : 'solid';
				$css .= $sel . ' .menux-mega-heading{border-bottom-color:' . $c . ';border-bottom-style:' . $ds . ';}';
				$css .= $sel . ' .menux-mega-divider{border-top-color:' . $c . ';border-top-style:' . $ds . ';}';
			} elseif ( ! empty( $ms['divider_style'] ) ) {
				$ds = in_array( $ms['divider_style'], array( 'solid', 'dashed', 'dotted' ), true ) ? $ms['divider_style'] : 'solid';
				$css .= $sel . ' .menux-mega-heading{border-bottom-style:' . $ds . ';}';
				$css .= $sel . ' .menux-mega-divider{border-top-style:' . $ds . ';}';
			}
		}
		return $css;
	}
}
