<?php
/**
 * Main Plugin Class
 *
 * Orchestrates all plugin components. Instantiates sub-classes, registers
 * WordPress hooks, and manages admin page and asset registration.
 *
 * @package Agency_Pulse_Admin_Menu
 * @since   1.0.0
 */

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

class Agency_Pulse_Admin_Menu {

	/**
	 * Singleton instance.
	 *
	 * @var Agency_Pulse_Admin_Menu|null
	 */
	private static $instance = null;

	/**
	 * @var Agency_Pulse_Admin_Menu_Settings_Manager
	 */
	private $settings_manager;

	/**
	 * @var Agency_Pulse_Admin_Menu_Builder
	 */
	private $menu_builder;

	/**
	 * @var Agency_Pulse_Admin_Menu_Role_Assignments
	 */
	private $role_assignments;

	/**
	 * @var Agency_Pulse_Admin_Menu_Settings_Page
	 */
	private $settings_page;

	/**
	 * @var Agency_Pulse_Admin_Menu_Role_Manager
	 */
	private $role_manager;

	/**
	 * Hook suffix returned by add_submenu_page() for the builder page.
	 * Used to scope asset loading to the correct admin page.
	 *
	 * @var string
	 */
	private $builder_hook = '';

	/**
	 * Hook suffix returned by add_submenu_page() for the role assignments page.
	 *
	 * @var string
	 */
	private $role_assignments_hook = '';

	/**
	 * Hook suffix returned by add_submenu_page() for the settings page.
	 *
	 * @var string
	 */
	private $settings_hook = '';

	/**
	 * Hook suffix returned by add_submenu_page() for the customize page.
	 *
	 * @var string
	 */
	private $customize_hook = '';

	/**
	 * Hook suffix returned by add_submenu_page() for the layout page.
	 *
	 * @var string
	 */
	private $layout_hook = '';

	/**
	 * Return the Settings_Page instance (used by static classes such as the webhook handler).
	 *
	 * @since 1.0.0
	 * @return Agency_Pulse_Admin_Menu_Settings_Page
	 */
	public function get_settings_page(): Agency_Pulse_Admin_Menu_Settings_Page {
		return $this->settings_page;
	}

	/**
	 * @return Agency_Pulse_Admin_Menu
	 */
	public static function get_instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		$this->init_components();
		$this->setup_hooks();
	}

	/** @ignore */
	private function __clone() {}

	/** @ignore */
	public function __wakeup() {}

	// -------------------------------------------------------------------------
	// Initialisation
	// -------------------------------------------------------------------------

	/**
	 * Instantiate all plugin sub-components.
	 *
	 * @since 1.0.0
	 */
	private function init_components(): void {
		$this->settings_manager = Agency_Pulse_Admin_Menu_Settings_Manager::get_instance();
		$this->menu_builder     = new Agency_Pulse_Admin_Menu_Builder( $this->settings_manager );
		$this->role_assignments = new Agency_Pulse_Admin_Menu_Role_Assignments( $this->settings_manager );
		$this->settings_page    = new Agency_Pulse_Admin_Menu_Settings_Page( $this->settings_manager );
		$this->role_manager     = Agency_Pulse_Admin_Menu_Role_Manager::get_instance();
	}

	/**
	 * Register all WordPress hooks.
	 *
	 * Note: activation and deactivation hooks are registered in the main plugin
	 * file (agency-pulse-admin-menu.php) rather than here, because they must be
	 * called at file-load time — before plugins_loaded fires — for correct
	 * behaviour during a first activation.
	 *
	 * @since 1.0.0
	 */
	private function setup_hooks(): void {
		// Admin pages — registered early so other plugins can hook admin_menu after us.
		add_action( 'admin_menu', array( $this, 'register_admin_menus' ), 5 );

		// Capture raw $menu/$submenu before labels are applied — used by the snapshot
		// so original_title always reflects the true WordPress-registered title.
		add_action( 'admin_menu', array( $this->menu_builder, 'capture_pre_apply_state' ), 998 );

		// Config apply — runs last, after all plugins have registered their items.
		add_action( 'admin_menu', array( $this->menu_builder, 'apply_menu_config' ), 999 );

		// Always render the Menu Organizer last in the sidebar, regardless of config.
		add_filter( 'custom_menu_order', '__return_true' );
		add_filter( 'menu_order', array( $this, 'push_organizer_last' ), 20 );

		// Assets.
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );

		// Builder AJAX endpoints.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_CONFIG,      array( $this->menu_builder, 'ajax_save_config' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CONFIG,     array( $this->menu_builder, 'ajax_reset_config' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_GET_MENU,         array( $this->menu_builder, 'ajax_get_current_menu' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_CREATE_CONFIG,    array( $this->menu_builder, 'ajax_create_config' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RENAME_CONFIG,    array( $this->menu_builder, 'ajax_rename_config' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_DUPLICATE_CONFIG, array( $this->menu_builder, 'ajax_duplicate_config' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_DELETE_CONFIG,    array( $this->menu_builder, 'ajax_delete_config' ) );

		// Role assignments AJAX endpoint.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ASSIGNMENTS, array( $this->role_assignments, 'ajax_save_role_assignments' ) );

		// Role Manager — Plugin Access AJAX endpoints.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GRANT_ROLE,  array( $this->role_manager, 'ajax_grant_role' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_REVOKE_ROLE, array( $this->role_manager, 'ajax_revoke_role' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GRANT_USER,  array( $this->role_manager, 'ajax_grant_user' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_REVOKE_USER, array( $this->role_manager, 'ajax_revoke_user' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_SEARCH_USERS, array( $this->role_manager, 'ajax_search_users' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GET_CAPS,     array( $this->role_manager, 'ajax_get_role_caps' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_SAVE_CAPS,    array( $this->role_manager, 'ajax_save_role_caps' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_CLONE_ROLE,   array( $this->role_manager, 'ajax_clone_role' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RM_DELETE_ROLE,  array( $this->role_manager, 'ajax_delete_role' ) );

		// Settings page: export / import (admin-post).
		add_action( 'admin_post_' . Agency_Pulse_Admin_Menu_Constants::EXPORT_ACTION, array( $this->settings_page, 'handle_export' ) );
		add_action( 'admin_post_' . Agency_Pulse_Admin_Menu_Constants::IMPORT_ACTION, array( $this->settings_page, 'handle_import' ) );

		// Settings page: cloud backup (AJAX).
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PUSH, array( $this->settings_page, 'ajax_backup_push' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PULL, array( $this->settings_page, 'ajax_backup_pull' ) );

		// Settings page: license & connection (AJAX).
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_AO_CONNECT, array( $this->settings_page, 'ajax_ao_connect' ) );

		// Dashboard message widget (local) and remote webhook endpoint.
		Agency_Pulse_Admin_Menu_Dashboard_Widget::init();
		Agency_Pulse_Admin_Menu_Dashboard_Webhook::init();

		// Access control — register user_has_cap filter when mode is admin_only.
		Agency_Pulse_Admin_Menu_Access_Control::init();

		// Access control AJAX endpoints.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SET_ACCESS_MODE,    array( $this->settings_page, 'ajax_set_access_mode' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_GRANT_MANAGER_CAP,  array( $this->settings_page, 'ajax_grant_manager_cap' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_REVOKE_MANAGER_CAP, array( $this->settings_page, 'ajax_revoke_manager_cap' ) );

		// Dashboard message AJAX endpoint.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_DASHBOARD_MSG, array( $this->settings_page, 'ajax_save_dashboard_message' ) );

		// Settings page: template library (AJAX).
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_SAVE,   array( $this->settings_page, 'ajax_template_save' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_LIST,   array( $this->settings_page, 'ajax_template_list' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_APPLY,  array( $this->settings_page, 'ajax_template_apply' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_DELETE, array( $this->settings_page, 'ajax_template_delete' ) );

		// Settings page: admin customization (AJAX).
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_BRANDING_ICON,  array( $this->settings_page, 'ajax_save_branding_icon' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ADMIN_FOOTER,   array( $this->settings_page, 'ajax_save_admin_footer' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_COLOR_SCHEME,   array( $this->settings_page, 'ajax_save_color_scheme' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_COLOR_SCHEME,  array( $this->settings_page, 'ajax_reset_color_scheme' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CUSTOMIZATION, array( $this->settings_page, 'ajax_reset_customization' ) );

		// Admin layout AJAX.
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_LAYOUT,  array( $this->settings_page, 'ajax_save_layout' ) );
		add_action( 'wp_ajax_' . Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_LAYOUT, array( $this->settings_page, 'ajax_reset_layout' ) );

		// Admin customization — runtime application.
		add_action( 'admin_bar_menu',        array( $this, 'apply_admin_bar_customizer' ), 999 );
		add_filter( 'admin_footer_text',     array( $this, 'apply_admin_footer_left' ) );
		add_filter( 'update_footer',         array( $this, 'apply_admin_footer_right' ), 10, 2 );
		add_action( 'admin_enqueue_scripts', array( $this, 'apply_color_scheme' ), 999 );
		add_action( 'admin_enqueue_scripts', array( $this, 'apply_layout_styles' ), 1000 );

		// License state notices on plugin admin pages.
		add_action( 'admin_init', array( 'Agency_Pulse_Admin_Menu_Notices', 'maybe_display_license_notices' ) );

		// Private server update checker.
		add_action( 'admin_init', array( $this, 'init_update_checker' ), 20 );
	}

	// -------------------------------------------------------------------------
	// Admin pages
	// -------------------------------------------------------------------------

	/**
	 * Register all plugin admin pages.
	 *
	 * Fires the extensibility hook after registration so companion plugins can
	 * attach sub-pages.
	 *
	 * @since 1.0.0
	 */
	public function register_admin_menus(): void {
		$capability = apply_filters(
			Agency_Pulse_Admin_Menu_Constants::FILTER_BUILDER_CAPS,
			Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS
		);

		// Top-level entry — links to the Menu Builder page.
		add_menu_page(
			__( 'Menu Organizer', 'agency-pulse-admin-menu' ),
			__( 'Menu Organizer', 'agency-pulse-admin-menu' ),
			$capability,
			Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			array( $this->menu_builder, 'render_builder_page' ),
			'dashicons-menu-alt',
			3
		);

		// Menu Builder sub-page (same slug as top-level — collapses the duplicate entry).
		$this->builder_hook = add_submenu_page(
			Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			__( 'Menu Builder', 'agency-pulse-admin-menu' ),
			__( 'Menu Builder', 'agency-pulse-admin-menu' ),
			$capability,
			Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			array( $this->menu_builder, 'render_builder_page' )
		);

		// Admin Customizer sub-page.
		$this->customize_hook = add_submenu_page(
			Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			__( 'Admin Customizer', 'agency-pulse-admin-menu' ),
			__( 'Admin Customizer', 'agency-pulse-admin-menu' ),
			$capability,
			Agency_Pulse_Admin_Menu_Constants::PAGE_CUSTOMIZE,
			array( $this, 'render_customize_page' )
		);

		// Settings sub-page (last).
		$this->settings_hook = add_submenu_page(
			Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			__( 'Settings', 'agency-pulse-admin-menu' ),
			__( 'Settings', 'agency-pulse-admin-menu' ),
			$capability,
			Agency_Pulse_Admin_Menu_Constants::PAGE_SETTINGS,
			array( $this, 'render_settings_page' )
		);

		/**
		 * Fires after the plugin registers its admin pages.
		 * Companion plugins can hook here to add sub-pages.
		 *
		 * @since 1.0.0
		 * @param array $menu_info {
		 *     @type string $parent_menu   Top-level menu slug.
		 *     @type string $settings_page Settings sub-page slug.
		 * }
		 */
		do_action( Agency_Pulse_Admin_Menu_Constants::HOOK_REGISTER_MENUS, array(
			'parent_menu'   => Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER,
			'settings_page' => Agency_Pulse_Admin_Menu_Constants::PAGE_SETTINGS,
		) );
	}

	// -------------------------------------------------------------------------
	// Menu order
	// -------------------------------------------------------------------------

	/**
	 * menu_order callback: always place the Menu Organizer at the very end.
	 *
	 * Runs at priority 20 — after apply_menu_config()'s priority-10 filter — so it
	 * wins regardless of where the saved config positioned the organizer.
	 *
	 * @since 1.0.0
	 * @param array $order Current ordered list of menu slugs.
	 * @return array
	 */
	public function push_organizer_last( array $order ): array {
		$slug  = Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER;
		$order = array_values( array_filter( $order, function ( $s ) use ( $slug ) {
			return $s !== $slug;
		} ) );
		$order[] = $slug;
		return $order;
	}

	// -------------------------------------------------------------------------
	// Assets
	// -------------------------------------------------------------------------

	/**
	 * Enqueue admin assets, scoped to the relevant plugin pages only.
	 *
	 * Hook suffixes are captured from the return values of add_submenu_page()
	 * during register_admin_menus() so that comparison here is exact.
	 * Localization data is added to each script in its respective build phase
	 * (Phase 6 for builder JS, Phase 7 for role assignments JS).
	 *
	 * @since 1.0.0
	 * @param string $hook_suffix Current admin page hook suffix.
	 */
	public function enqueue_assets( string $hook_suffix ): void {
		if ( $this->builder_hook && $hook_suffix === $this->builder_hook ) {
			wp_enqueue_style(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_CSS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/css/admin.css',
				array(),
				AGENCY_PULSE_ADMIN_MENU_VERSION
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/admin.js',
				array( 'jquery', 'jquery-ui-sortable' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				true
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_COLLAPSE_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/collapse.js',
				array( 'jquery' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);

			$all_configs      = $this->settings_manager->get_all_configs();
			$active_config_id = $this->menu_builder->resolve_active_config_id( $all_configs );
			$active_config    = isset( $all_configs[ $active_config_id ] ) ? $all_configs[ $active_config_id ] : array();
			$saved_config     = $this->settings_manager->get_config_items( $active_config_id );

			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_JS,
				'aamoData',
				array(
					'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
					'nonce'            => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_CONFIG ),
					'allConfigs'       => $all_configs,
					'activeConfigId'   => $active_config_id,
					'activeConfigName' => isset( $active_config['name'] ) ? $active_config['name'] : '',
					'savedConfig'      => $saved_config,
					'builderUrl'       => admin_url( 'admin.php?page=' . Agency_Pulse_Admin_Menu_Constants::PAGE_MENU_BUILDER ),
					'actions'          => array(
						'saveConfig'      => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_CONFIG,
						'resetConfig'     => Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CONFIG,
						'createConfig'    => Agency_Pulse_Admin_Menu_Constants::AJAX_CREATE_CONFIG,
						'renameConfig'    => Agency_Pulse_Admin_Menu_Constants::AJAX_RENAME_CONFIG,
						'duplicateConfig' => Agency_Pulse_Admin_Menu_Constants::AJAX_DUPLICATE_CONFIG,
						'deleteConfig'    => Agency_Pulse_Admin_Menu_Constants::AJAX_DELETE_CONFIG,
					),
					'i18n'             => array(
						'saving'          => __( 'Saving…', 'agency-pulse-admin-menu' ),
						'saved'           => __( 'Saved!', 'agency-pulse-admin-menu' ),
						'error'           => __( 'Could not save. Please try again.', 'agency-pulse-admin-menu' ),
						'unsavedChanges'  => __( '(unsaved changes)', 'agency-pulse-admin-menu' ),
						'unsavedWarning'  => __( 'You have unsaved changes. Leave the page?', 'agency-pulse-admin-menu' ),
						'confirmReset'    => __( 'Reset to WordPress default? All customizations for this configuration will be lost.', 'agency-pulse-admin-menu' ),
						'confirmDelete'   => __( 'Delete this configuration? This cannot be undone.', 'agency-pulse-admin-menu' ),
						'newConfigName'   => __( 'Enter a name for the new configuration:', 'agency-pulse-admin-menu' ),
						'renamePrompt'    => __( 'Enter a new name for this configuration:', 'agency-pulse-admin-menu' ),
						'duplicatePrompt' => __( 'Enter a name for the duplicate:', 'agency-pulse-admin-menu' ),
						'copyOf'          => __( 'Copy of', 'agency-pulse-admin-menu' ),
						'containerName'   => __( 'Enter a name for the new container:', 'agency-pulse-admin-menu' ),
						'containerBadge'  => __( 'custom', 'agency-pulse-admin-menu' ),
						'hideItem'        => __( 'Hide item', 'agency-pulse-admin-menu' ),
						'showItem'        => __( 'Show item', 'agency-pulse-admin-menu' ),
						'restoreLabel'    => __( 'click to restore', 'agency-pulse-admin-menu' ),
						'chooseIcon'      => __( 'Choose Icon', 'agency-pulse-admin-menu' ),
					'dblClickIcon'    => __( 'dbl click to change icon', 'agency-pulse-admin-menu' ),
					),
				)
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_ROLE_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/role-assignments.js',
				array( 'jquery' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				true
			);
			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_ROLE_JS,
				'aamoRoleData',
				array(
					'ajaxUrl' => admin_url( 'admin-ajax.php' ),
					'nonce'   => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ASSIGNMENTS ),
					'action'  => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ASSIGNMENTS,
					'i18n'    => array(
						'saving' => __( 'Saving…', 'agency-pulse-admin-menu' ),
						'saved'  => __( 'Saved!', 'agency-pulse-admin-menu' ),
						'error'  => __( 'Could not save. Please try again.', 'agency-pulse-admin-menu' ),
					),
				)
			);

			// Role Manager localization.
			$_rm_all_caps_set = array();
			foreach ( get_editable_roles() as $_rm_rd ) {
				foreach ( array_keys( $_rm_rd['capabilities'] ) as $_rm_c ) {
					$_rm_all_caps_set[ $_rm_c ] = true;
				}
			}
			ksort( $_rm_all_caps_set );
			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_JS,
				'aamoRoleManagerData',
				array(
					'ajaxUrl' => admin_url( 'admin-ajax.php' ),
					'nonce'   => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_RM_NONCE ),
					'actions' => array(
						'grantRole'   => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GRANT_ROLE,
						'revokeRole'  => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_REVOKE_ROLE,
						'grantUser'   => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GRANT_USER,
						'revokeUser'  => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_REVOKE_USER,
						'searchUsers' => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_SEARCH_USERS,
					'getCaps'     => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_GET_CAPS,
					'saveCaps'    => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_SAVE_CAPS,
					'cloneRole'   => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_CLONE_ROLE,
					'deleteRole'  => Agency_Pulse_Admin_Menu_Constants::AJAX_RM_DELETE_ROLE,
					),
					'i18n'    => array(
						'saved'            => __( 'Saved.', 'agency-pulse-admin-menu' ),
						'error'            => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'revoke'           => __( 'Revoke', 'agency-pulse-admin-menu' ),
						'builtinRole'      => __( 'built-in role', 'agency-pulse-admin-menu' ),
						'roleLabel'        => __( 'role', 'agency-pulse-admin-menu' ),
						'userLabel'        => __( 'user', 'agency-pulse-admin-menu' ),
						'noAccess'         => __( 'No roles or users currently have Menu Organizer access.', 'agency-pulse-admin-menu' ),
						'userRequired'     => __( 'Please enter a user ID.', 'agency-pulse-admin-menu' ),
						'adminWarn'        => __( 'Revoking administrator access may lock you out of the Menu Organizer.', 'agency-pulse-admin-menu' ),
						'adminWarnConfirm' => __( 'Revoking administrator role access may lock all administrators out of the Menu Organizer. Are you sure?', 'agency-pulse-admin-menu' ),
						'noResults'        => __( 'No users found.', 'agency-pulse-admin-menu' ),
						'searching'        => __( 'Searching…', 'agency-pulse-admin-menu' ),
					'loading'          => __( 'Loading…', 'agency-pulse-admin-menu' ),
					'coreGroup'        => __( 'WordPress Core', 'agency-pulse-admin-menu' ),
					'otherGroup'       => __( 'Other Capabilities', 'agency-pulse-admin-menu' ),
					'noCaps'           => __( 'This role has no capabilities.', 'agency-pulse-admin-menu' ),
					'searchCaps'       => __( 'Search capabilities…', 'agency-pulse-admin-menu' ),
					'adminCapWarn'     => __( 'Editing the Administrator role may affect your access to the Menu Organizer.', 'agency-pulse-admin-menu' ),
					'saveChanges'      => __( 'Save Changes', 'agency-pulse-admin-menu' ),
					'saving'           => __( 'Saving…', 'agency-pulse-admin-menu' ),
					'addCap'           => __( 'Add Capability', 'agency-pulse-admin-menu' ),
					'addCapPlaceholder'=> __( 'new_capability_slug', 'agency-pulse-admin-menu' ),
					'addCapEmpty'      => __( 'Please enter a capability slug.', 'agency-pulse-admin-menu' ),
					'addCapInvalid'    => __( 'Slug must start with a letter and contain only lowercase letters, numbers, and underscores.', 'agency-pulse-admin-menu' ),
					'addCapDuplicate'  => __( 'This capability already exists on this role.', 'agency-pulse-admin-menu' ),
					'nameRequired'     => __( 'Role name is required.', 'agency-pulse-admin-menu' ),
					'slugInvalid'      => __( 'Slug must start with a letter and use only lowercase letters, numbers, and underscores.', 'agency-pulse-admin-menu' ),
					'slugExists'       => __( 'A role with this slug already exists.', 'agency-pulse-admin-menu' ),
					'cloning'          => __( 'Cloning…', 'agency-pulse-admin-menu' ),
					'cloned'           => __( 'Role cloned successfully.', 'agency-pulse-admin-menu' ),
					'newRole'          => __( 'New Role', 'agency-pulse-admin-menu' ),
					'newRoleDesc'      => __( 'The new role will start with no capabilities.', 'agency-pulse-admin-menu' ),
					'createRole'       => __( 'Create Role', 'agency-pulse-admin-menu' ),
					'deleteRole'       => __( 'Delete Role', 'agency-pulse-admin-menu' ),
					'deleteConfirm'    => __( 'Type the role name to confirm deletion:', 'agency-pulse-admin-menu' ),
					'deleteWarning'    => __( 'Users assigned this role will retain the stale role slug in their database record.', 'agency-pulse-admin-menu' ),
					'deleteIrreversible' => __( 'This action cannot be undone.', 'agency-pulse-admin-menu' ),
					'deleting'         => __( 'Deleting…', 'agency-pulse-admin-menu' ),
					'deleted'          => __( 'Role deleted.', 'agency-pulse-admin-menu' ),
					'selectRole'       => __( 'Select a role on the left to view and edit its capabilities.', 'agency-pulse-admin-menu' ),
					),
				'allCaps' => array_keys( $_rm_all_caps_set ),
				)
			);
		}

		$is_settings_page = isset( $_GET['page'] ) // phpcs:ignore WordPress.Security.NonceVerification
			&& Agency_Pulse_Admin_Menu_Constants::PAGE_SETTINGS === $_GET['page'];
		if ( $is_settings_page ) {
			wp_enqueue_style(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_CSS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/css/admin.css',
				array(),
				AGENCY_PULSE_ADMIN_MENU_VERSION
			);
			// Load in the head — admin_footer does not fire reliably on this page.
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_SETTINGS_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/settings.js',
				array( 'jquery' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_COLLAPSE_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/collapse.js',
				array( 'jquery' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);

			$license_state = Agency_Pulse_Admin_Menu_License_Manager::get_state();
			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_SETTINGS_JS,
				'aamoSettingsData',
				array(
					'ajaxUrl'        => admin_url( 'admin-ajax.php' ),
					'noncePush'      => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PUSH ),
					'noncePull'      => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PULL ),
					'nonceAoConnect' => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_AO_CONNECT ),
					'actionPush'     => Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PUSH,
					'actionPull'     => Agency_Pulse_Admin_Menu_Constants::AJAX_BACKUP_PULL,
					'actionAoConnect'=> Agency_Pulse_Admin_Menu_Constants::AJAX_AO_CONNECT,
					'proActive'      => Agency_Pulse_Admin_Menu_License_Manager::is_active() ? 1 : 0,
					'licenseStatus'  => in_array( $license_state['status'], array( 'active', 'pending_email', 'slot_full' ), true )
						? $license_state['status']
						: 'inactive',
					'licenseSlotId'  => (int) $license_state['slot_id'],
					'i18n'           => array(
						'pushing'       => __( 'Saving to cloud…', 'agency-pulse-admin-menu' ),
						'pushSuccess'   => __( 'Backup saved to cloud.', 'agency-pulse-admin-menu' ),
						'pulling'       => __( 'Restoring from cloud…', 'agency-pulse-admin-menu' ),
						'pullSuccess'   => __( 'Configuration restored from cloud.', 'agency-pulse-admin-menu' ),
						'error'         => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'pullConfirm'   => __( 'This will replace all current configurations and role assignments with the cloud backup. Continue?', 'agency-pulse-admin-menu' ),
						'noFile'        => __( 'No file chosen', 'agency-pulse-admin-menu' ),
					),
					'aoI18n'         => array(
						'connecting' => __( 'Connecting…', 'agency-pulse-admin-menu' ),
						'checking'   => __( 'Checking…', 'agency-pulse-admin-menu' ),
					),
					'accessMode'         => Agency_Pulse_Admin_Menu_Access_Control::get_mode(),
					'canEnableCapOnly'   => Agency_Pulse_Admin_Menu_Access_Control::can_enable_cap_only() ? 1 : 0,
					'nonceSetMode'       => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SET_ACCESS_MODE ),
					'nonceGrant'         => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_GRANT_MANAGER_CAP ),
					'nonceRevoke'        => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_REVOKE_MANAGER_CAP ),
					'actionSetMode'      => Agency_Pulse_Admin_Menu_Constants::AJAX_SET_ACCESS_MODE,
					'actionGrant'        => Agency_Pulse_Admin_Menu_Constants::AJAX_GRANT_MANAGER_CAP,
					'actionRevoke'       => Agency_Pulse_Admin_Menu_Constants::AJAX_REVOKE_MANAGER_CAP,
					'acI18n'             => array(
						'applying'        => __( 'Applying…', 'agency-pulse-admin-menu' ),
						'applied'         => __( 'Mode applied.', 'agency-pulse-admin-menu' ),
						'error'           => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'grantSuccess'    => __( 'Permission granted.', 'agency-pulse-admin-menu' ),
						'revokeSuccess'   => __( 'Permission revoked.', 'agency-pulse-admin-menu' ),
						'usersWithAccess' => __( 'Users with explicit access:', 'agency-pulse-admin-menu' ),
						'noHolders'       => __( 'No users have been explicitly granted access yet.', 'agency-pulse-admin-menu' ),
						'revoke'          => __( 'Revoke', 'agency-pulse-admin-menu' ),
						'lockoutWarning'  => __( "You do not have an explicit permission grant. You will lose access to this settings page after applying. Are you sure?", 'agency-pulse-admin-menu' ),
						'lockoutReminder' => __( 'Mode applied. Grant yourself permission below to retain access before reloading.', 'agency-pulse-admin-menu' ),
					),
					'dashboardMsg'       => Agency_Pulse_Admin_Menu_Constants::OPTION_DASHBOARD_MESSAGE,
					'nonceDashboard'     => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_DASHBOARD_MSG ),
					'actionDashboard'    => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_DASHBOARD_MSG,
					'dmI18n'             => array(
						'saving' => __( 'Saving…', 'agency-pulse-admin-menu' ),
						'saved'  => __( 'Settings saved.', 'agency-pulse-admin-menu' ),
						'error'  => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
					),
					'nonceTlSave'   => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_SAVE ),
					'nonceTlList'   => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_LIST ),
					'nonceTlApply'  => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_APPLY ),
					'nonceTlDelete' => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_DELETE ),
					'actionTlSave'   => Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_SAVE,
					'actionTlList'   => Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_LIST,
					'actionTlApply'  => Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_APPLY,
					'actionTlDelete' => Agency_Pulse_Admin_Menu_Constants::AJAX_TEMPLATE_DELETE,
					'tlI18n'         => array(
						'saving'        => __( 'Saving…', 'agency-pulse-admin-menu' ),
						'saved'         => __( 'Template saved.', 'agency-pulse-admin-menu' ),
						'updated'       => __( 'Template updated.', 'agency-pulse-admin-menu' ),
						'nameRequired'  => __( 'Template name is required.', 'agency-pulse-admin-menu' ),
						'nameTaken'     => __( 'A template with this name already exists. Choose a different name.', 'agency-pulse-admin-menu' ),
						'loading'       => __( 'Loading templates…', 'agency-pulse-admin-menu' ),
						'loadError'     => __( 'Could not load templates.', 'agency-pulse-admin-menu' ),
						'empty'         => __( 'No templates saved yet. Save your current configuration above to start your library.', 'agency-pulse-admin-menu' ),
						'applying'      => __( 'Applying…', 'agency-pulse-admin-menu' ),
						'applyConfirm'  => __( 'Applying this template will replace all current configurations and role assignments. Continue?', 'agency-pulse-admin-menu' ),
						'deleting'      => __( 'Deleting…', 'agency-pulse-admin-menu' ),
						'deleteConfirm' => __( 'Delete template "%s"? This cannot be undone.', 'agency-pulse-admin-menu' ),
						'error'         => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
					),
					'nonceResetCustomization'  => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CUSTOMIZATION ),
					'actionResetCustomization' => Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_CUSTOMIZATION,
					'resetI18n'                => array(
						'resetting'       => __( 'Resetting…', 'agency-pulse-admin-menu' ),
						'success'         => __( 'Reset complete.', 'agency-pulse-admin-menu' ),
						'error'           => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'confirm'         => __( 'This will reset all admin customization settings to defaults. Continue?', 'agency-pulse-admin-menu' ),
						'confirmWithMenu' => __( 'This will reset all admin customization settings and affect saved menu data. Continue?', 'agency-pulse-admin-menu' ),
						'confirmNuke'     => __( 'This will delete all custom menu configurations and role assignments. Every user will see the default WordPress admin menu. This cannot be undone. Continue?', 'agency-pulse-admin-menu' ),
					),
				)
			);

			// Enqueue the wp_editor scripts so TinyMCE initialises on this page.
			wp_enqueue_editor();
		}

		// ── Customize page ────────────────────────────────────────────────────
		$is_customize_page = isset( $_GET['page'] ) // phpcs:ignore WordPress.Security.NonceVerification
			&& Agency_Pulse_Admin_Menu_Constants::PAGE_CUSTOMIZE === $_GET['page'];
		if ( $is_customize_page ) {
			// Color picker and Media Library live here, not on settings,
			// so the settings page footer scripts are no longer disrupted.
			wp_enqueue_style( 'wp-color-picker' );
			wp_enqueue_script( 'wp-color-picker' );
			wp_enqueue_media();

			wp_enqueue_style(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_CSS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/css/admin.css',
				array(),
				AGENCY_PULSE_ADMIN_MENU_VERSION
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_SETTINGS_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/settings.js',
				array( 'jquery', 'wp-color-picker', 'media-editor' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_COLLAPSE_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/collapse.js',
				array( 'jquery' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);
			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_SETTINGS_JS,
				'aamoSettingsData',
				array(
					'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
					'i18n'             => array(
						'error' => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
					),
					'nonceBranding'    => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_BRANDING_ICON ),
					'actionBranding'   => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_BRANDING_ICON,
					'nonceFooter'      => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ADMIN_FOOTER ),
					'actionFooter'     => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_ADMIN_FOOTER,
					'nonceColorSave'   => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_COLOR_SCHEME ),
					'actionColorSave'  => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_COLOR_SCHEME,
					'nonceColorReset'  => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_COLOR_SCHEME ),
					'actionColorReset' => Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_COLOR_SCHEME,
					'custI18n'         => array(
						'saving'       => __( 'Savingâ¦', 'agency-pulse-admin-menu' ),
						'saved'        => __( 'Settings saved.', 'agency-pulse-admin-menu' ),
						'error'        => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'colorSaved'   => __( 'Color scheme saved.', 'agency-pulse-admin-menu' ),
						'colorReset'   => __( 'Color scheme reset to defaults.', 'agency-pulse-admin-menu' ),
						'selectIcon'   => __( 'Select Icon', 'agency-pulse-admin-menu' ),
						'useThisImage' => __( 'Use This Image', 'agency-pulse-admin-menu' ),
					),
				)
			);
			wp_enqueue_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_LAYOUT_JS,
				AGENCY_PULSE_ADMIN_MENU_URL . 'assets/js/layout.js',
				array( 'jquery', 'wp-color-picker' ),
				AGENCY_PULSE_ADMIN_MENU_VERSION,
				false
			);
			$layout_opts = wp_parse_args(
				get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_LAYOUT, array() ),
				array(
					'enabled'             => false,
					'sidebar_style'       => 'default',
					'sidebar_item_radius' => 6,
					'content_bg'          => '#f0f0f1',
					'card_radius'         => 0,
					'card_shadow'         => false,
					'button_radius'       => 3,
					'input_radius'        => 4,
					'font_source'         => 'system',
					'font_family'         => '',
					'font_url'            => '',
				)
			);
			wp_localize_script(
				Agency_Pulse_Admin_Menu_Constants::ASSET_HANDLE_LAYOUT_JS,
				'aamoLayoutData',
				array(
					'ajaxUrl'           => admin_url( 'admin-ajax.php' ),
					'nonceSave'         => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_LAYOUT ),
					'nonceReset'        => wp_create_nonce( Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_LAYOUT ),
					'actionSave'        => Agency_Pulse_Admin_Menu_Constants::AJAX_SAVE_LAYOUT,
					'actionReset'       => Agency_Pulse_Admin_Menu_Constants::AJAX_RESET_LAYOUT,
						'sidebarStyle'      => $layout_opts['sidebar_style'],
					'sidebarItemRadius' => (int) $layout_opts['sidebar_item_radius'],
					'contentBg'         => $layout_opts['content_bg'],
					'cardRadius'        => (int) $layout_opts['card_radius'],
					'cardShadow'        => ! empty( $layout_opts['card_shadow'] ) ? 1 : 0,
					'buttonRadius'      => (int) $layout_opts['button_radius'],
					'inputRadius'       => (int) $layout_opts['input_radius'],
					'fontSource'        => $layout_opts['font_source'],
					'fontFamily'        => $layout_opts['font_family'],
					'fontUrl'           => $layout_opts['font_url'],
					'i18n'              => array(
						'saving'    => __( 'Saving…', 'agency-pulse-admin-menu' ),
						'saved'     => __( 'Layout settings saved.', 'agency-pulse-admin-menu' ),
						'resetting' => __( 'Resetting…', 'agency-pulse-admin-menu' ),
						'reset'     => __( 'Layout settings reset to defaults.', 'agency-pulse-admin-menu' ),
						'error'     => __( 'An error occurred. Please try again.', 'agency-pulse-admin-menu' ),
						'confirm'   => __( 'Reset all layout settings to defaults?', 'agency-pulse-admin-menu' ),
					),
				)
			);
		}
	}

	// -------------------------------------------------------------------------
	// Admin Bar Customizer — runtime
	// -------------------------------------------------------------------------

	/**
	 * Apply admin bar customizations: custom site name and/or logo icon.
	 *
	 * @since 1.0.0
	 * @param WP_Admin_Bar $wp_admin_bar
	 */
	public function apply_admin_bar_customizer( WP_Admin_Bar $wp_admin_bar ): void {
		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			return;
		}

		$opts = wp_parse_args(
			get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_BRANDING_ICON, array() ),
			array(
				'enabled'      => false,
				'icon_url'     => '',
				'icon_alt'     => '',
				'link_mode'    => 'wordpress',
				'link_url'     => '',
				'link_target'  => false,
				'hide_submenu' => false,
				'site_name'    => '',
			)
		);

		// ── Site name ────────────────────────────────────────────────────────
		if ( '' !== $opts['site_name'] ) {
			$node = $wp_admin_bar->get_node( 'site-name' );
			if ( $node ) {
				$node->title = esc_html( $opts['site_name'] );
				$wp_admin_bar->add_node( (array) $node );
			}
		}

		// ── Icon ─────────────────────────────────────────────────────────────
		if ( ! $opts['enabled'] || '' === $opts['icon_url'] ) {
			return;
		}

		$alt = '' !== $opts['icon_alt']
			? esc_attr( $opts['icon_alt'] )
			: esc_attr( $opts['site_name'] ?: get_bloginfo( 'name' ) );

		$img = sprintf(
			'<img src="%s" alt="%s" style="width:20px;height:20px;vertical-align:middle;" />',
			esc_url( $opts['icon_url'] ),
			$alt
		);

		$logo_node = $wp_admin_bar->get_node( 'wp-logo' );
		if ( $logo_node ) {
			// Set title (inside <a class="ab-item">) not meta['html'] (which
			// replaces the entire <li> content including the <a> wrapper and
			// loses the line-height/alignment that keeps the icon in the bar).
			$logo_node->title            = $img;
			$logo_node->meta['html']     = '';

			if ( 'none' === $opts['link_mode'] ) {
				$logo_node->href             = '';
				unset( $logo_node->meta['onclick'] );
			} elseif ( 'custom' === $opts['link_mode'] && '' !== $opts['link_url'] ) {
				$logo_node->href = esc_url( $opts['link_url'] );
				if ( $opts['link_target'] ) {
					$logo_node->meta['target'] = '_blank';
					$logo_node->meta['rel']    = 'noopener noreferrer';
				}
			}

			$wp_admin_bar->add_node( (array) $logo_node );
		}

		if ( $opts['hide_submenu'] ) {
			// remove_node() does not cascade to children — each node in
			// $_nodes must be removed individually. Groups are also removed
			// to suppress the menupop hover behaviour on the parent.
			// Node IDs confirmed from live HTML inspection of this WP build.
			foreach ( array(
				'wp-logo-default',   // group containing About + Get Involved
				'wp-logo-external',  // group containing Documentation, Support, etc.
				'about',             // About WordPress
				'contribute',        // Get Involved (actual node ID in this WP version)
				'get-involved',      // alternate ID used in some WP versions
				'wporg',             // WordPress.org
				'documentation',     // Documentation
				'support',           // Support
				'feedback',          // Feedback
			) as $node_id ) {
				$wp_admin_bar->remove_node( $node_id );
			}
		}
	}

	// -------------------------------------------------------------------------
	// Admin Footer Customizer — runtime
	// -------------------------------------------------------------------------

	/**
	 * Replace the left side of the admin footer bar.
	 *
	 * @since 1.0.0
	 * @param string $text Default WP footer text.
	 * @return string
	 */
	public function apply_admin_footer_left( string $text ): string {
		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			return $text;
		}
		$opts = wp_parse_args(
			get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, array() ),
			array( 'enabled' => false, 'left_content' => '' )
		);
		if ( empty( $opts['enabled'] ) ) {
			return $text;
		}
		return wp_kses_post( $opts['left_content'] );
	}

	/**
	 * Replace or preserve the right side of the admin footer bar.
	 *
	 * @since 1.0.0
	 * @param string $text    Default right-side text (WP version string).
	 * @param string $version WordPress version number.
	 * @return string
	 */
	public function apply_admin_footer_right( string $text, string $version ): string {
		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			return $text;
		}
		$opts = wp_parse_args(
			get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ADMIN_FOOTER, array() ),
			array( 'enabled' => false, 'show_version' => true, 'right_text' => '' )
		);
		if ( empty( $opts['enabled'] ) ) {
			return $text;
		}
		if ( ! empty( $opts['show_version'] ) ) {
			return $text;
		}
		return sanitize_text_field( $opts['right_text'] ?? '' );
	}

	// -------------------------------------------------------------------------
	// Color Customizer — runtime
	// -------------------------------------------------------------------------

	/**
	 * Inject the custom color palette as inline CSS on top of the active scheme.
	 *
	 * @since 1.0.0
	 */
	public function apply_color_scheme(): void {
		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			return;
		}
		$defaults = array(
			'enabled'           => false,
			'sidebar_bg'        => '#1d2327',
			'submenu_bg'        => '#2c3338',
			'submenu_text'      => '#a7aaad',
			'submenu_hl_text'   => '#ffffff',
			'sidebar_text'      => '#a7aaad',
			'sidebar_icon'      => '#a7aaad',
			'sidebar_highlight' => '#2271b1',
			'sidebar_hl_text'   => '#ffffff',
			'adminbar_bg'       => '#1d2327',
			'adminbar_text'     => '#a7aaad',
			'accent'            => '#2271b1',
			'bubble_bg'         => '#d63638',
			'bubble_text'       => '#ffffff',
		);
		$opts = wp_parse_args(
			get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, array() ),
			$defaults
		);
		wp_add_inline_style( 'colors', $this->build_color_css( $opts ) );
	}

	/**
	 * Build the inline CSS string from the stored color values.
	 *
	 * @since 1.0.0
	 * @param array $opts Validated color options array.
	 * @return string
	 */
	private function build_color_css( array $opts ): string {
		$d   = array(
			'sidebar_bg'        => '#1d2327',
			'submenu_bg'        => '#2c3338',
			'submenu_text'      => '#a7aaad',
			'submenu_hl_text'   => '#ffffff',
			'sidebar_text'      => '#a7aaad',
			'sidebar_icon'      => '#a7aaad',
			'sidebar_highlight' => '#2271b1',
			'sidebar_hl_text'   => '#ffffff',
			'adminbar_bg'       => '#1d2327',
			'adminbar_text'     => '#a7aaad',
			'accent'            => '#2271b1',
			'bubble_bg'         => '#d63638',
			'bubble_text'       => '#ffffff',
		);
		$sb  = sanitize_hex_color( $opts['sidebar_bg']        ?? '' ) ?: $d['sidebar_bg'];
		$smb = sanitize_hex_color( $opts['submenu_bg']        ?? '' ) ?: $d['submenu_bg'];
		$smt = sanitize_hex_color( $opts['submenu_text']      ?? '' ) ?: $d['submenu_text'];
		$smht = sanitize_hex_color( $opts['submenu_hl_text']  ?? '' ) ?: $d['submenu_hl_text'];
		$st  = sanitize_hex_color( $opts['sidebar_text']      ?? '' ) ?: $d['sidebar_text'];
		$si  = sanitize_hex_color( $opts['sidebar_icon']      ?? '' ) ?: $d['sidebar_icon'];
		$sh  = sanitize_hex_color( $opts['sidebar_highlight'] ?? '' ) ?: $d['sidebar_highlight'];
		$sht = sanitize_hex_color( $opts['sidebar_hl_text']   ?? '' ) ?: $d['sidebar_hl_text'];
		$ab  = sanitize_hex_color( $opts['adminbar_bg']       ?? '' ) ?: $d['adminbar_bg'];
		$at  = sanitize_hex_color( $opts['adminbar_text']     ?? '' ) ?: $d['adminbar_text'];
		$ac  = sanitize_hex_color( $opts['accent']            ?? '' ) ?: $d['accent'];
		$bb  = sanitize_hex_color( $opts['bubble_bg']         ?? '' ) ?: $d['bubble_bg'];
		$bt  = sanitize_hex_color( $opts['bubble_text']       ?? '' ) ?: $d['bubble_text'];

		return implode( '', array(
			"#adminmenu,#adminmenuwrap,#adminmenuback,body.folded #adminmenu{background:{$sb}}",
			"#adminmenu .wp-has-current-submenu .wp-submenu,#adminmenu .wp-has-current-submenu.opensub .wp-submenu,#adminmenu .wp-submenu,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu{background:{$smb}}",
			"#adminmenu .wp-has-current-submenu .wp-submenu a,#adminmenu .wp-has-current-submenu.opensub .wp-submenu a,#adminmenu .wp-submenu a,#adminmenu a.wp-has-current-submenu:focus+.wp-submenu a{color:{$smt}}",
			"#adminmenu a{color:{$st}}",
			"#adminmenu div.wp-menu-image::before,#adminmenu li div.wp-menu-image::before,#adminmenu li.wp-has-current-submenu div.wp-menu-image::before{color:{$si}}",
			"#adminmenu .wp-has-current-submenu .wp-menu-name,#adminmenu .current a.menu-top,#adminmenu li.wp-has-current-submenu,#adminmenu li a:hover,#adminmenu li a:focus{background:{$sh}}",
			"#adminmenu .wp-has-current-submenu .wp-menu-name,#adminmenu .current a.menu-top,#adminmenu li a:hover,#adminmenu li a:focus{color:{$sht}}",
			"#adminmenu .wp-submenu li.current a,#adminmenu .wp-submenu li.current a:hover,#adminmenu .wp-has-current-submenu .wp-submenu a:hover,#adminmenu .wp-submenu a:hover{color:{$smht}}",
			"#wpadminbar,#wpadminbar .ab-top-menu,#wpadminbar-root-default{background:{$ab}}",
			"#wpadminbar .ab-item,#wpadminbar .ab-top-menu>li>.ab-item{color:{$at}}",
			".button-primary,.wp-core-ui .button-primary{background:{$ac};border-color:{$ac}}",
			"#adminmenu .awaiting-mod,#adminmenu .update-plugins,.update-count,.plugin-count{background:{$bb}}",
			"#adminmenu .awaiting-mod,#adminmenu .update-plugins,.update-count,.plugin-count{color:{$bt}}",
		) );
	}

	// -------------------------------------------------------------------------
	// Settings page
	// -------------------------------------------------------------------------

	/**
	 * Render the Settings admin page.
	 *
	 * @since 1.0.0
	 */
	public function render_settings_page(): void {
		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'agency-pulse-admin-menu' ) );
		}
		$license_state = Agency_Pulse_Admin_Menu_License_Manager::get_state();
		include AGENCY_PULSE_ADMIN_MENU_DIR . 'templates/admin-settings.php';
	}

	// -------------------------------------------------------------------------
	// Customize page
	// -------------------------------------------------------------------------

	/**
	 * Render the Customize admin page (Admin Bar, Admin Footer, Color Customizer).
	 *
	 * @since 1.0.0
	 */
	public function render_customize_page(): void {
		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'agency-pulse-admin-menu' ) );
		}
		$ao_active = Agency_Pulse_Admin_Menu_License_Manager::is_active();
		include AGENCY_PULSE_ADMIN_MENU_DIR . 'templates/admin-customize.php';
	}

	/**
	 * Render the Admin Layout page.
	 *
	 * @since 1.0.0
	 */
	public function render_layout_page(): void {
		if ( ! current_user_can( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'agency-pulse-admin-menu' ) );
		}
		$ao_active = Agency_Pulse_Admin_Menu_License_Manager::is_active();
		include AGENCY_PULSE_ADMIN_MENU_DIR . 'templates/admin-layout.php';
	}

	/**
	 * Enqueue inline layout CSS on every admin page when layout styles are enabled.
	 *
	 * Runs at priority 1000 — after apply_color_scheme() at 999 — so layout CSS
	 * wins any specificity tie with the colour scheme output.
	 *
	 * @since 1.0.0
	 */
	public function apply_layout_styles(): void {
		if ( ! Agency_Pulse_Admin_Menu_License_Manager::is_active() ) {
			return;
		}
		$opts = wp_parse_args(
			get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_LAYOUT, array() ),
			array(
				'enabled'               => false,
				'sidebar_style_enabled' => true,
				'content_cards_enabled' => true,
				'interactive_enabled'   => true,
				'typography_enabled'    => true,
				'card_shadow'           => false,
				'card_shadow_opacity'   => 15,
				'card_shadow_offset'    => 2,
				'card_shadow_blur'      => 8,
				'font_source'           => 'system',
				'font_url'              => '',
			)
		);
		if ( 'custom' === ( $opts['font_source'] ?? 'system' ) ) {
			$font_url = $opts['font_url'] ?? '';
			if ( '' !== $font_url && 0 === strpos( $font_url, 'https://' ) ) {
				wp_enqueue_style( 'aamo-font', $font_url, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
			}
		}
		$css = $this->build_layout_css( $opts );
		if ( '' !== $css ) {
			wp_add_inline_style( 'colors', $css );
		}
	}

	/**
	 * Build the inline CSS string from the stored layout values.
	 *
	 * Covers all four layout phases: sidebar style, content & cards,
	 * interactive elements, and typography.
	 *
	 * @since 1.0.0
	 * @param array $opts Validated layout options array.
	 * @return string
	 */
	private function build_layout_css( array $opts ): string {
		$d = array(
			'sidebar_style_enabled' => true,
			'content_cards_enabled' => true,
			'interactive_enabled'   => true,
			'typography_enabled'    => true,
			'sidebar_style'         => 'default',
			'sidebar_item_radius'   => 6,
			'content_bg'            => '#f0f0f1',
			'card_radius'           => 0,
			'card_shadow'           => false,
			'card_shadow_opacity'   => 15,
			'card_shadow_offset'    => 2,
			'card_shadow_blur'      => 8,
			'button_radius'         => 3,
			'input_radius'          => 4,
			'font_source'           => 'system',
			'font_family'           => '',
		);
		$o   = wp_parse_args( $opts, $d );
		$css = array();

		// ── Sidebar style ─────────────────────────────────────────────────────
		if ( ! empty( $o['sidebar_style_enabled'] ) ) {
			$style = in_array( $o['sidebar_style'], array( 'default', 'border_accent', 'pill' ), true )
				? $o['sidebar_style'] : 'default';

			if ( 'border_accent' === $style ) {
				$color_opts = get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_COLOR_SCHEME, array() );
				$hl         = sanitize_hex_color( $color_opts['sidebar_highlight'] ?? '' ) ?: '#2271b1';
				$css[] = "#adminmenu li.current,#adminmenu li.wp-has-current-submenu{background:transparent}";
				$css[] = "#adminmenu li.current a.menu-top,#adminmenu li.wp-has-current-submenu a.menu-top,#adminmenu li.wp-has-current-submenu a.wp-has-current-submenu{background:transparent;border-left:3px solid {$hl}}";
			} elseif ( 'pill' === $style ) {
				$r = min( 24, max( 0, absint( $o['sidebar_item_radius'] ) ) );
				$css[] = "#adminmenu li.current,#adminmenu li.wp-has-current-submenu{background:transparent}";
				$css[] = "#adminmenu li.current a.menu-top,#adminmenu li.wp-has-current-submenu a.menu-top,#adminmenu li.wp-has-current-submenu a.wp-has-current-submenu{border-radius:{$r}px;margin:2px 8px;padding-left:8px}";
			}
		}

		// ── Content background + card styling ────────────────────────────────
		if ( ! empty( $o['content_cards_enabled'] ) ) {
			$bg    = sanitize_hex_color( $o['content_bg'] ) ?: $d['content_bg'];
			$css[] = "#wpcontent,#wpbody,#wpwrap{background:{$bg}}";

			$cr = min( 16, max( 0, absint( $o['card_radius'] ) ) );
			if ( $cr > 0 ) {
				$css[] = ".postbox,.card{border-radius:{$cr}px!important}";
			}
			if ( ! empty( $o['card_shadow'] ) ) {
				$so      = min( 50, max( 0, absint( $o['card_shadow_opacity'] ) ) );
				$sv      = min( 20, max( 0, absint( $o['card_shadow_offset'] ) ) );
				$sb      = min( 20, max( 0, absint( $o['card_shadow_blur'] ) ) );
				$opacity = round( $so / 100, 2 );
				$css[]   = ".postbox,.card{box-shadow:0 {$sv}px {$sb}px rgba(0,0,0,{$opacity})!important}";
			}
		}

		// ── Interactive elements (button + input radius) ──────────────────────
		if ( ! empty( $o['interactive_enabled'] ) ) {
			$br    = min( 12, max( 0, absint( $o['button_radius'] ) ) );
			$css[] = ".button,.button-primary,.button-secondary,.button-large,.button-small,input[type=submit],input[type=button]{border-radius:{$br}px!important}";

			$ir    = min( 12, max( 0, absint( $o['input_radius'] ) ) );
			$css[] = "input[type=text],input[type=email],input[type=url],input[type=password],input[type=search],input[type=number],select,textarea{border-radius:{$ir}px}";
		}

		// ── Typography ────────────────────────────────────────────────────────
		if ( ! empty( $o['typography_enabled'] ) && 'custom' === $o['font_source'] && '' !== $o['font_family'] ) {
			$css[] = "body.wp-admin,body.wp-admin *{font-family:" . $o['font_family'] . "} .dashicons,.dashicons-before::before{font-family:dashicons!important}";
		}

		return implode( '', $css );
	}

	// -------------------------------------------------------------------------
	// Utilities
	// -------------------------------------------------------------------------

	/**
	 * Return the plugin version string.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public function get_version(): string {
		return AGENCY_PULSE_ADMIN_MENU_VERSION;
	}

	public function init_update_checker(): void {
		if ( did_action( 'aamo_update_checker_init' ) > 0 ) {
			return;
		}
		do_action( 'aamo_update_checker_init' );

		$autoload_file = AGENCY_PULSE_ADMIN_MENU_DIR . 'vendor/autoload.php';
		if ( ! file_exists( $autoload_file ) ) {
			error_log( '[AAMO] Composer autoloader not found. Run: composer install' );
			return;
		}
		require_once $autoload_file;

		try {
			$config_file = AGENCY_PULSE_ADMIN_MENU_DIR . 'config/update-checker.php';
			if ( ! file_exists( $config_file ) ) {
				error_log( '[AAMO] Update Checker config not found: ' . $config_file );
				return;
			}

			$config = new \AgencyPulse\UpdateChecker\Config(
				AGENCY_PULSE_ADMIN_MENU_FILE,
				$config_file
			);
			new \AgencyPulse\UpdateChecker\UpdateChecker(
				AGENCY_PULSE_ADMIN_MENU_FILE,
				$config
			);
		} catch ( \Exception $e ) {
			error_log( '[AAMO] Update Checker Error: ' . $e->getMessage() );
		}
	}
}
