<?php

declare(strict_types=1);

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

final class CRToday_Plugin_Settings
{
	private const OPTION_NAME        = 'crtoday_plugin_source_config';
	private const LOCAL_SOURCES_KEY  = 'crtoday_local_sources';
	private const BUILTIN_SOURCE_METRICS_OPTION = 'crtoday_builtin_source_metrics';
	private const PAGE_SLUG          = 'crtoday-plugin-settings';
	private const CACHE_PREFIX       = 'crtoday_src_';
	private const CACHE_PREFIXES     = [
		'crtoday_src_',
		'crtoday_source_currencies_',
		'crtoday_active_',
	];
	public const  CRON_HOOK          = 'crtoday_background_fetch';
	private const MAX_SOURCES        = 3;

	/* ── Deal label constants ── */
	private const DEAL_LABEL_MID_MARKET      = 'Mid-market';
	private const DEAL_LABEL_CASH            = 'Cash';
	private const DEAL_LABEL_TRANSFER        = 'Transfer';
	private const DEAL_LABEL_REFERENCE       = 'Reference';
	private const DEAL_LABEL_PARALLEL_MARKET = 'Parallel market';

	private const API_BASE = 'https://crtoday.fex.to/api/v1/';

	/* ── Deterministic source registry ── */

	/** @return array<string, array{label:string, emoji:string, base:string, deals:array}> */
	private static function sources_registry(): array {
		/** @var array|null */
		static $cache;
		if ($cache !== null) {
			return $cache;
		}

		$api = self::API_BASE;
		$ref = fn(string $file): array => [
			'STANDARD' => ['label' => self::DEAL_LABEL_REFERENCE, 'market' => true, 'endpoint' => $api . $file],
		];

		return $cache = [
			'FEX' => [
				'label' => 'CurrencyRate',
				'emoji' => "\u{1F30D}",
				'base'  => 'USD',
				'deals' => [
					'STANDARD' => ['label' => self::DEAL_LABEL_MID_MARKET, 'market' => true, 'endpoint' => $api . 'FEX.json'],
				],
			],
			'UAPB' => [
				'label' => 'PrivatBank',
				'emoji' => "\u{1F1FA}\u{1F1E6}",
				'base'  => 'UAH',
				'deals' => [
					'NOTE'     => ['label' => self::DEAL_LABEL_CASH,     'market' => true, 'endpoint' => $api . 'UAPB-NOTE.json'],
					'TRANSFER' => ['label' => self::DEAL_LABEL_TRANSFER, 'market' => true, 'endpoint' => $api . 'UAPB-TRANSFER.json'],
				],
			],
			'BLUELYTICS' => [
				'label' => 'Dolar Blue',
				'emoji' => "\u{1F1E6}\u{1F1F7}",
				'base'  => 'ARS',
				'deals' => [
					'STANDARD' => ['label' => self::DEAL_LABEL_PARALLEL_MARKET, 'market' => true, 'endpoint' => $api . 'BLUELYTICS.json'],
				],
			],
			'EUCB' => ['label' => 'ECB',  'name' => 'European Central Bank',  'emoji' => "\u{1F1EA}\u{1F1FA}", 'base' => 'EUR', 'deals' => $ref('EUCB.json')],
			'AURB' => ['label' => 'RBA',  'name' => 'Reserve Bank of Australia', 'emoji' => "\u{1F1E6}\u{1F1FA}", 'base' => 'AUD', 'deals' => $ref('AURB.json')],
			'UANB' => ['label' => 'NBU',  'name' => 'National Bank of Ukraine', 'emoji' => "\u{1F1FA}\u{1F1E6}", 'base' => 'UAH', 'deals' => $ref('UANB.json')],
			'ARCB' => ['label' => 'BCRA', 'name' => 'Central Bank of Argentina', 'emoji' => "\u{1F1E6}\u{1F1F7}", 'base' => 'ARS', 'deals' => $ref('ARCB.json')],
			'NGCB' => ['label' => 'CBN',  'name' => 'Central Bank of Nigeria', 'emoji' => "\u{1F1F3}\u{1F1EC}", 'base' => 'NGN', 'deals' => $ref('NGCB.json')],
			'BCB'  => ['label' => 'BCB',  'name' => 'Central Bank of Brazil', 'emoji' => "\u{1F1E7}\u{1F1F7}", 'base' => 'BRL', 'deals' => $ref('BCB.json')],
			'CZCB' => ['label' => 'CNB',  'name' => 'Czech National Bank', 'emoji' => "\u{1F1E8}\u{1F1FF}", 'base' => 'CZK', 'deals' => $ref('CZCB.json')],
			'PLNB' => ['label' => 'NBP',  'name' => 'National Bank of Poland', 'emoji' => "\u{1F1F5}\u{1F1F1}", 'base' => 'PLN', 'deals' => $ref('PLNB.json')],
			'ROBN' => ['label' => 'NBR',  'name' => 'National Bank of Romania', 'emoji' => "\u{1F1F7}\u{1F1F4}", 'base' => 'RON', 'deals' => $ref('ROBN.json')],
			'GBBE' => ['label' => 'BoE',  'name' => 'Bank of England', 'emoji' => "\u{1F1EC}\u{1F1E7}", 'base' => 'GBP', 'deals' => $ref('GBBE.json')],
			'ILBL' => ['label' => 'BoI',  'name' => 'Bank of Israel', 'emoji' => "\u{1F1EE}\u{1F1F1}", 'base' => 'ILS', 'deals' => $ref('ILBL.json')],
			'THBT' => ['label' => 'BoT',  'name' => 'Bank of Thailand', 'emoji' => "\u{1F1F9}\u{1F1ED}", 'base' => 'THB', 'deals' => $ref('THBT.json')],
		];
	}

	private const ALLOWED_RETENTION    = [2, 7, 30, 90];
	private const ALLOWED_API_INTERVAL = [15, 30, 60];
	private const ALLOWED_ROUNDING     = ['half_up', 'half_even', 'down'];
	private const CURRENCY_SYMBOL_FALLBACKS = [
		'AFN' => '؋',
		'XCD' => '$',
		'KHR' => '៛',
		'XAF' => 'FCFA',
		'AUD' => '$',
		'EUR' => '€',
		'XOF' => 'CFA',
		'CUP' => '$',
		'BDT' => '৳',
		'BRL' => 'R$',
		'GEL' => '₾',
		'HTG' => 'G',
		'INR' => '₹',
		'ILS' => '₪',
		'KZT' => '₸',
		'KWD' => 'د.ك',
		'LSL' => 'L',
		'MYR' => 'RM',
		'MUR' => '₨',
		'MNT' => '₮',
		'MMK' => 'K',
		'XPF' => '₣',
		'WST' => 'WS$',
		'TZS' => 'TSh',
		'NOK' => 'kr',
		'RSD' => 'RSD',
		'TND' => 'د.ت',
		'UZS' => 'so\'m',
		'RWF' => 'FRw',
		'SEK' => 'kr',
		'UAH' => '₴',
		'DZD' => 'دج',
		'ARS' => '$',
		'AZN' => '₼',
		'BYN' => 'Br',
		'BOB' => 'Bs',
		'CAD' => 'C$',
		'CNY' => '¥',
		'CRC' => '₡',
		'CZK' => 'Kč',
		'USD' => '$',
		'HKD' => 'HK$',
		'JPY' => '¥',
		'CHF' => 'CHF',
		'GBP' => '£',
		'VES' => 'Bs.S',
		'TRY' => '₺',
		'TMT' => 'm',
		'ZMW' => 'ZK',
		'MZN' => 'MT',
	];

	/*
	 * ── Deterministic currency registry ──
	 *
	 * SINGLE SOURCE OF TRUTH for every currency the plugin supports.
	 * All other layers (TypeScript, block editor, shortcode generator,
	 * local-source dialog) derive their data from this method.
	 *
	 * Structure:  CODE => ['name' => translated name, 'flag' => flag file stem, 'group' => region key]
	 *
	 * • 'name'  – wrapped in __() directly here, so wp i18n make-pot can
	 *             extract strings statically AND consumers get pre-translated
	 *             values with zero extra effort.  No separate POT file needed.
	 * • 'flag'  – lowercase filename stem inside assets/flags/webp/128/<flag>.webp.
	 *             For country currencies the value equals the ISO 3166-1 alpha-2
	 *             code in lowercase.  For non-country assets (XAU, BTC …) it is
	 *             the descriptive file name (gold, bitcoin …).
	 * • 'group' – regional grouping key used by the converter dropdown.
	 *             One of: north_america, europe, asia, middle_east, south_america,
	 *             africa, oceania, caribbean, precious_metals, international,
	 *             cryptocurrency, virtual.
	 *
	 * To add a new currency:
	 *   1. Add an entry here – this is the ONLY place that needs editing.
	 *   2. Place the flag image in assets/flags/webp/128/<flag>.webp.
	 *   3. Run `wp i18n make-pot` (or `make i18n`) to update the POT file.
	 *
	 * Contract: docs/contracts/currency-registry.md
	 */
	private static function currencies(): array
	{
		/** @var array<string,array<string,array{name:string,flag:string,group:string}>> */
		static $cache = [];
		$locale = determine_locale();
		if (isset($cache[$locale])) {
			return $cache[$locale];
		}
		return $cache[$locale] = [
			'AED' => ['name' => __('UAE Dirham', 'currencyrate-today-currency-blocks'), 'flag' => 'ae', 'group' => 'middle_east'],
			'AFN' => ['name' => __('Afghani', 'currencyrate-today-currency-blocks'), 'flag' => 'af', 'group' => 'asia'],
			'ALL' => ['name' => __('Lek', 'currencyrate-today-currency-blocks'), 'flag' => 'al', 'group' => 'europe'],
			'AMD' => ['name' => __('Armenian Dram', 'currencyrate-today-currency-blocks'), 'flag' => 'am', 'group' => 'europe'],
			'AOA' => ['name' => __('Kwanza', 'currencyrate-today-currency-blocks'), 'flag' => 'ao', 'group' => 'africa'],
			'ARS' => ['name' => __('Argentine Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'ar', 'group' => 'south_america'],
			'AUD' => ['name' => __('Australian Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'au', 'group' => 'oceania'],
			'AWG' => ['name' => __('Aruban Florin', 'currencyrate-today-currency-blocks'), 'flag' => 'aw', 'group' => 'caribbean'],
			'AZN' => ['name' => __('Azerbaijan Manat', 'currencyrate-today-currency-blocks'), 'flag' => 'az', 'group' => 'europe'],
			'BAM' => ['name' => __('Convertible Mark', 'currencyrate-today-currency-blocks'), 'flag' => 'ba', 'group' => 'europe'],
			'BBD' => ['name' => __('Barbados Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'bb', 'group' => 'caribbean'],
			'BDT' => ['name' => __('Taka', 'currencyrate-today-currency-blocks'), 'flag' => 'bd', 'group' => 'asia'],
			'BHD' => ['name' => __('Bahraini Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'bh', 'group' => 'middle_east'],
			'BIF' => ['name' => __('Burundi Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'bi', 'group' => 'africa'],
			'BMD' => ['name' => __('Bermudian Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'bm', 'group' => 'north_america'],
			'BND' => ['name' => __('Brunei Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'bn', 'group' => 'asia'],
			'BOB' => ['name' => __('Boliviano', 'currencyrate-today-currency-blocks'), 'flag' => 'bo', 'group' => 'south_america'],
			'BRL' => ['name' => __('Brazilian Real', 'currencyrate-today-currency-blocks'), 'flag' => 'br', 'group' => 'south_america'],
			'BSD' => ['name' => __('Bahamian Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'bs', 'group' => 'caribbean'],
			'BTC' => ['name' => __('Bitcoin', 'currencyrate-today-currency-blocks'), 'flag' => 'bitcoin', 'group' => 'cryptocurrency'],
			'BTN' => ['name' => __('Ngultrum', 'currencyrate-today-currency-blocks'), 'flag' => 'bt', 'group' => 'asia'],
			'BWP' => ['name' => __('Pula', 'currencyrate-today-currency-blocks'), 'flag' => 'bw', 'group' => 'africa'],
			'BYN' => ['name' => __('Belarusian Ruble', 'currencyrate-today-currency-blocks'), 'flag' => 'by', 'group' => 'europe'],
			'BZD' => ['name' => __('Belize Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'bz', 'group' => 'north_america'],
			'CAD' => ['name' => __('Canadian Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'ca', 'group' => 'north_america'],
			'CDF' => ['name' => __('Congolese Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'cd', 'group' => 'africa'],
			'CHF' => ['name' => __('Swiss Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'ch', 'group' => 'europe'],
			'CLF' => ['name' => __('CLF', 'currencyrate-today-currency-blocks'), 'flag' => 'cl', 'group' => 'south_america'],
			'CLP' => ['name' => __('Chilean Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'cl', 'group' => 'south_america'],
			'CNH' => ['name' => __('Chinese Yuan (Offshore)', 'currencyrate-today-currency-blocks'), 'flag' => 'cn', 'group' => 'asia'],
			'CNY' => ['name' => __('Yuan Renminbi', 'currencyrate-today-currency-blocks'), 'flag' => 'cn', 'group' => 'asia'],
			'COP' => ['name' => __('Colombian Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'co', 'group' => 'south_america'],
			'CRC' => ['name' => __('Costa Rican Colon', 'currencyrate-today-currency-blocks'), 'flag' => 'cr', 'group' => 'north_america'],
			'CUP' => ['name' => __('Cuban Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'cu', 'group' => 'caribbean'],
			'CVE' => ['name' => __('Cabo Verde Escudo', 'currencyrate-today-currency-blocks'), 'flag' => 'cv', 'group' => 'africa'],
			'CZK' => ['name' => __('Czech Koruna', 'currencyrate-today-currency-blocks'), 'flag' => 'cz', 'group' => 'europe'],
			'DJF' => ['name' => __('Djibouti Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'dj', 'group' => 'africa'],
			'DKK' => ['name' => __('Danish Krone', 'currencyrate-today-currency-blocks'), 'flag' => 'dk', 'group' => 'europe'],
			'DOGE' => ['name' => __('Dogecoin', 'currencyrate-today-currency-blocks'), 'flag' => 'dogecoin', 'group' => 'cryptocurrency'],
			'DOP' => ['name' => __('Dominican Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'do', 'group' => 'caribbean'],
			'DZD' => ['name' => __('Algerian Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'dz', 'group' => 'africa'],
			'EGP' => ['name' => __('Egyptian Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'eg', 'group' => 'middle_east'],
			'ERN' => ['name' => __('Nakfa', 'currencyrate-today-currency-blocks'), 'flag' => 'er', 'group' => 'africa'],
			'ETB' => ['name' => __('Ethiopian Birr', 'currencyrate-today-currency-blocks'), 'flag' => 'et', 'group' => 'africa'],
			'ETH' => ['name' => __('Ethereum', 'currencyrate-today-currency-blocks'), 'flag' => 'ethereum', 'group' => 'cryptocurrency'],
			'EUR' => ['name' => __('Euro', 'currencyrate-today-currency-blocks'), 'flag' => 'eu', 'group' => 'europe'],
			'FJD' => ['name' => __('Fiji Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'fj', 'group' => 'oceania'],
			'FKP' => ['name' => __('Falkland Islands Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'fk', 'group' => 'south_america'],
			'GBP' => ['name' => __('Pound Sterling', 'currencyrate-today-currency-blocks'), 'flag' => 'gb', 'group' => 'europe'],
			'GEL' => ['name' => __('Lari', 'currencyrate-today-currency-blocks'), 'flag' => 'ge', 'group' => 'europe'],
			'GGP' => ['name' => __('Guernsey pound', 'currencyrate-today-currency-blocks'), 'flag' => 'gg', 'group' => 'europe'],
			'GHS' => ['name' => __('Ghana Cedi', 'currencyrate-today-currency-blocks'), 'flag' => 'gh', 'group' => 'africa'],
			'GIP' => ['name' => __('Gibraltar Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'gi', 'group' => 'europe'],
			'GMD' => ['name' => __('Dalasi', 'currencyrate-today-currency-blocks'), 'flag' => 'gm', 'group' => 'africa'],
			'GNF' => ['name' => __('Guinean Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'gn', 'group' => 'africa'],
			'GTQ' => ['name' => __('Quetzal', 'currencyrate-today-currency-blocks'), 'flag' => 'gt', 'group' => 'north_america'],
			'GYD' => ['name' => __('Guyana Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'gy', 'group' => 'south_america'],
			'HKD' => ['name' => __('Hong Kong Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'hk', 'group' => 'asia'],
			'HNL' => ['name' => __('Lempira', 'currencyrate-today-currency-blocks'), 'flag' => 'hn', 'group' => 'north_america'],
			'HRK' => ['name' => __('Croatian Kuna', 'currencyrate-today-currency-blocks'), 'flag' => 'hr', 'group' => 'europe'],
			'HTG' => ['name' => __('Gourde', 'currencyrate-today-currency-blocks'), 'flag' => 'ht', 'group' => 'caribbean'],
			'HUF' => ['name' => __('Forint', 'currencyrate-today-currency-blocks'), 'flag' => 'hu', 'group' => 'europe'],
			'IDR' => ['name' => __('Rupiah', 'currencyrate-today-currency-blocks'), 'flag' => 'id', 'group' => 'asia'],
			'ILS' => ['name' => __('New Israeli Sheqel', 'currencyrate-today-currency-blocks'), 'flag' => 'il', 'group' => 'middle_east'],
			'IMP' => ['name' => __('IMP', 'currencyrate-today-currency-blocks'), 'flag' => 'im', 'group' => 'europe'],
			'INR' => ['name' => __('Indian Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'in', 'group' => 'asia'],
			'IQD' => ['name' => __('Iraqi Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'iq', 'group' => 'middle_east'],
			'IRR' => ['name' => __('Iranian Rial', 'currencyrate-today-currency-blocks'), 'flag' => 'ir', 'group' => 'middle_east'],
			'ISK' => ['name' => __('Iceland Krona', 'currencyrate-today-currency-blocks'), 'flag' => 'is', 'group' => 'europe'],
			'JEP' => ['name' => __('Jersey Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'je', 'group' => 'europe'],
			'JMD' => ['name' => __('Jamaican Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'jm', 'group' => 'caribbean'],
			'JOD' => ['name' => __('Jordanian Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'jo', 'group' => 'middle_east'],
			'JPY' => ['name' => __('Yen', 'currencyrate-today-currency-blocks'), 'flag' => 'jp', 'group' => 'asia'],
			'KES' => ['name' => __('Kenyan Shilling', 'currencyrate-today-currency-blocks'), 'flag' => 'ke', 'group' => 'africa'],
			'KGS' => ['name' => __('Som', 'currencyrate-today-currency-blocks'), 'flag' => 'kg', 'group' => 'asia'],
			'KHR' => ['name' => __('Riel', 'currencyrate-today-currency-blocks'), 'flag' => 'kh', 'group' => 'asia'],
			'KMF' => ['name' => __('Comorian Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'km', 'group' => 'africa'],
			'KPW' => ['name' => __('North Korean Won', 'currencyrate-today-currency-blocks'), 'flag' => 'kp', 'group' => 'asia'],
			'KRW' => ['name' => __('Won', 'currencyrate-today-currency-blocks'), 'flag' => 'kr', 'group' => 'asia'],
			'KWD' => ['name' => __('Kuwaiti Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'kw', 'group' => 'middle_east'],
			'KYD' => ['name' => __('Cayman Islands Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'ky', 'group' => 'caribbean'],
			'KZT' => ['name' => __('Tenge', 'currencyrate-today-currency-blocks'), 'flag' => 'kz', 'group' => 'asia'],
			'LAK' => ['name' => __('Lao Kip', 'currencyrate-today-currency-blocks'), 'flag' => 'la', 'group' => 'asia'],
			'LBP' => ['name' => __('Lebanese Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'lb', 'group' => 'middle_east'],
			'LD' => ['name' => __('Linden Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'ld', 'group' => 'virtual'],
			'LKR' => ['name' => __('Sri Lanka Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'lk', 'group' => 'asia'],
			'LRD' => ['name' => __('Liberian Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'lr', 'group' => 'africa'],
			'LSL' => ['name' => __('Loti', 'currencyrate-today-currency-blocks'), 'flag' => 'ls', 'group' => 'africa'],
			'LYD' => ['name' => __('Libyan Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'ly', 'group' => 'africa'],
			'MAD' => ['name' => __('Moroccan Dirham', 'currencyrate-today-currency-blocks'), 'flag' => 'ma', 'group' => 'africa'],
			'MDL' => ['name' => __('Moldovan Leu', 'currencyrate-today-currency-blocks'), 'flag' => 'md', 'group' => 'europe'],
			'MGA' => ['name' => __('Malagasy Ariary', 'currencyrate-today-currency-blocks'), 'flag' => 'mg', 'group' => 'africa'],
			'MKD' => ['name' => __('Denar', 'currencyrate-today-currency-blocks'), 'flag' => 'mk', 'group' => 'europe'],
			'MMK' => ['name' => __('Kyat', 'currencyrate-today-currency-blocks'), 'flag' => 'mm', 'group' => 'asia'],
			'MNT' => ['name' => __('Tugrik', 'currencyrate-today-currency-blocks'), 'flag' => 'mn', 'group' => 'asia'],
			'MOP' => ['name' => __('Pataca', 'currencyrate-today-currency-blocks'), 'flag' => 'mo', 'group' => 'asia'],
			'MRU' => ['name' => __('Ouguiya', 'currencyrate-today-currency-blocks'), 'flag' => 'mr', 'group' => 'africa'],
			'MUR' => ['name' => __('Mauritius Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'mu', 'group' => 'africa'],
			'MVR' => ['name' => __('Rufiyaa', 'currencyrate-today-currency-blocks'), 'flag' => 'mv', 'group' => 'asia'],
			'MWK' => ['name' => __('Malawi Kwacha', 'currencyrate-today-currency-blocks'), 'flag' => 'mw', 'group' => 'africa'],
			'MXN' => ['name' => __('Mexican Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'mx', 'group' => 'north_america'],
			'MYR' => ['name' => __('Malaysian Ringgit', 'currencyrate-today-currency-blocks'), 'flag' => 'my', 'group' => 'asia'],
			'MZN' => ['name' => __('Mozambique Metical', 'currencyrate-today-currency-blocks'), 'flag' => 'mz', 'group' => 'africa'],
			'NAD' => ['name' => __('Namibia Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'na', 'group' => 'africa'],
			'NGN' => ['name' => __('Naira', 'currencyrate-today-currency-blocks'), 'flag' => 'ng', 'group' => 'africa'],
			'NIO' => ['name' => __('Cordoba Oro', 'currencyrate-today-currency-blocks'), 'flag' => 'ni', 'group' => 'north_america'],
			'NOK' => ['name' => __('Norwegian Krone', 'currencyrate-today-currency-blocks'), 'flag' => 'no', 'group' => 'europe'],
			'NPR' => ['name' => __('Nepalese Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'np', 'group' => 'asia'],
			'NZD' => ['name' => __('New Zealand Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'nz', 'group' => 'oceania'],
			'OMR' => ['name' => __('Rial Omani', 'currencyrate-today-currency-blocks'), 'flag' => 'om', 'group' => 'middle_east'],
			'PAB' => ['name' => __('Balboa', 'currencyrate-today-currency-blocks'), 'flag' => 'pa', 'group' => 'north_america'],
			'PEN' => ['name' => __('Sol', 'currencyrate-today-currency-blocks'), 'flag' => 'pe', 'group' => 'south_america'],
			'PGK' => ['name' => __('Kina', 'currencyrate-today-currency-blocks'), 'flag' => 'pg', 'group' => 'oceania'],
			'PHP' => ['name' => __('Philippine Peso', 'currencyrate-today-currency-blocks'), 'flag' => 'ph', 'group' => 'asia'],
			'PKR' => ['name' => __('Pakistan Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'pk', 'group' => 'asia'],
			'PLN' => ['name' => __('Zloty', 'currencyrate-today-currency-blocks'), 'flag' => 'pl', 'group' => 'europe'],
			'PYG' => ['name' => __('Guarani', 'currencyrate-today-currency-blocks'), 'flag' => 'py', 'group' => 'south_america'],
			'QAR' => ['name' => __('Qatari Rial', 'currencyrate-today-currency-blocks'), 'flag' => 'qa', 'group' => 'middle_east'],
			'RON' => ['name' => __('Romanian Leu', 'currencyrate-today-currency-blocks'), 'flag' => 'ro', 'group' => 'europe'],
			'RSD' => ['name' => __('Serbian Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'rs', 'group' => 'europe'],
			'RUB' => ['name' => __('Russian Ruble', 'currencyrate-today-currency-blocks'), 'flag' => 'ru', 'group' => 'europe'],
			'RWF' => ['name' => __('Rwanda Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'rw', 'group' => 'africa'],
			'SAR' => ['name' => __('Saudi Riyal', 'currencyrate-today-currency-blocks'), 'flag' => 'sa', 'group' => 'middle_east'],
			'SBD' => ['name' => __('Solomon Islands Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'sb', 'group' => 'oceania'],
			'SCR' => ['name' => __('Seychelles Rupee', 'currencyrate-today-currency-blocks'), 'flag' => 'sc', 'group' => 'africa'],
			'SDG' => ['name' => __('Sudanese Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'sd', 'group' => 'africa'],
			'SEK' => ['name' => __('Swedish Krona', 'currencyrate-today-currency-blocks'), 'flag' => 'se', 'group' => 'europe'],
			'SGD' => ['name' => __('Singapore Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'sg', 'group' => 'asia'],
			'SHP' => ['name' => __('Saint Helena Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'sh', 'group' => 'africa'],
			'SLE' => ['name' => __('Leone', 'currencyrate-today-currency-blocks'), 'flag' => 'sl', 'group' => 'africa'],
			'SOS' => ['name' => __('Somali Shilling', 'currencyrate-today-currency-blocks'), 'flag' => 'so', 'group' => 'africa'],
			'SPL' => ['name' => __('Seborgan Luigini', 'currencyrate-today-currency-blocks'), 'flag' => 'sbg', 'group' => 'europe'],
			'SRD' => ['name' => __('Surinam Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'sr', 'group' => 'south_america'],
			'SSP' => ['name' => __('South Sudanese Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'ss', 'group' => 'africa'],
			'STN' => ['name' => __('Dobra', 'currencyrate-today-currency-blocks'), 'flag' => 'st', 'group' => 'africa'],
			'SVC' => ['name' => __('El Salvador Colon', 'currencyrate-today-currency-blocks'), 'flag' => 'sv', 'group' => 'north_america'],
			'SYP' => ['name' => __('Syrian Pound', 'currencyrate-today-currency-blocks'), 'flag' => 'sy', 'group' => 'middle_east'],
			'SZL' => ['name' => __('Lilangeni', 'currencyrate-today-currency-blocks'), 'flag' => 'sz', 'group' => 'africa'],
			'THB' => ['name' => __('Baht', 'currencyrate-today-currency-blocks'), 'flag' => 'th', 'group' => 'asia'],
			'TJS' => ['name' => __('Somoni', 'currencyrate-today-currency-blocks'), 'flag' => 'tj', 'group' => 'asia'],
			'TMT' => ['name' => __('Turkmenistan New Manat', 'currencyrate-today-currency-blocks'), 'flag' => 'tm', 'group' => 'asia'],
			'TND' => ['name' => __('Tunisian Dinar', 'currencyrate-today-currency-blocks'), 'flag' => 'tn', 'group' => 'africa'],
			'TOP' => ['name' => __('Pa’anga', 'currencyrate-today-currency-blocks'), 'flag' => 'to', 'group' => 'oceania'],
			'TRY' => ['name' => __('Turkish Lira', 'currencyrate-today-currency-blocks'), 'flag' => 'tr', 'group' => 'europe'],
			'TTD' => ['name' => __('Trinidad and Tobago Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'tt', 'group' => 'caribbean'],
			'TVD' => ['name' => __('Tuvaluan Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'tv', 'group' => 'oceania'],
			'TWD' => ['name' => __('New Taiwan Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'tw', 'group' => 'asia'],
			'TZS' => ['name' => __('Tanzanian Shilling', 'currencyrate-today-currency-blocks'), 'flag' => 'tz', 'group' => 'africa'],
			'UAH' => ['name' => __('Hryvnia', 'currencyrate-today-currency-blocks'), 'flag' => 'ua', 'group' => 'europe'],
			'UGX' => ['name' => __('Uganda Shilling', 'currencyrate-today-currency-blocks'), 'flag' => 'ug', 'group' => 'africa'],
			'USD' => ['name' => __('US Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'us', 'group' => 'north_america'],
			'UYU' => ['name' => __('Peso Uruguayo', 'currencyrate-today-currency-blocks'), 'flag' => 'uy', 'group' => 'south_america'],
			'UZS' => ['name' => __('Uzbekistan Sum', 'currencyrate-today-currency-blocks'), 'flag' => 'uz', 'group' => 'asia'],
			'VED' => ['name' => __('Venezuelan Digital Bolívar', 'currencyrate-today-currency-blocks'), 'flag' => 've', 'group' => 'south_america'],
			'VES' => ['name' => __('Venezuelan Bolívar Soberano', 'currencyrate-today-currency-blocks'), 'flag' => 've', 'group' => 'south_america'],
			'VND' => ['name' => __('Dong', 'currencyrate-today-currency-blocks'), 'flag' => 'vn', 'group' => 'asia'],
			'VUV' => ['name' => __('Vatu', 'currencyrate-today-currency-blocks'), 'flag' => 'vu', 'group' => 'oceania'],
			'WST' => ['name' => __('Tala', 'currencyrate-today-currency-blocks'), 'flag' => 'ws', 'group' => 'oceania'],
			'XAF' => ['name' => __('CFA Franc BEAC', 'currencyrate-today-currency-blocks'), 'flag' => 'cm', 'group' => 'international'],
			'XAG' => ['name' => __('Silver (troy oz)', 'currencyrate-today-currency-blocks'), 'flag' => 'silver', 'group' => 'precious_metals'],
			'XAU' => ['name' => __('Gold (troy oz)', 'currencyrate-today-currency-blocks'), 'flag' => 'gold', 'group' => 'precious_metals'],
			'XCD' => ['name' => __('East Caribbean Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'ag', 'group' => 'caribbean'],
			'XCG' => ['name' => __('Caribbean Guilder', 'currencyrate-today-currency-blocks'), 'flag' => 'cw', 'group' => 'caribbean'],
			'XDR' => ['name' => __('Special Drawing Rights', 'currencyrate-today-currency-blocks'), 'flag' => 'un', 'group' => 'international'],
			'XOF' => ['name' => __('CFA Franc BCEAO', 'currencyrate-today-currency-blocks'), 'flag' => 'sn', 'group' => 'international'],
			'XPD' => ['name' => __('Palladium (troy oz)', 'currencyrate-today-currency-blocks'), 'flag' => 'palladium', 'group' => 'precious_metals'],
			'XPF' => ['name' => __('CFP Franc', 'currencyrate-today-currency-blocks'), 'flag' => 'pf', 'group' => 'international'],
			'XPT' => ['name' => __('Platinum (troy oz)', 'currencyrate-today-currency-blocks'), 'flag' => 'platinum', 'group' => 'precious_metals'],
			'YER' => ['name' => __('Yemeni Rial', 'currencyrate-today-currency-blocks'), 'flag' => 'ye', 'group' => 'middle_east'],
			'ZAR' => ['name' => __('Rand', 'currencyrate-today-currency-blocks'), 'flag' => 'za', 'group' => 'africa'],
			'ZMW' => ['name' => __('Zambian Kwacha', 'currencyrate-today-currency-blocks'), 'flag' => 'zm', 'group' => 'africa'],
			'ZWG' => ['name' => __('Zimbabwe Gold', 'currencyrate-today-currency-blocks'), 'flag' => 'zw', 'group' => 'africa'],
			'ZWL' => ['name' => __('Zimbabwean Dollar', 'currencyrate-today-currency-blocks'), 'flag' => 'zw', 'group' => 'africa'],
		];
	}

	private static function rounding_labels(): array {
		return [
			'half_up'   => __('Standard (half-up)', 'currencyrate-today-currency-blocks'),
			'half_even' => __('Banker\'s (half-even)', 'currencyrate-today-currency-blocks'),
			'down'      => __('Truncate (floor)', 'currencyrate-today-currency-blocks'),
		];
	}

	/* ================================================================
	 * Currency registry – public API
	 * ================================================================ */

	/** Return the flag file stem for a currency (e.g. 'us', 'gold'), or null. */
	public static function get_currency_flag(string $code): ?string
	{
		return self::currencies()[$code]['flag'] ?? null;
	}

	/** Return the translated display name for a currency, or the code itself as fallback. */
	public static function get_currency_name(string $code): string
	{
		return self::currencies()[$code]['name'] ?? $code;
	}

	/**
	 * Return currency symbol resolved via native intl first, then fallback map.
	 */
	public static function get_currency_symbol(string $code): ?string
	{
		$normalized = strtoupper(trim($code));
		if ($normalized === '') {
			return null;
		}

		$native = self::resolve_native_currency_symbol($normalized);
		if ($native !== null) {
			return $native;
		}

		return self::CURRENCY_SYMBOL_FALLBACKS[$normalized] ?? null;
	}

	private static function resolve_native_currency_symbol(string $code): ?string
	{
		if (! class_exists('NumberFormatter')) {
			return null;
		}

		try {
			$formatter = new \NumberFormatter('en', \NumberFormatter::CURRENCY);
			if (! $formatter->setTextAttribute(\NumberFormatter::CURRENCY_CODE, $code)) {
				return null;
			}

			$symbol = trim((string) $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL));
			if ($symbol === '' || $symbol === '¤' || strtoupper($symbol) === $code) {
				return null;
			}

			return $symbol;
		} catch (\Throwable $e) {
			return null;
		}
	}

	/**
	 * Place a currency symbol next to a pre-formatted numeric value,
	 * respecting the current WordPress locale.
	 *
	 * Examples (en_US): "$1,234.56"   —  symbol before the number.
	 * Examples (ru_RU): "1 234,56 ₽"  —  symbol after the number.
	 *
	 * @param string $formatted Already-formatted numeric string.
	 * @param string $code      ISO 4217 currency code.
	 * @return string           Value with symbol placed per locale.
	 */
	public static function format_with_symbol( string $formatted, string $code ): string {
		$symbol = self::get_currency_symbol( $code );
		if ( $symbol === null || $symbol === '' ) {
			return $formatted;
		}
		if ( self::is_symbol_before( $code ) ) {
			return $symbol . $formatted;
		}
		// Use non-breaking space between value and trailing symbol.
		return $formatted . "\u{00A0}" . $symbol;
	}

	/** @var array<string, bool> Cached per-currency results for symbol position. */
	private static array $symbol_position_cache = [];

	/**
	 * Determine whether the currency symbol is placed before the number
	 * in the current WordPress locale (via ICU / NumberFormatter).
	 */
	private static function is_symbol_before( string $code ): bool {
		$code = strtoupper( $code );
		if ( isset( self::$symbol_position_cache[ $code ] ) ) {
			return self::$symbol_position_cache[ $code ];
		}

		$result = true; // default: prefix
		if ( class_exists( 'NumberFormatter' ) ) {
			try {
				$fmt = new \NumberFormatter( get_locale(), \NumberFormatter::CURRENCY );
				$out = $fmt->formatCurrency( 9.99, $code );
				if ( $out !== false && preg_match( '/\d/', $out, $m, PREG_OFFSET_CAPTURE ) ) {
					$result = $m[0][1] > 0;
				}
			} catch ( \Throwable $e ) {
				// keep default
			}
		}

		self::$symbol_position_cache[ $code ] = $result;
		return $result;
	}

	/**
	 * Return currency group keys in deterministic UI order.
	 *
	 * @return list<string>
	 */
	public static function get_currency_group_order(): array
	{
		return [
			'north_america',
			'europe',
			'asia',
			'middle_east',
			'south_america',
			'africa',
			'oceania',
			'caribbean',
			'precious_metals',
			'international',
			'cryptocurrency',
			'virtual',
		];
	}

	/**
	 * Return translated labels for all currency groups.
	 *
	 * @param bool $include_recent Include the synthetic "recent" group label used by frontend dropdown.
	 * @return array<string,string>
	 */
	public static function get_currency_group_labels(bool $include_recent = false): array
	{
		$labels = [
			'north_america'  => __('North America', 'currencyrate-today-currency-blocks'),
			'europe'         => __('Europe', 'currencyrate-today-currency-blocks'),
			'asia'           => __('Asia', 'currencyrate-today-currency-blocks'),
			'middle_east'    => __('Middle East', 'currencyrate-today-currency-blocks'),
			'south_america'  => __('South America', 'currencyrate-today-currency-blocks'),
			'africa'         => __('Africa', 'currencyrate-today-currency-blocks'),
			'oceania'        => __('Oceania', 'currencyrate-today-currency-blocks'),
			'caribbean'      => __('Caribbean', 'currencyrate-today-currency-blocks'),
			'precious_metals'=> __('Precious Metals', 'currencyrate-today-currency-blocks'),
			'international'  => __('International', 'currencyrate-today-currency-blocks'),
			'cryptocurrency' => __('Cryptocurrency', 'currencyrate-today-currency-blocks'),
			'virtual'        => __('Virtual', 'currencyrate-today-currency-blocks'),
		];

		if (! $include_recent) {
			return $labels;
		}

		return ['recent' => __('Recently used', 'currencyrate-today-currency-blocks')] + $labels;
	}

	/**
	 * Return the full registry for the block-editor / admin JS.
	 * Names are already translated via __().
	 *
	 * @return list<array{code:string,name:string,flag:string,group:string}>
	 */
	public static function get_currencies_for_editor(): array
	{
		$list = [];
		foreach (self::currencies() as $code => $meta) {
			$list[] = [
				'code'  => $code,
				'name'  => $meta['name'],
				'flag'  => $meta['flag'],
				'group' => $meta['group'],
			];
		}
		return $list;
	}

	/**
	 * Shared accent theme presets for all rendering surfaces.
	 *
	 * @return array<string,array{light:array{primary:string,secondary:string,tertiary:string},dark:array{primary:string,secondary:string,tertiary:string}}>
	 */
	public static function get_accent_theme_presets(): array
	{
		static $presets = null;

		if (is_array($presets)) {
			return $presets;
		}

		$presets = [
			'default' => [
				'light' => ['primary' => '#6366f1', 'secondary' => '#a855f7', 'tertiary' => '#ec4899'],
				'dark'  => ['primary' => '#6369b2', 'secondary' => '#7466a5', 'tertiary' => '#8f6a8a'],
			],
			'ocean' => [
				'light' => ['primary' => '#0ea5e9', 'secondary' => '#2563eb', 'tertiary' => '#4f46e5'],
				'dark'  => ['primary' => '#4a8faa', 'secondary' => '#5a82a8', 'tertiary' => '#6a7d9e'],
			],
			'forest' => [
				'light' => ['primary' => '#16a34a', 'secondary' => '#059669', 'tertiary' => '#0d9488'],
				'dark'  => ['primary' => '#4a946f', 'secondary' => '#4a917e', 'tertiary' => '#5a9189'],
			],
			'sunset' => [
				'light' => ['primary' => '#f59e0b', 'secondary' => '#f97316', 'tertiary' => '#ef4444'],
				'dark'  => ['primary' => '#b38f52', 'secondary' => '#b07c5e', 'tertiary' => '#a9716f'],
			],
			'rose' => [
				'light' => ['primary' => '#f43f5e', 'secondary' => '#ec4899', 'tertiary' => '#fb7185'],
				'dark'  => ['primary' => '#b27082', 'secondary' => '#ab7390', 'tertiary' => '#b28693'],
			],
			'amber' => [
				'light' => ['primary' => '#d97706', 'secondary' => '#f59e0b', 'tertiary' => '#fbbf24'],
				'dark'  => ['primary' => '#a8844f', 'secondary' => '#b09161', 'tertiary' => '#b89f78'],
			],
			'teal' => [
				'light' => ['primary' => '#0f766e', 'secondary' => '#14b8a6', 'tertiary' => '#22d3ee'],
				'dark'  => ['primary' => '#4a8f88', 'secondary' => '#5b958f', 'tertiary' => '#6a9a98'],
			],
			'cyan' => [
				'light' => ['primary' => '#0891b2', 'secondary' => '#06b6d4', 'tertiary' => '#22d3ee'],
				'dark'  => ['primary' => '#4d90a4', 'secondary' => '#5e98aa', 'tertiary' => '#729fb0'],
			],
			'emerald' => [
				'light' => ['primary' => '#059669', 'secondary' => '#10b981', 'tertiary' => '#34d399'],
				'dark'  => ['primary' => '#4b9278', 'secondary' => '#5a9a86', 'tertiary' => '#6ea394'],
			],
			'violet' => [
				'light' => ['primary' => '#7c3aed', 'secondary' => '#8b5cf6', 'tertiary' => '#a78bfa'],
				'dark'  => ['primary' => '#756fae', 'secondary' => '#867db5', 'tertiary' => '#988fbf'],
			],
			'crimson' => [
				'light' => ['primary' => '#b91c1c', 'secondary' => '#dc2626', 'tertiary' => '#f87171'],
				'dark'  => ['primary' => '#a56366', 'secondary' => '#af7073', 'tertiary' => '#bb8282'],
			],
			'slate' => [
				'light' => ['primary' => '#334155', 'secondary' => '#475569', 'tertiary' => '#64748b'],
				'dark'  => ['primary' => '#7f8fa4', 'secondary' => '#9ca9ba', 'tertiary' => '#b6c1cf'],
			],
		];

		return $presets;
	}

	/* ================================================================
	 * Boot
	 * ================================================================ */

	public static function register(): void
	{
		add_action('admin_menu', [self::class, 'register_admin_page']);
		add_action('admin_init', [self::class, 'register_settings']);
		add_action('admin_post_crtoday_invalidate_cache', [self::class, 'handle_invalidate_cache']);
		add_action('admin_post_crtoday_invalidate_expired', [self::class, 'handle_invalidate_expired']);
		add_action('admin_post_crtoday_force_fetch', [self::class, 'handle_force_fetch']);
		add_action('admin_post_crtoday_delete_data', [self::class, 'handle_delete_data']);
		add_action('admin_post_crtoday_save_local_source', [self::class, 'handle_save_local_source']);
		add_action('admin_post_crtoday_delete_local_source', [self::class, 'handle_delete_local_source']);
		add_action('admin_post_crtoday_refresh_local_source', [self::class, 'handle_refresh_local_source']);
		add_action('wp_ajax_crtoday_refresh_local_source', [self::class, 'handle_ajax_refresh_local_source']);
		add_action('wp_ajax_crtoday_debug_action', [self::class, 'handle_debug_action']);
		add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_assets']);
		add_action(self::CRON_HOOK, [self::class, 'handle_background_fetch']);
	}

	/**
	 * WP-Cron handler: fetch rates for all active source+deal pairs.
	 *
	 * Ensures historical data continuity even when no visitors hit the site.
	 */
	public static function handle_background_fetch(): void
	{
		$pairs = self::get_active_source_deals();
		if ($pairs === []) {
			return;
		}

		foreach ($pairs as $pair) {
			CRToday_Source_Cache::resolve($pair['source_id'], $pair['deal']);
		}
	}

	public static function option_name(): string
	{
		return self::OPTION_NAME;
	}

	private static function translate_deal_label(string $label): string
	{
		$translations = [
			self::DEAL_LABEL_MID_MARKET      => __('Mid-market', 'currencyrate-today-currency-blocks'),
			self::DEAL_LABEL_CASH            => __('Cash', 'currencyrate-today-currency-blocks'),
			self::DEAL_LABEL_TRANSFER        => __('Transfer', 'currencyrate-today-currency-blocks'),
			self::DEAL_LABEL_REFERENCE       => __('Reference', 'currencyrate-today-currency-blocks'),
			self::DEAL_LABEL_PARALLEL_MARKET => __('Parallel market', 'currencyrate-today-currency-blocks'),
		];

		return $translations[$label] ?? $label;
	}

	/* ================================================================
	 * Source resolution — unified API for built-in + local
	 * ================================================================ */

	/**
	 * Resolve a source by ID and deal.
	 * Returns metadata for built-in or local source, or null if not found.
	 */
	public static function resolve_source(string $source_id, string $deal = 'STANDARD'): ?array
	{
		$source_id = strtoupper($source_id);
		$deal      = strtoupper($deal);

		/* ── Built-in source ── */
		if (isset(self::sources_registry()[$source_id])) {
			$meta = self::sources_registry()[$source_id];
			if (! isset($meta['deals'][$deal])) {
				$deal = array_key_first($meta['deals']);
			}
			$deal_meta = $meta['deals'][$deal];

			return [
				'type'       => 'builtin',
				'source_id'  => $source_id,
				'deal'       => $deal,
				'label'      => (string) $meta['label'],
				'name'       => (string) ($meta['name'] ?? $meta['label']),
				'deal_label' => self::translate_deal_label((string) $deal_meta['label']),
				'base'       => $meta['base'],
				'is_market'  => (bool) ($deal_meta['market'] ?? false),
				'endpoint'   => $deal_meta['endpoint'],
			];
		}

		/* ── Local source ── */
		if (substr($source_id, 0, 6) === 'LOCAL_') {
			$local_id = (int) substr($source_id, 6);
			$locals   = self::get_local_sources();
			if (isset($locals[$local_id])) {
				$ls = $locals[$local_id];
				return [
					'type'         => 'local',
					'source_id'    => $source_id,
					'deal'         => 'STANDARD',
					'label'        => $ls['name'],
					'deal_label'   => __('Manual', 'currencyrate-today-currency-blocks'),
					'base'         => $ls['base'],
					'rates'        => $ls['rates'],
					'is_market'    => (($ls['pricing_mode'] ?? 'mid') === 'bidask'),
					'pricing_mode' => $ls['pricing_mode'],
					'updated_at'   => $ls['updated_at'],
				];
			}
		}

		return null;
	}

	/**
	 * Build the complete list of available sources for dropdowns.
	 * Returns array of [ 'id' => 'FEX:STANDARD', 'label' => '...', 'group' => 'builtin'|'local' ]
	 */
	public static function get_all_source_options(): array
	{
		$options = [];

		/* Built-in */
		foreach (self::sources_registry() as $id => $meta) {
			foreach ($meta['deals'] as $deal_id => $deal_meta) {
				$emoji = isset($meta['emoji']) ? $meta['emoji'] . ' ' : '';
				$options[] = [
					'id'    => $id . ':' . $deal_id,
					'label' => $emoji . (string) $meta['label'] . ' — ' . self::translate_deal_label((string) $deal_meta['label']) . ' (' . $meta['base'] . ')',
					'group' => 'builtin',
				];
			}
		}

		/* Local */
		foreach (self::get_local_sources() as $local_id => $ls) {
			$options[] = [
				'id'    => 'LOCAL_' . $local_id . ':STANDARD',
				'label' => $ls['name'] . ' (' . $ls['base'] . ')',
				'group' => 'local',
			];
		}

		return $options;
	}

	/* ================================================================
	 * Source currency availability map
	 * ================================================================ */

	/**
	 * Build a map of available currency codes per source+deal.
	 * Used by the editor to indicate which currencies are actually available.
	 *
	 * @return array<string, string[]>  Keys like "FEX:STANDARD", values are sorted currency code arrays.
	 */
	public static function get_source_currencies_map(): array
	{
		$map = [];

		/* Built-in sources: read from cached transients. */
		foreach (self::sources_registry() as $id => $meta) {
			foreach ($meta['deals'] as $deal_id => $deal_meta) {
				$key = $id . ':' . $deal_id;
				$cached = get_transient('crtoday_source_currencies_' . strtolower($id . '_' . $deal_id));
				if (is_array($cached) && $cached !== []) {
					$map[$key] = $cached;
				}
			}
		}

		/* Local sources: derive from stored rates. */
		foreach (self::get_local_sources() as $local_id => $ls) {
			$key = 'LOCAL_' . $local_id . ':STANDARD';
			$codes = array_map('strtoupper', array_keys($ls['rates'] ?? []));
			$base = strtoupper((string) ($ls['base'] ?? ''));
			if ($base !== '' && ! in_array($base, $codes, true)) {
				$codes[] = $base;
			}
			sort($codes);
			$map[$key] = $codes;
		}

		return $map;
	}

	/* ================================================================
	 * Local sources CRUD
	 * ================================================================ */

	/** @return array<int, array{name:string,base:string,pricing_mode:string,rates:array,updated_at:int}> */
	public static function get_local_sources(): array
	{
		$data = get_option(self::LOCAL_SOURCES_KEY, []);
		return is_array($data) ? $data : [];
	}

	public static function save_local_source(int $id, array $source): void
	{
		$all = self::get_local_sources();
		$all[$id] = $source;
		update_option(self::LOCAL_SOURCES_KEY, $all, false);
	}

	public static function delete_local_source(int $id): void
	{
		$all = self::get_local_sources();
		unset($all[$id]);
		update_option(self::LOCAL_SOURCES_KEY, $all, false);
	}

	public static function next_local_id(): int
	{
		$all = self::get_local_sources();
		if ($all === []) {
			return 1;
		}
		return max(array_keys($all)) + 1;
	}

	/**
	 * Persist local source rates into the rates DB table so they appear
	 * in Storage Analytics and support historical snapshots.
	 */
	private static function persist_local_source_to_db(int $id, array $source): void
	{
		$source_id = 'LOCAL_' . $id;
		$base      = $source['base'] ?? '';
		$rates     = $source['rates'] ?? [];
		$now       = $source['updated_at'] ?? time();

		if ($base === '' || ! is_array($rates) || $rates === []) {
			return;
		}

		/* Build a payload compatible with save_remote_payload(). */
		$payload_rates = [];
		foreach ($rates as $code => $entry) {
			if (! is_array($entry)) {
				continue;
			}
			$mid = (float) ($entry['mid'] ?? 0);
			$bid = (float) ($entry['bid'] ?? 0);
			$ask = (float) ($entry['ask'] ?? 0);
			$payload_rates[$code] = [
				'mid' => $mid > 0 ? $mid : null,
				'bid' => $bid > 0 ? $bid : null,
				'ask' => $ask > 0 ? $ask : null,
			];
		}

		CRToday_Raw_Snapshot_Repository::save_remote_payload(
			$source_id,
			'STANDARD',
			$base,
			['rates' => $payload_rates],
			$now,
			$now,
			1440 /* manual sources: 1 day interval */
		);
	}

	/* ================================================================
	 * Config accessor
	 * ================================================================ */

	public static function get_config(): array
	{
		$defaults = [
			'retention'      => 2,
			'api_interval'   => 15,
			'rounding_mode'  => 'half_even',
		];

		$stored = get_option(self::OPTION_NAME, []);
		if (! is_array($stored)) {
			$stored = [];
		}

		$config = wp_parse_args($stored, $defaults);

		$retention = (int) $config['retention'];
		if (! in_array($retention, self::ALLOWED_RETENTION, true)) {
			$retention = $defaults['retention'];
		}

		$api_interval = (int) $config['api_interval'];
		if (! in_array($api_interval, self::ALLOWED_API_INTERVAL, true)) {
			$api_interval = $defaults['api_interval'];
		}

		$rounding_mode = (string) ($config['rounding_mode'] ?? 'half_even');
		if (! in_array($rounding_mode, self::ALLOWED_ROUNDING, true)) {
			$rounding_mode = $defaults['rounding_mode'];
		}

		$history_scope = (string) ($config['history_scope'] ?? 'all');
		if (! in_array($history_scope, ['all', 'active'], true)) {
			$history_scope = 'all';
		}

		return [
			'retention'         => $retention,
			'api_interval'      => $api_interval,
			'rounding_mode'     => $rounding_mode,
			'history_scope'     => $history_scope,
		];
	}

	/* ================================================================
	 * Active source+deal scanner
	 * ================================================================ */

	/**
	 * Collect unique source+deal pairs used in published blocks and shortcodes.
	 *
	 * Scans published posts/pages/wp_block for crtoday blocks and shortcodes,
	 * extracts sourceId + deal from attributes. Only builtin sources are returned
	 * (local sources are manually managed and don't need background fetching).
	 *
	 * Result is cached in a transient for 1 hour.
	 *
	 * @return array<int, array{source_id: string, deal: string}>
	 */
	public static function get_active_source_deals(): array
	{
		$transient_key = 'crtoday_active_source_deals';
		$cached = get_transient($transient_key);
		if (is_array($cached)) {
			return $cached;
		}

		$pairs = [];

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time scan; result cached via transient.
		$rows = $wpdb->get_col(
			"SELECT post_content FROM {$wpdb->posts}
			 WHERE post_status = 'publish'
			   AND post_type IN ('post','page','wp_block')
			   AND post_content LIKE '%crtoday%'"
		);

		$block_names = [
			'crtoday/exchange-rates',
			'crtoday/currency-converter',
			'crtoday/rate-card',
			'crtoday/ticker',
			'crtoday/price-badge',
		];

		$shortcode_names = [
			'crtoday_exchange_rates',
			'crtoday_currency_converter',
			'crtoday_converter',
			'crtoday_rate_card',
			'crtoday_ticker',
			'crtoday_price_badge',
		];
		$sc_pattern = get_shortcode_regex($shortcode_names);

		foreach ($rows as $content) {
			/* Gutenberg blocks */
			$blocks = parse_blocks($content);
			self::extract_source_deals_from_blocks($blocks, $block_names, $pairs);

			/* Shortcodes */
			if (preg_match_all('/' . $sc_pattern . '/', $content, $matches, PREG_SET_ORDER)) {
				foreach ($matches as $m) {
					$atts = shortcode_parse_atts($m[3] ?? '');
					if (is_array($atts)) {
						self::extract_source_deal_from_shortcode_atts($atts, $pairs);
					}
				}
			}
		}

		/* Deduplicate */
		$unique = [];
		foreach ($pairs as $p) {
			$key = $p['source_id'] . ':' . $p['deal'];
			if (! isset($unique[$key]) && isset(self::sources_registry()[$p['source_id']])) {
				$unique[$key] = $p;
			}
		}

		$result = array_values($unique);
		set_transient($transient_key, $result, HOUR_IN_SECONDS);

		return $result;
	}

	/**
	 * Recursively extract source+deal pairs from parsed blocks.
	 *
	 * @param array<string,string>[] $pairs Collected pairs (mutated).
	 */
	private static function extract_source_deals_from_blocks(array $blocks, array $block_names, array &$pairs): void
	{
		foreach ($blocks as $block) {
			if (! empty($block['blockName']) && in_array($block['blockName'], $block_names, true)) {
				$a = $block['attrs'] ?? [];
				$pairs[] = [
					'source_id' => strtoupper((string) ($a['sourceId'] ?? 'FEX')),
					'deal'      => strtoupper((string) ($a['deal'] ?? 'STANDARD')),
				];
			}
			if (! empty($block['innerBlocks'])) {
				self::extract_source_deals_from_blocks($block['innerBlocks'], $block_names, $pairs);
			}
		}
	}

	/**
	 * Extract source+deal from shortcode attributes.
	 *
	 * Shortcodes use the compound `source="FEX:STANDARD"` format.
	 *
	 * @param array<string,string>[] $pairs Collected pairs (mutated).
	 */
	private static function extract_source_deal_from_shortcode_atts(array $atts, array &$pairs): void
	{
		$source_param = (string) ($atts['source'] ?? '');
		if ($source_param !== '') {
			$parsed = CRToday_Source_Selection::parse_compound_source($source_param);
			$pairs[] = [
				'source_id' => strtoupper($parsed['sourceId']),
				'deal'      => strtoupper($parsed['deal']),
			];
		} else {
			/* Default source when not specified */
			$pairs[] = [
				'source_id' => 'FEX',
				'deal'      => 'STANDARD',
			];
		}
	}

	/* ================================================================
	 * Active currency scanner
	 * ================================================================ */

	/**
	 * Collect currency codes actually used in published blocks and shortcodes.
	 *
	 * Result is cached in a transient for 1 hour so that each fetch cycle
	 * does not re-scan all posts.
	 *
	 * @return string[]|null Uppercase currency codes, or null when scope is 'all'.
	 */
	public static function get_active_currency_codes(): ?array
	{
		$config = self::get_config();
		if ($config['history_scope'] !== 'active') {
			return null;
		}

		$transient_key = 'crtoday_active_currencies';
		$cached = get_transient($transient_key);
		if (is_array($cached)) {
			return $cached;
		}

		$codes = [];

		/* ── Gutenberg blocks + shortcodes from published content ── */
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time scan for active currencies; result cached via transient.
		$rows = $wpdb->get_col(
			"SELECT post_content FROM {$wpdb->posts}
			 WHERE post_status = 'publish'
			   AND post_type IN ('post','page','wp_block')
			   AND post_content LIKE '%crtoday%'"
		);

		$block_names = [
			'crtoday/exchange-rates',
			'crtoday/currency-converter',
			'crtoday/rate-card',
			'crtoday/ticker',
			'crtoday/price-badge',
		];

		$shortcode_names = [
			'crtoday_exchange_rates',
			'crtoday_currency_converter',
			'crtoday_converter',
			'crtoday_rate_card',
			'crtoday_ticker',
			'crtoday_price_badge',
		];
		$sc_pattern = get_shortcode_regex($shortcode_names);

		foreach ($rows as $content) {
			/* Gutenberg blocks */
			$blocks = parse_blocks($content);
			self::extract_currency_codes_from_blocks($blocks, $block_names, $codes);

			/* Shortcodes */
			if (preg_match_all('/' . $sc_pattern . '/', $content, $matches, PREG_SET_ORDER)) {
				foreach ($matches as $m) {
					$atts = shortcode_parse_atts($m[3] ?? '');
					if (is_array($atts)) {
						self::extract_codes_from_shortcode_atts($atts, $codes);
					}
				}
			}
		}

		/* ── Widget template instances (legacy) ── */
		$widget_instances = get_option('widget_crtoday_widget', []);
		if (is_array($widget_instances)) {
			foreach ($widget_instances as $inst) {
				if (is_array($inst)) {
					self::extract_codes_from_shortcode_atts($inst, $codes);
				}
			}
		}

		$codes = array_values(array_unique(array_filter($codes)));
		sort($codes);

		set_transient($transient_key, $codes, HOUR_IN_SECONDS);

		return $codes;
	}

	/**
	 * Recursively extract currency codes from parsed blocks.
	 */
	private static function extract_currency_codes_from_blocks(array $blocks, array $block_names, array &$codes): void
	{
		foreach ($blocks as $block) {
			if (! empty($block['blockName']) && in_array($block['blockName'], $block_names, true)) {
				self::collect_codes_from_attrs($block['attrs'] ?? [], $codes);
			}
			if (! empty($block['innerBlocks'])) {
				self::extract_currency_codes_from_blocks($block['innerBlocks'], $block_names, $codes);
			}
		}
	}

	/**
	 * Extract currency codes from block attributes (unified for all 5 blocks).
	 */
	private static function collect_codes_from_attrs(array $a, array &$codes): void
	{
		/* Exchange Rates */
		if (isset($a['base']) && is_string($a['base'])) {
			$codes[] = strtoupper($a['base']);
		}
		if (isset($a['symbols']) && is_array($a['symbols'])) {
			foreach ($a['symbols'] as $s) {
				$codes[] = strtoupper((string) $s);
			}
		}

		/* Converter */
		if (isset($a['fromCurrency']) && is_string($a['fromCurrency'])) {
			$codes[] = strtoupper($a['fromCurrency']);
		}
		if (isset($a['toCurrency']) && is_string($a['toCurrency'])) {
			$codes[] = strtoupper($a['toCurrency']);
		}

		/* Rate Card */
		if (isset($a['from']) && is_string($a['from'])) {
			$codes[] = strtoupper($a['from']);
		}
		if (isset($a['to']) && is_string($a['to'])) {
			$codes[] = strtoupper($a['to']);
		}

		/* Ticker */
		if (isset($a['pairs']) && is_array($a['pairs'])) {
			foreach ($a['pairs'] as $pair) {
				if (is_array($pair)) {
					if (isset($pair['from'])) {
						$codes[] = strtoupper((string) $pair['from']);
					}
					if (isset($pair['to'])) {
						$codes[] = strtoupper((string) $pair['to']);
					}
				}
			}
		}

		/* Price Badge */
		if (isset($a['primaryCurrency']) && is_string($a['primaryCurrency'])) {
			$codes[] = strtoupper($a['primaryCurrency']);
		}
		if (isset($a['targetCurrencies']) && is_array($a['targetCurrencies'])) {
			foreach ($a['targetCurrencies'] as $t) {
				$codes[] = strtoupper((string) $t);
			}
		}
	}

	/**
	 * Extract currency codes from shortcode/widget attributes.
	 */
	private static function extract_codes_from_shortcode_atts(array $atts, array &$codes): void
	{
		/* Array/CSV currency lists */
		foreach (['symbols', 'target_currencies', 'targetCurrencies', 'targets'] as $key) {
			if (isset($atts[$key]) && is_string($atts[$key])) {
				foreach (explode(',', $atts[$key]) as $c) {
					$c = strtoupper(trim($c));
					if ($c !== '') {
						$codes[] = $c;
					}
				}
			} elseif (isset($atts[$key]) && is_array($atts[$key])) {
				foreach ($atts[$key] as $c) {
					$codes[] = strtoupper(trim((string) $c));
				}
			}
		}

		/* Single currency attrs */
		foreach (['base', 'from', 'to', 'fromCurrency', 'toCurrency', 'from_currency', 'to_currency', 'primary', 'primaryCurrency', 'primary_currency'] as $key) {
			if (isset($atts[$key]) && is_string($atts[$key])) {
				$c = strtoupper(trim($atts[$key]));
				if ($c !== '' && preg_match('/^[A-Z]{3}$/', $c)) {
					$codes[] = $c;
				}
			}
		}

		/* pairs="EUR/USD,GBP/JPY" */
		if (isset($atts['pairs']) && is_string($atts['pairs'])) {
			foreach (explode(',', $atts['pairs']) as $pair) {
				foreach (explode('/', trim($pair)) as $p) {
					$p = strtoupper(trim($p));
					if ($p !== '' && preg_match('/^[A-Z]{3}$/', $p)) {
						$codes[] = $p;
					}
				}
			}
		} elseif (isset($atts['pairs']) && is_array($atts['pairs'])) {
			foreach ($atts['pairs'] as $pair) {
				if (is_array($pair)) {
					if (isset($pair['from'])) {
						$codes[] = strtoupper(trim((string) $pair['from']));
					}
					if (isset($pair['to'])) {
						$codes[] = strtoupper(trim((string) $pair['to']));
					}
				}
			}
		}
	}

	/**
	 * Dashboard-only scanner: collect active currencies split into base and target groups.
	 *
	 * Unlike get_active_currency_codes() (used by history filtering), this always scans
	 * regardless of the history_scope setting, and separates base currencies from targets.
	 *
	 * @return array{base: string[], target: string[]}
	 */
	private static function get_active_currency_codes_for_dashboard(): array
	{
		$base_codes   = [];
		$target_codes = [];

		/* ── Gutenberg blocks + shortcodes from published content ── */
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard scan for active currencies overview; no persistent cache needed.
		$rows = $wpdb->get_col(
			"SELECT post_content FROM {$wpdb->posts}
			 WHERE post_status = 'publish'
			   AND post_type IN ('post','page','wp_block')
			   AND post_content LIKE '%crtoday%'"
		);

		$block_names = [
			'crtoday/exchange-rates',
			'crtoday/currency-converter',
			'crtoday/rate-card',
			'crtoday/ticker',
			'crtoday/price-badge',
		];

		$shortcode_names = [
			'crtoday_exchange_rates',
			'crtoday_currency_converter',
			'crtoday_converter',
			'crtoday_rate_card',
			'crtoday_ticker',
			'crtoday_price_badge',
		];
		$sc_pattern = get_shortcode_regex($shortcode_names);

		foreach ($rows as $content) {
			$blocks = parse_blocks($content);
			self::extract_split_codes_from_blocks($blocks, $block_names, $base_codes, $target_codes);

			if (preg_match_all('/' . $sc_pattern . '/', $content, $matches, PREG_SET_ORDER)) {
				foreach ($matches as $m) {
					$atts = shortcode_parse_atts($m[3] ?? '');
					if (is_array($atts)) {
						self::extract_split_codes_from_shortcode_atts($atts, $base_codes, $target_codes);
					}
				}
			}
		}

		/* ── Widget template instances (legacy) ── */
		$widget_instances = get_option('widget_crtoday_widget', []);
		if (is_array($widget_instances)) {
			foreach ($widget_instances as $inst) {
				if (is_array($inst)) {
					self::extract_split_codes_from_shortcode_atts($inst, $base_codes, $target_codes);
				}
			}
		}

		$base_codes   = array_values(array_unique(array_filter($base_codes)));
		$target_codes = array_values(array_unique(array_filter($target_codes)));
		sort($base_codes);
		sort($target_codes);

		/* Remove base codes from target list to avoid duplicates */
		$target_codes = array_values(array_diff($target_codes, $base_codes));

		return ['base' => $base_codes, 'target' => $target_codes];
	}

	/**
	 * Recursively extract currency codes from parsed blocks, splitting base vs target.
	 */
	private static function extract_split_codes_from_blocks(array $blocks, array $block_names, array &$base, array &$target): void
	{
		foreach ($blocks as $block) {
			if (! empty($block['blockName']) && in_array($block['blockName'], $block_names, true)) {
				$a = $block['attrs'] ?? [];

				/* Exchange Rates: base is base, symbols are targets */
				if (isset($a['base']) && is_string($a['base'])) {
					$base[] = strtoupper($a['base']);
				}
				if (isset($a['symbols']) && is_array($a['symbols'])) {
					foreach ($a['symbols'] as $s) {
						$target[] = strtoupper((string) $s);
					}
				}

				/* Converter: fromCurrency=base, toCurrency=target */
				if (isset($a['fromCurrency']) && is_string($a['fromCurrency'])) {
					$base[] = strtoupper($a['fromCurrency']);
				}
				if (isset($a['toCurrency']) && is_string($a['toCurrency'])) {
					$target[] = strtoupper($a['toCurrency']);
				}

				/* Rate Card: from=base, to=target */
				if (isset($a['from']) && is_string($a['from'])) {
					$base[] = strtoupper($a['from']);
				}
				if (isset($a['to']) && is_string($a['to'])) {
					$target[] = strtoupper($a['to']);
				}

				/* Ticker: pairs[].from=base, pairs[].to=target */
				if (isset($a['pairs']) && is_array($a['pairs'])) {
					foreach ($a['pairs'] as $pair) {
						if (is_array($pair)) {
							if (isset($pair['from'])) {
								$base[] = strtoupper((string) $pair['from']);
							}
							if (isset($pair['to'])) {
								$target[] = strtoupper((string) $pair['to']);
							}
						}
					}
				}

				/* Price Badge: primaryCurrency=base, targetCurrencies=targets */
				if (isset($a['primaryCurrency']) && is_string($a['primaryCurrency'])) {
					$base[] = strtoupper($a['primaryCurrency']);
				}
				if (isset($a['targetCurrencies']) && is_array($a['targetCurrencies'])) {
					foreach ($a['targetCurrencies'] as $t) {
						$target[] = strtoupper((string) $t);
					}
				}
			}
			if (! empty($block['innerBlocks'])) {
				self::extract_split_codes_from_blocks($block['innerBlocks'], $block_names, $base, $target);
			}
		}
	}

	/**
	 * Extract split base/target codes from shortcode/widget attributes.
	 */
	private static function extract_split_codes_from_shortcode_atts(array $atts, array &$base, array &$target): void
	{
		/* Base currency attrs */
		foreach (['base', 'from', 'fromCurrency', 'from_currency', 'primary', 'primaryCurrency', 'primary_currency'] as $key) {
			if (isset($atts[$key]) && is_string($atts[$key])) {
				$c = strtoupper(trim($atts[$key]));
				if ($c !== '' && preg_match('/^[A-Z]{3}$/', $c)) {
					$base[] = $c;
				}
			}
		}

		/* Target currency attrs */
		foreach (['to', 'toCurrency', 'to_currency'] as $key) {
			if (isset($atts[$key]) && is_string($atts[$key])) {
				$c = strtoupper(trim($atts[$key]));
				if ($c !== '' && preg_match('/^[A-Z]{3}$/', $c)) {
					$target[] = $c;
				}
			}
		}

		/* Array/CSV target lists */
		foreach (['symbols', 'target_currencies', 'targetCurrencies', 'targets'] as $key) {
			if (isset($atts[$key]) && is_string($atts[$key])) {
				foreach (explode(',', $atts[$key]) as $c) {
					$c = strtoupper(trim($c));
					if ($c !== '') {
						$target[] = $c;
					}
				}
			} elseif (isset($atts[$key]) && is_array($atts[$key])) {
				foreach ($atts[$key] as $c) {
					$target[] = strtoupper(trim((string) $c));
				}
			}
		}

		/* pairs="EUR/USD,GBP/JPY" */
		if (isset($atts['pairs']) && is_string($atts['pairs'])) {
			foreach (explode(',', $atts['pairs']) as $pair) {
				$parts = explode('/', trim($pair));
				if (count($parts) === 2) {
					$from = strtoupper(trim($parts[0]));
					$to   = strtoupper(trim($parts[1]));
					if ($from !== '' && preg_match('/^[A-Z]{3}$/', $from)) {
						$base[] = $from;
					}
					if ($to !== '' && preg_match('/^[A-Z]{3}$/', $to)) {
						$target[] = $to;
					}
				}
			}
		}
	}

	/* ================================================================
	 * Cache / data operations
	 * ================================================================ */

	public static function invalidate_cache(): int
	{
		global $wpdb;

		$deleted = 0;

		foreach (self::CACHE_PREFIXES as $prefix) {
			$like_val     = $wpdb->esc_like('_transient_' . $prefix) . '%';
			$like_timeout = $wpdb->esc_like('_transient_timeout_' . $prefix) . '%';

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk transient cleanup requires direct DELETE by option_name pattern.
			$deleted += (int) $wpdb->query(
				$wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_val)
			);
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk transient timeout cleanup requires direct DELETE by option_name pattern.
			$deleted += (int) $wpdb->query(
				$wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_timeout)
			);
		}

		return $deleted;
	}

	/**
	 * Delete only expired transients for this plugin.
	 *
	 * @return int Number of expired transient rows deleted.
	 */
	public static function invalidate_expired_cache(): int
	{
		global $wpdb;

		$now     = time();
		$deleted = 0;

		foreach (self::CACHE_PREFIXES as $prefix) {
			$timeout_like = $wpdb->esc_like('_transient_timeout_' . $prefix) . '%';

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Retrieve expired timeout option names.
			$expired_timeouts = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND CAST(option_value AS UNSIGNED) < %d",
					$timeout_like,
					$now
				)
			);

			foreach ($expired_timeouts as $timeout_option) {
				$transient_option = str_replace('_transient_timeout_', '_transient_', $timeout_option);
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Delete expired transient value.
				$wpdb->delete($wpdb->options, ['option_name' => $transient_option]);
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Delete expired transient timeout.
				$wpdb->delete($wpdb->options, ['option_name' => $timeout_option]);
				$deleted += 2;
			}
		}

		return $deleted;
	}

	public static function delete_all_data(): void
	{
		self::invalidate_cache();
		CRToday_Storage_Installer::truncate_rates_table();
		/* Settings option and local sources are intentionally NOT deleted here — only cached rates are removed.
		 * Use delete_all_plugin_data() for full cleanup (uninstall). */
	}

	/**
	 * Delete all plugin data including local sources. Used by uninstall.php.
	 */
	public static function delete_all_plugin_data(): void
	{
		self::invalidate_cache();
		delete_option(self::LOCAL_SOURCES_KEY);
		delete_option(self::BUILTIN_SOURCE_METRICS_OPTION);
		delete_option(self::OPTION_NAME);
	}

	/**
	 * Persist latest built-in source fetch metadata for admin Sources tab.
	 */
	public static function record_builtin_source_fetch(string $source_id, string $deal, int $requested_at, int $api_ts = 0): void
	{
		$source_id = strtoupper(trim($source_id));
		$deal      = strtoupper(trim($deal));

		if (! isset(self::sources_registry()[$source_id])) {
			return;
		}

		if (! isset(self::sources_registry()[$source_id]['deals'][$deal])) {
			$deal = (string) array_key_first(self::sources_registry()[$source_id]['deals']);
		}

		$stored = get_option(self::BUILTIN_SOURCE_METRICS_OPTION, []);
		if (! is_array($stored)) {
			$stored = [];
		}

		$cache_key = $source_id . ':' . $deal;
		$stored[$cache_key] = [
			'source_id'    => $source_id,
			'deal'         => $deal,
			'requested_at' => max(1, $requested_at),
			'api_ts'       => max(0, $api_ts),
		];

		update_option(self::BUILTIN_SOURCE_METRICS_OPTION, $stored, false);
	}

	/**
	 * @return array<string,array{source_id:string,deal:string,requested_at:int,api_ts:int}>
	 */
	private static function get_builtin_source_fetch_metrics(): array
	{
		$stored = get_option(self::BUILTIN_SOURCE_METRICS_OPTION, []);
		if (! is_array($stored)) {
			return [];
		}

		$normalized = [];
		foreach ($stored as $item) {
			if (! is_array($item)) {
				continue;
			}

			$source_id = strtoupper((string) ($item['source_id'] ?? ''));
			$deal      = strtoupper((string) ($item['deal'] ?? ''));

			if (! isset(self::sources_registry()[$source_id]) || ! isset(self::sources_registry()[$source_id]['deals'][$deal])) {
				continue;
			}

			$key = $source_id . ':' . $deal;
			$normalized[$key] = [
				'source_id'    => $source_id,
				'deal'         => $deal,
				'requested_at' => max(0, (int) ($item['requested_at'] ?? 0)),
				'api_ts'       => max(0, (int) ($item['api_ts'] ?? 0)),
			];
		}

		return $normalized;
	}

	/**
	 * Merge persisted per source+deal fetch metrics with cache-derived fallback.
	 *
	 * @return array<string,array{source_id:string,deal:string,requested_at:int,api_ts:int}>
	 */
	private static function get_builtin_source_metrics_with_fallback(): array
	{
		$metrics = self::get_builtin_source_fetch_metrics();
		$diagnostics = self::get_diagnostics();

		/** @var array<string,array{requested_at:int,api_ts:int}> $source_fallback */
		$source_fallback = [];

		foreach ((array) ($diagnostics['entries'] ?? []) as $entry) {
			if (! is_array($entry)) {
				continue;
			}

			$source = strtoupper((string) ($entry['source'] ?? ''));
			if (! isset(self::sources_registry()[$source])) {
				continue;
			}

			$saved_at = max(0, (int) ($entry['saved_at'] ?? 0));
			$api_ts = 0;
			$timestamp_raw = (string) ($entry['timestamp'] ?? '');
			if ($timestamp_raw !== '' && $timestamp_raw !== '—') {
				$parsed = strtotime($timestamp_raw);
				if ($parsed !== false && $parsed > 0) {
					$api_ts = (int) $parsed;
				}
			}

			if (! isset($source_fallback[$source])) {
				$source_fallback[$source] = ['requested_at' => 0, 'api_ts' => 0];
			}

			if ($saved_at > $source_fallback[$source]['requested_at']) {
				$source_fallback[$source]['requested_at'] = $saved_at;
			}
			if ($api_ts > $source_fallback[$source]['api_ts']) {
				$source_fallback[$source]['api_ts'] = $api_ts;
			}
		}

		foreach (self::sources_registry() as $source_id => $meta) {
			foreach ((array) ($meta['deals'] ?? []) as $deal_id => $deal_meta) {
				unset($deal_meta);
				$key = $source_id . ':' . $deal_id;
				if (isset($metrics[$key])) {
					continue;
				}

				$fallback = $source_fallback[$source_id] ?? ['requested_at' => 0, 'api_ts' => 0];
				$metrics[$key] = [
					'source_id'    => $source_id,
					'deal'         => (string) $deal_id,
					'requested_at' => (int) $fallback['requested_at'],
					'api_ts'       => (int) $fallback['api_ts'],
				];
			}
		}

		return $metrics;
	}

	private static function format_wp_local_datetime(int $timestamp): string
	{
		if ($timestamp <= 0) {
			return '—';
		}

		// Short format: "Feb 18, 2026 5:23 pm"
		$format = 'M j, Y g:i a';

		return wp_date($format, $timestamp);
	}

	private static function wp_timezone_label(): string
	{
		$tz = trim(wp_timezone_string());
		if ($tz !== '') {
			return $tz;
		}

		$offset = (float) get_option('gmt_offset', 0);
		$sign = $offset >= 0 ? '+' : '-';
		$abs = abs($offset);
		$hours = (int) floor($abs);
		$minutes = (int) round(($abs - $hours) * 60);

		return sprintf('UTC%s%02d:%02d', $sign, $hours, $minutes);
	}

	/**
	 * Returns comprehensive diagnostics about all cached transients for the plugin.
	 *
	 * @return array{entries:list<array>,total:int,total_size:int,settings_size:int,by_source:array<string,array>,oldest_saved:int,newest_saved:int,total_rates:int,total_db_rows:int}
	 */
	public static function get_diagnostics(): array
	{
		global $wpdb;

		$prefix = $wpdb->esc_like('_transient_' . self::CACHE_PREFIX) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics reads transient rows directly from wp_options for accurate footprint reporting.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name, option_value FROM {$wpdb->options}
				 WHERE option_name LIKE %s AND option_name NOT LIKE %s",
				$prefix,
				$wpdb->esc_like('_transient_timeout_') . '%'
			)
		);

		/* Count timeout rows too (they occupy DB space) */
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics counts timeout rows for total DB footprint.
		$timeout_rows = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
				$wpdb->esc_like('_transient_timeout_' . self::CACHE_PREFIX) . '%'
			)
		);

		/* Settings option size */
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostics reads serialized option size for storage analytics.
		$settings_raw = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
				self::OPTION_NAME
			)
		);
		$settings_size = is_string($settings_raw) ? strlen($settings_raw) : 0;

		$entries       = [];
		$total_size    = 0;
		$by_source     = [];
		$oldest_saved  = PHP_INT_MAX;
		$newest_saved  = 0;
		$total_rates   = 0;
		$ttl           = self::get_cache_ttl_seconds();
		$now           = time();

		if (is_array($rows)) {
			foreach ($rows as $row) {
				$data = maybe_unserialize($row->option_value);
				$size = strlen($row->option_value);
				$total_size += $size;

				$source       = '—';
				$base         = '—';
				$timestamp    = '—';
				$state        = 'unknown';
				$age_str      = '—';
				$pricing_mode = '—';
				$rate_count   = 0;
				$has_bidask   = false;

				if (is_array($data) && isset($data['payload']) && is_array($data['payload'])) {
					$pl           = $data['payload'];
					$source       = $pl['source'] ?? '—';
					$base         = strtoupper((string) ($pl['base'] ?? '—'));
					$api_ts_unix  = (int) ($data['api_ts'] ?? 0);
					$timestamp    = $api_ts_unix > 0 ? gmdate('c', $api_ts_unix) : '—';
					$state        = (isset($pl['rates']) && is_array($pl['rates']) && $pl['rates'] !== []) ? 'ready' : 'empty';

					if (isset($pl['rates']) && is_array($pl['rates'])) {
						$rate_count = count($pl['rates']);
						foreach ($pl['rates'] as $entry) {
							if (is_array($entry) && (! empty($entry['bid']) || ! empty($entry['ask']))) {
								$has_bidask = true;
								break;
							}
						}
					}
					$pricing_mode = $has_bidask ? 'bidask' : 'mid';
				}

				$total_rates += $rate_count;

				$saved_at = isset($data['savedAt']) ? (int) $data['savedAt'] : 0;
				if ($saved_at > 0) {
					$oldest_saved = min($oldest_saved, $saved_at);
					$newest_saved = max($newest_saved, $saved_at);

					$age = $now - $saved_at;
					if ($age < 60) {
						$age_str = $age . 's ago';
					} elseif ($age < 3600) {
						$age_str = (int) round($age / 60) . 'm ago';
					} else {
						$age_str = round($age / 3600, 1) . 'h ago';
					}
				}

				$ttl_remaining = ($saved_at > 0) ? max(0, ($saved_at + $ttl) - $now) : 0;
				$is_expired    = ($saved_at > 0) && ($ttl_remaining <= 0);

				/* Per-source aggregation */
				if ($source !== '—') {
					if (! isset($by_source[$source])) {
						$by_source[$source] = ['count' => 0, 'size' => 0, 'rates' => 0];
					}
					$by_source[$source]['count']++;
					$by_source[$source]['size'] += $size;
					$by_source[$source]['rates'] += $rate_count;
				}

				$entries[] = [
					'key'           => str_replace('_transient_', '', $row->option_name),
					'source'        => $source,
					'base'          => $base,
					'timestamp'     => $timestamp,
					'state'         => $state,
					'size'          => $size,
					'age'           => $age_str,
					'saved_at'      => $saved_at,
					'rate_count'    => $rate_count,
					'pricing_mode'  => $pricing_mode,
					'has_bidask'    => $has_bidask,
					'ttl_remaining' => $ttl_remaining,
					'is_expired'    => $is_expired,
				];
			}
		}

		if ($oldest_saved === PHP_INT_MAX) {
			$oldest_saved = 0;
		}

		return [
			'entries'       => $entries,
			'total'         => count($entries),
			'total_size'    => $total_size,
			'settings_size' => $settings_size,
			'by_source'     => $by_source,
			'oldest_saved'  => $oldest_saved,
			'newest_saved'  => $newest_saved,
			'total_rates'   => $total_rates,
			'total_db_rows' => count($entries) + $timeout_rows,
			'cache_ttl'     => $ttl,
		];
	}

	/**
	 * Returns the active cache TTL in seconds for diagnostics.
	 */
	private static function get_cache_ttl_seconds(): int
	{
		$config = self::get_config();
		return max(1, (int) $config['api_interval']) * 60;
	}

	/**
	 * @return array{rates_rows:int,total_rows:int,rates_bytes:int,total_bytes:int,by_source:array<string,array{rows:int}>}
	 */
	private static function get_db_storage_diagnostics(): array
	{
		global $wpdb;

		$rates_table = CRToday_Storage_Installer::rates_table();

		$rates_rows = 0;
		$rates_bytes = 0;
		$by_source = [];

		if (self::dashboard_table_exists($rates_table)) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard analytics requires direct row count from storage table.
			$rates_rows = max(0, (int) $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i', $rates_table)));

			$rates_bytes = self::read_table_footprint_bytes($rates_table);

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard analytics groups rows by source for reporting.
			$source_rows = $wpdb->get_results($wpdb->prepare(
				'SELECT source, COUNT(*) AS rows_count, COUNT(DISTINCT fetch_ts_utc) AS snapshots_count, MIN(rate_day_utc) AS min_date, MAX(rate_day_utc) AS max_date, COUNT(DISTINCT rate_day_utc) AS days_count FROM %i GROUP BY source',
				$rates_table
			), ARRAY_A);
			if (is_array($source_rows)) {
				foreach ($source_rows as $row) {
					if (! is_array($row)) {
						continue;
					}

					$source = strtoupper((string) ($row['source'] ?? ''));
					if ($source === '') {
						continue;
					}

					$count = max(0, (int) ($row['rows_count'] ?? 0));
					$snapshots = max(0, (int) ($row['snapshots_count'] ?? 0));
					$by_source[$source] = [
						'rows'      => $count,
						'snapshots' => $snapshots,
						'min_date'  => (string) ($row['min_date'] ?? ''),
						'max_date'  => (string) ($row['max_date'] ?? ''),
						'days'      => max(0, (int) ($row['days_count'] ?? 0)),
					];
				}
			}
		}

		ksort($by_source);

		return [
			'rates_rows' => $rates_rows,
			'total_rows' => $rates_rows,
			'rates_bytes' => $rates_bytes,
			'total_bytes' => $rates_bytes,
			'by_source' => $by_source,
		];
	}

	private static function dashboard_table_exists(string $table): bool
	{
		return CRToday_Storage_Installer::table_exists($table);
	}

	private static function read_table_footprint_bytes(string $table): int
	{
		global $wpdb;

		if (! defined('DB_NAME') || ! is_string(DB_NAME) || DB_NAME === '') {
			return 0;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard analytics reads metadata from information_schema.
		$size = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT (data_length + index_length) FROM information_schema.TABLES WHERE table_schema = %s AND table_name = %s LIMIT 1',
				DB_NAME,
				$table
			)
		);

		if (! is_numeric($size)) {
			return 0;
		}

		return max(0, (int) $size);
	}

	/* ================================================================
	 * Admin page registration
	 * ================================================================ */

	public static function register_admin_page(): void
	{
		add_options_page(
			__('CurrencyRate.Today - Currency Blocks and Widgets', 'currencyrate-today-currency-blocks'),
			__('CurrencyRate.Today - Currency Blocks and Widgets', 'currencyrate-today-currency-blocks'),
			'manage_options',
			self::PAGE_SLUG,
			[self::class, 'render_admin_page']
		);

		/* Hide the submenu item from the Settings list while keeping
		   the page accessible via direct URL and plugin action link. */
	}

	public static function register_settings(): void
	{
		register_setting(
			'crtoday_settings_group',
			self::OPTION_NAME,
			[
				'type'              => 'array',
				'sanitize_callback' => [self::class, 'sanitize_options'],
				'default'           => [
					'retention'      => 2,
					'api_interval'   => 15,
					'rounding_mode'  => 'half_even',
				],
			]
		);

	}

	public static function sanitize_options($value): array
	{
		$defaults = ['retention' => 2, 'api_interval' => 15, 'rounding_mode' => 'half_even'];

		if (! is_array($value)) {
			return $defaults;
		}

		/*
		 * Merge with stored values so that partial form submissions
		 * (e.g. Settings block without Data Source fields) do not
		 * reset keys that were not included in the POST payload.
		 */
		$stored = (array) get_option(self::OPTION_NAME, []);
		$merged = array_merge($defaults, $stored, $value);

		$retention = (int) ($merged['retention'] ?? 2);
		if (! in_array($retention, self::ALLOWED_RETENTION, true)) {
			$retention = 2;
		}

		$api_interval = (int) ($merged['api_interval'] ?? 15);
		if (! in_array($api_interval, self::ALLOWED_API_INTERVAL, true)) {
			$api_interval = 15;
		}

		$rounding_mode = sanitize_text_field((string) ($merged['rounding_mode'] ?? 'half_even'));
		if (! in_array($rounding_mode, self::ALLOWED_ROUNDING, true)) {
			$rounding_mode = 'half_even';
		}

		$history_scope = sanitize_text_field((string) ($merged['history_scope'] ?? 'all'));
		if (! in_array($history_scope, ['all', 'active'], true)) {
			$history_scope = 'all';
		}

		$old_scope = $stored['history_scope'] ?? 'all';
		if ($history_scope !== $old_scope) {
			delete_transient('crtoday_active_currencies');
		}

		return [
			'retention'      => $retention,
			'api_interval'   => $api_interval,
			'rounding_mode'  => $rounding_mode,
			'history_scope'  => $history_scope,
		];
	}

	/* ================================================================
	 * Admin page render
	 * ================================================================ */

	public static function render_admin_page(): void
	{
		if (! current_user_can('manage_options')) {
			return;
		}

		$config   = self::get_config();
		$tab      = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page tab switch, no data mutation.
		$page_url = admin_url('options-general.php?page=' . self::PAGE_SLUG);

		$allowed_tabs = ['dashboard', 'storage', 'sources', 'shortcodes', 'currencies'];
		if (defined('CRTODAY_DEBUG') && CRTODAY_DEBUG) {
			$allowed_tabs[] = 'debug';
		}
		if (! in_array($tab, $allowed_tabs, true)) {
			$tab = 'dashboard';
		}
?>
		<div class="wrap crtoday-admin">
			<h1 style="display:flex;align-items:center;gap:16px;padding:8px 0;margin-bottom:16px;">
				<img
					src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'logo.svg'); ?>"
					alt="<?php esc_attr_e('Exchange Rates logo', 'currencyrate-today-currency-blocks'); ?>"
					style="width:64px;height:64px;display:block;border-radius:14px;filter:drop-shadow(0 2px 8px rgba(0,0,0,0.10));" />
				<span style="display:flex;flex-direction:column;gap:6px;">
					<span style="display:flex;align-items:center;gap:8px;">
						<span style="display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:#475569;background:#fff;border:1px solid rgba(148,163,184,0.12);border-radius:100px;padding:3px 12px;box-shadow:0 1px 3px rgba(15,23,42,0.04);white-space:nowrap;"><img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'currencyrate_today.svg'); ?>" alt="" style="width:14px;height:14px;display:block;flex-shrink:0;" />CurrencyRate.Today</span>
						<span style="font-size:10px;font-weight:600;color:#6366f1;background:#eef2ff;border:1px solid #c7d2fe;border-radius:100px;padding:2px 10px;letter-spacing:0.04em;">v<?php echo esc_html(CRTODAY_PLUGIN_VERSION); ?></span>
					</span>
					<span style="font-size:24px;font-weight:800;letter-spacing:-0.03em;line-height:1.1;background:linear-gradient(135deg,#0f172a 0%,#334155 50%,#6366f1 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;"><?php esc_html_e('Currency Blocks & Widgets', 'currencyrate-today-currency-blocks'); ?></span>
				</span>
			</h1>

			<nav class="nav-tab-wrapper" style="margin-bottom:6px;">
				<a class="nav-tab <?php echo $tab === 'dashboard' ? 'nav-tab-active' : ''; ?>"
					href="<?php echo esc_url(add_query_arg('tab', 'dashboard', $page_url)); ?>">
					<span class="dashicons dashicons-dashboard"></span>
					<?php esc_html_e('Dashboard', 'currencyrate-today-currency-blocks'); ?>
				</a>
				<a class="nav-tab <?php echo $tab === 'storage' ? 'nav-tab-active' : ''; ?>"
					href="<?php echo esc_url(add_query_arg('tab', 'storage', $page_url)); ?>">
					<span class="dashicons dashicons-archive"></span>
					<?php esc_html_e('Storage', 'currencyrate-today-currency-blocks'); ?>
				</a>
				<a class="nav-tab <?php echo $tab === 'sources' ? 'nav-tab-active' : ''; ?>"
					href="<?php echo esc_url(add_query_arg('tab', 'sources', $page_url)); ?>">
					<span class="dashicons dashicons-database"></span>
					<?php esc_html_e('Sources', 'currencyrate-today-currency-blocks'); ?>
				</a>
				<a class="nav-tab <?php echo $tab === 'shortcodes' ? 'nav-tab-active' : ''; ?>"
					href="<?php echo esc_url(add_query_arg('tab', 'shortcodes', $page_url)); ?>">
					<span class="dashicons dashicons-shortcode"></span>
					<?php esc_html_e('Shortcodes', 'currencyrate-today-currency-blocks'); ?>
				</a>
				<a class="nav-tab <?php echo $tab === 'currencies' ? 'nav-tab-active' : ''; ?>"
					href="<?php echo esc_url(add_query_arg('tab', 'currencies', $page_url)); ?>">
					<span class="dashicons dashicons-money-alt"></span>
					<?php esc_html_e('Currencies', 'currencyrate-today-currency-blocks'); ?>
				</a>
				<?php if (defined('CRTODAY_DEBUG') && CRTODAY_DEBUG) : ?>
					<a class="nav-tab <?php echo $tab === 'debug' ? 'nav-tab-active' : ''; ?>"
						href="<?php echo esc_url(add_query_arg('tab', 'debug', $page_url)); ?>">
						<span class="dashicons dashicons-admin-tools"></span>
						<?php esc_html_e('Debug', 'currencyrate-today-currency-blocks'); ?>
					</a>
				<?php endif; ?>
				<span class="crtoday-nav-spacer"></span>
				<a class="nav-tab crtoday-nav-external" href="https://wordpress.org/support/plugin/currencyrate-today-currency-blocks/" target="_blank" rel="noopener">
					<span class="dashicons dashicons-sos"></span>
					<?php esc_html_e('Support', 'currencyrate-today-currency-blocks'); ?>
					<span class="dashicons dashicons-external crtoday-dashicon-external"></span>
				</a>
				<a class="nav-tab crtoday-nav-external crtoday-nav-review" href="https://wordpress.org/support/plugin/currencyrate-today-currency-blocks/reviews/#new-post" target="_blank" rel="noopener">
					<span class="dashicons dashicons-star-filled"></span>
					<?php esc_html_e('Review', 'currencyrate-today-currency-blocks'); ?>
					<span class="dashicons dashicons-external crtoday-dashicon-external"></span>
				</a>
			</nav>

			<?php if ($tab === 'dashboard') : ?>
				<?php self::render_dashboard_tab($config); ?>
			<?php elseif ($tab === 'storage') : ?>
				<?php self::render_storage_tab(); ?>
			<?php elseif ($tab === 'sources') : ?>
				<?php self::render_sources_tab(); ?>
			<?php elseif ($tab === 'shortcodes') : ?>
				<?php self::render_shortcodes_tab($config); ?>
			<?php elseif ($tab === 'currencies') : ?>
				<?php self::render_currencies_tab(); ?>
			<?php elseif ($tab === 'debug') : ?>
				<?php self::render_debug_tab(); ?>
			<?php endif; ?>

			<?php self::render_admin_footer(); ?>
		</div>
	<?php
	}

	/* ================================================================
	 * Dashboard tab
	 * ================================================================ */

	private static function render_dashboard_tab(array $config): void
	{
		$db_storage    = self::get_db_storage_diagnostics();
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only flash flags from admin redirect.
		$flash_fetch   = isset($_GET['crtoday_force_fetched']);
		// phpcs:enable
	?>

		<?php self::render_flash($flash_fetch, __('Done. Rates refresh on next page load.', 'currencyrate-today-currency-blocks')); ?>

		<div class="crtoday-admin-grid crtoday-dashboard-grid crtoday-admin-tab-container">

			<!-- ═══ LEFT: Overview ═══ -->
			<div class="postbox crtoday-postbox">
				<div class="postbox-header">
					<h2 class="crtoday-postbox-heading">
						<span class="dashicons dashicons-dashboard"></span>
						<?php esc_html_e('Overview', 'currencyrate-today-currency-blocks'); ?>
					</h2>
				</div>
				<div class="inside crtoday-postbox-body">
					<?php
					$db_total_bytes   = max(0, (int) ($db_storage['total_bytes'] ?? 0));
					$db_rates_rows    = max(0, (int) ($db_storage['rates_rows'] ?? 0));
					$db_sources_count = is_array($db_storage['by_source'] ?? null) ? count($db_storage['by_source']) : 0;
					$history_scope_label = $config['history_scope'] === 'active'
						? __('Active currencies only', 'currencyrate-today-currency-blocks')
						: __('All currencies', 'currencyrate-today-currency-blocks');
					?>

					<!-- KPI cards -->
					<div class="crtoday-kpi-grid crtoday-admin-kpis">
						<div class="crtoday-kpi-card">
							<div class="crtoday-kpi-value"><?php echo esc_html(self::format_bytes($db_total_bytes)); ?></div>
							<div class="crtoday-kpi-label"><?php esc_html_e('Storage used', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
						<div class="crtoday-kpi-card">
							<div class="crtoday-kpi-value" title="<?php echo esc_attr(number_format_i18n($db_sources_count)); ?>"><?php echo esc_html(self::format_number($db_sources_count)); ?></div>
							<div class="crtoday-kpi-label"><?php esc_html_e('Sources', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
						<div class="crtoday-kpi-card">
							<div class="crtoday-kpi-value" title="<?php echo esc_attr(number_format_i18n($db_rates_rows)); ?>"><?php echo esc_html(self::format_number($db_rates_rows)); ?></div>
							<div class="crtoday-kpi-label"><?php esc_html_e('Stored rates', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
					</div>

					<table class="crtoday-table-borderless widefat striped">
						<tbody>
							<tr>
								<td style="font-weight:600;width:180px;"><?php esc_html_e('Available sources', 'currencyrate-today-currency-blocks'); ?></td>
								<td>
									<?php
									$local_count = count(self::get_local_sources());
									$builtin_count = count(self::sources_registry());
									echo esc_html(sprintf(
										/* translators: %1$d: built-in sources count, %2$d: local sources count */
										__('%1$d built-in, %2$d local', 'currencyrate-today-currency-blocks'),
										$builtin_count,
										$local_count,
									));
									?>
								</td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Update interval', 'currencyrate-today-currency-blocks'); ?></td>
								<td>
									<?php
									/* translators: %d: number of minutes */
									echo esc_html(sprintf(__('Every %d minutes', 'currencyrate-today-currency-blocks'), $config['api_interval']));
									?>
								</td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('History scope', 'currencyrate-today-currency-blocks'); ?></td>
								<td><?php echo esc_html($history_scope_label); ?></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Retention', 'currencyrate-today-currency-blocks'); ?></td>
								<td>
									<?php
									/* translators: %d: number of days */
									echo esc_html(sprintf(_n('%d day', '%d days', $config['retention'], 'currencyrate-today-currency-blocks'), $config['retention']));
									?>
								</td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Plugin version', 'currencyrate-today-currency-blocks'); ?></td>
								<td><code><?php echo esc_html(CRTODAY_PLUGIN_VERSION); ?></code></td>
							</tr>
						</tbody>
					</table>

					<?php
					/* ── Active currencies badges ── */
					$active_codes = self::get_active_currency_codes_for_dashboard();
					$all_codes    = array_unique(array_merge($active_codes['base'], $active_codes['target']));
					sort($all_codes);
					$flags_url = CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/';
					if (! empty($all_codes)) :
					?>
						<div style="padding:12px 16px 16px;">
							<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.3px;color:#64748b;margin-bottom:6px;">
								<?php esc_html_e('Active currencies', 'currencyrate-today-currency-blocks'); ?>
							</div>
							<div style="display:flex;flex-wrap:wrap;gap:6px;">
								<?php foreach ($all_codes as $code) :
									$flag = self::get_currency_flag($code);
								?>
									<span class="crtoday-tag">
										<?php if ($flag) : ?>
											<img class="crtoday-tag-flag" src="<?php echo esc_url($flags_url . $flag . '.webp'); ?>" alt="" width="16" height="16" />
										<?php endif; ?>
										<?php echo esc_html($code); ?>
									</span>
								<?php endforeach; ?>
							</div>
							<div style="font-size:11px;color:#64748b;margin-top:6px;">
								<?php esc_html_e('Currencies currently used across your blocks and widgets.', 'currencyrate-today-currency-blocks'); ?>
							</div>
						</div>
					<?php endif; ?>
				</div>
			</div>

			<!-- ═══ ROW 1, RIGHT: Settings ═══ -->
			<div class="postbox crtoday-postbox">
				<div class="postbox-header">
					<h2 class="crtoday-postbox-heading">
						<span class="dashicons dashicons-admin-generic"></span>
						<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
					</h2>
				</div>
				<div class="inside crtoday-postbox-body">
					<form action="options.php" method="post" class="crtoday-pad-16">
						<?php settings_fields('crtoday_settings_group'); ?>
						<table class="form-table" role="presentation" style="margin-top:0;">
							<tr>
								<th scope="row"><label for="crtoday-rounding-mode"><?php esc_html_e('Rounding mode', 'currencyrate-today-currency-blocks'); ?></label></th>
								<td>
									<select name="<?php echo esc_attr(self::OPTION_NAME); ?>[rounding_mode]" id="crtoday-rounding-mode">
										<?php $rounding_labels = self::rounding_labels(); ?>
										<?php foreach (self::ALLOWED_ROUNDING as $mode) : ?>
											<option value="<?php echo esc_attr($mode); ?>" <?php selected($config['rounding_mode'], $mode); ?>>
												<?php echo esc_html($rounding_labels[$mode] ?? $mode); ?>
											</option>
										<?php endforeach; ?>
									</select>
								</td>
							</tr>
							<tr>
								<th scope="row"><label for="crtoday-retention"><?php esc_html_e('Keep data for', 'currencyrate-today-currency-blocks'); ?></label></th>
								<td>
									<select name="<?php echo esc_attr(self::OPTION_NAME); ?>[retention]" id="crtoday-retention">
										<?php foreach (self::ALLOWED_RETENTION as $days) : ?>
											<option value="<?php echo esc_attr((string) $days); ?>" <?php selected($config['retention'], $days); ?>>
												<?php
												/* translators: %d: number of days */
												echo esc_html(sprintf(_n('%d day', '%d days', $days, 'currencyrate-today-currency-blocks'), $days));
												?>
											</option>
										<?php endforeach; ?>
									</select>
									<p class="description"><?php esc_html_e('2-day minimum always retained. Older data is automatically purged.', 'currencyrate-today-currency-blocks'); ?></p>
								</td>
							</tr>
							<tr>
								<th scope="row"><?php esc_html_e('History scope', 'currencyrate-today-currency-blocks'); ?></th>
								<td>
									<label for="crtoday-history-scope" class="crtoday-label-inline">
										<input type="hidden" name="<?php echo esc_attr(self::OPTION_NAME); ?>[history_scope]" value="all" />
										<input type="checkbox" name="<?php echo esc_attr(self::OPTION_NAME); ?>[history_scope]" id="crtoday-history-scope" value="active" <?php checked($config['history_scope'], 'active'); ?> />
										<?php esc_html_e('Active currencies only', 'currencyrate-today-currency-blocks'); ?>
									</label>
									<p class="description"><?php esc_html_e('Store history only for currencies used in your blocks. Saves up to 95% DB space. New currencies will have no chart history until data accumulates.', 'currencyrate-today-currency-blocks'); ?></p>
								</td>
							</tr>
						</table>
						<?php submit_button(__('Save settings', 'currencyrate-today-currency-blocks'), 'primary', 'submit', false, ['class' => 'button button-primary crtoday-save-settings-btn']); ?>
						<span class="description crtoday-desc-beside"><?php esc_html_e('Primary action — saves all options above.', 'currencyrate-today-currency-blocks'); ?></span>
					</form>

					<hr style="margin:0;border:none;border-top:1px solid var(--crtoday-admin-border);" />

					<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:16px;">
						<input type="hidden" name="action" value="crtoday_force_fetch" />
						<?php wp_nonce_field('crtoday_force_fetch'); ?>
						<button type="submit" class="button button-secondary crtoday-fetch-now-btn">
							<span class="dashicons dashicons-update"></span>
							<?php esc_html_e('Fetch now', 'currencyrate-today-currency-blocks'); ?>
						</button>
						<span class="crtoday-desc-inline description"><?php esc_html_e('Fetch fresh rates now, without waiting for the next scheduled update.', 'currencyrate-today-currency-blocks'); ?></span>
					</form>
				</div>
			</div>

		</div>
	<?php
	}

	/* ================================================================
	 * Storage tab
	 * ================================================================ */

	private static function render_storage_tab(): void
	{
		$diagnostics = self::get_diagnostics();
		$db_storage  = self::get_db_storage_diagnostics();
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only flash flags from admin redirect.
		$flash_cache   = isset($_GET['crtoday_cache_invalidated']);
		$flash_expired = isset($_GET['crtoday_expired_cleared']);
		$flash_deleted = isset($_GET['crtoday_data_deleted']);
		// phpcs:enable
	?>

		<?php self::render_flash($flash_cache, __('Cache cleared successfully.', 'currencyrate-today-currency-blocks')); ?>
		<?php self::render_flash($flash_expired, __('Expired cache cleared successfully.', 'currencyrate-today-currency-blocks')); ?>
		<?php self::render_flash($flash_deleted, __('Stored rates and temporary data deleted. Data will rebuild automatically.', 'currencyrate-today-currency-blocks'), 'warning'); ?>

		<div class="crtoday-admin-shortcodes crtoday-admin-tab-container">
			<div class="crtoday-sc-folder">
				<nav class="crtoday-sc-nav" role="tablist">
					<button type="button" role="tab" class="crtoday-sc-nav-btn is-active" aria-selected="true" data-pane="database">
						<span class="dashicons dashicons-chart-pie"></span>
						<?php esc_html_e('Storage Analytics', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="cache">
						<span class="dashicons dashicons-database"></span>
						<?php esc_html_e('Cached Data', 'currencyrate-today-currency-blocks'); ?>
						<?php if ($diagnostics['total'] > 0) : ?>
							<span class="crtoday-nav-count">(<?php echo esc_html((string) $diagnostics['total']); ?>)</span>
						<?php endif; ?>
					</button>
				</nav>

				<!-- ═══ Pane: Cached Data ═══ -->
				<div class="crtoday-sc-pane crtoday-pane-flush" data-pane="cache" role="tabpanel">
					<?php if ($diagnostics['total'] === 0) : ?>
						<div class="crtoday-empty-state">
							<span class="dashicons dashicons-database"></span>
							<p class="description" style="margin:0;">
								<?php esc_html_e('No cached data. Rates load on next page view.', 'currencyrate-today-currency-blocks'); ?>
							</p>
						</div>
					<?php else : ?>
						<table class="crtoday-table-noborder fixed widefat striped">
							<thead>
								<tr>
									<th style="width:50px;"><?php esc_html_e('Source', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:40px;"><?php esc_html_e('Base', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:50px;"><?php esc_html_e('Rates', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:60px;"><?php esc_html_e('Mode', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:50px;"><?php esc_html_e('State', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:90px;"><?php esc_html_e('Freshness', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:55px;"><?php esc_html_e('Size', 'currencyrate-today-currency-blocks'); ?></th>
								</tr>
							</thead>
							<tbody>
								<?php foreach ($diagnostics['entries'] as $entry) : ?>
									<?php
									$state_colors = [
										'ready'   => '#00a32a',
										'empty'   => '#dba617',
										'unknown' => '#787c82',
									];
									$state_color = $state_colors[$entry['state']] ?? '#787c82';

									$freshness_color = $entry['is_expired'] ? '#d63638' : '#00a32a';
									if ($entry['ttl_remaining'] > 0 && $entry['ttl_remaining'] < 120) {
										$freshness_color = '#dba617';
									}

									$ttl_label = '—';
									if ($entry['is_expired']) {
										$ttl_label = __('Expired', 'currencyrate-today-currency-blocks');
									} elseif ($entry['ttl_remaining'] > 0) {
										$remaining_m = (int) ceil($entry['ttl_remaining'] / 60);
										/* translators: %d: number of minutes remaining */
										$ttl_label = sprintf(__('%dm left', 'currencyrate-today-currency-blocks'), $remaining_m);
									}

									$mode_label = $entry['pricing_mode'] === 'bidask'
										? __('Bid/Ask', 'currencyrate-today-currency-blocks')
										: __('Mid', 'currencyrate-today-currency-blocks');

									$size_pct = ($diagnostics['total_size'] > 0) ? round(($entry['size'] / $diagnostics['total_size']) * 100) : 0;
									$progress_palette = ['#6366f1', '#7c3aed', '#f97316', '#ec4899', '#dc2626', '#0891b2'];
									$entry_progress_color = $progress_palette[abs(crc32((string) ($entry['source'] ?? '—'))) % count($progress_palette)];
									$entry_source_view = sprintf('<strong>%s</strong>', esc_html((string) $entry['source']));
									?>
									<tr>
										<td><?php echo $entry_source_view; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above ?></td>
										<td><?php echo esc_html($entry['base']); ?></td>
										<td class="crtoday-text-sm crtoday-text-center">
											<?php echo esc_html($entry['rate_count'] > 0 ? (string) $entry['rate_count'] : '—'); ?>
										</td>
										<td>
											<span style="font-size:11px;color:#1d2327;">
												<?php echo esc_html($mode_label); ?>
											</span>
										</td>
										<td>
											<span style="color:<?php echo esc_attr($state_color); ?>;font-weight:600;font-size:11px;text-transform:uppercase;">
												<?php echo esc_html($entry['state']); ?>
											</span>
										</td>
										<td class="crtoday-text-sm">
											<span style="color:<?php echo esc_attr($freshness_color); ?>;font-weight:500;">
												<?php echo esc_html($ttl_label); ?>
											</span>
											<br />
											<span style="color:#787c82;font-size:10px;"><?php echo esc_html($entry['age']); ?></span>
										</td>
										<td class="crtoday-text-sm">
											<?php echo esc_html(self::format_bytes($entry['size'])); ?>
											<div style="background:#f0f0f1;border-radius:2px;height:4px;margin-top:3px;overflow:hidden;">
												<div style="width:<?php echo esc_attr((string) $size_pct); ?>%;background:<?php echo esc_attr($entry_progress_color); ?>;height:100%;border-radius:2px;"></div>
											</div>
										</td>
									</tr>
								<?php endforeach; ?>
							</tbody>
						</table>
					<?php endif; ?>

					<div class="crtoday-flex-row crtoday-pad-16">
						<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="crtoday-action-form--inline">
							<input type="hidden" name="action" value="crtoday_invalidate_expired" />
							<?php wp_nonce_field('crtoday_invalidate_expired'); ?>
							<button type="submit" class="button">
								<span class="dashicons dashicons-clock"></span>
								<?php esc_html_e('Clear expired', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</form>
						<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="crtoday-action-form--inline" onsubmit="return confirm('<?php echo esc_js(__('Clear all stored rates? They will rebuild on the next page view.', 'currencyrate-today-currency-blocks')); ?>');">
							<input type="hidden" name="action" value="crtoday_invalidate_cache" />
							<?php wp_nonce_field('crtoday_invalidate_cache'); ?>
							<button type="submit" class="button crtoday-btn-danger">
								<span class="dashicons dashicons-trash"></span>
								<?php esc_html_e('Clear cache', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</form>
					</div>
				</div>

				<!-- ═══ Pane: Storage Analytics ═══ -->
				<div class="crtoday-sc-pane is-active crtoday-pane-flush" data-pane="database" role="tabpanel">
					<?php
					$db_total_rows = max(0, (int) ($db_storage['total_rows'] ?? 0));
					$db_rates_rows = max(0, (int) ($db_storage['rates_rows'] ?? 0));
					$db_total_bytes = max(0, (int) ($db_storage['total_bytes'] ?? 0));
					$db_sources_count = is_array($db_storage['by_source'] ?? null) ? count($db_storage['by_source']) : 0;
					?>

					<!-- Summary cards -->
					<div class="crtoday-kpi-grid crtoday-kpi-grid--lg crtoday-admin-kpis">
						<div class="crtoday-kpi-card crtoday-kpi-card--lg">
							<div class="crtoday-kpi-value crtoday-kpi-value--lg" title="<?php echo esc_attr(number_format_i18n($db_rates_rows)); ?>"><?php echo esc_html(self::format_number($db_rates_rows)); ?></div>
							<div class="crtoday-kpi-label crtoday-kpi-label--lg"><?php esc_html_e('Stored rates', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
						<div class="crtoday-kpi-card crtoday-kpi-card--lg">
							<div class="crtoday-kpi-value crtoday-kpi-value--lg" title="<?php echo esc_attr(number_format_i18n($db_sources_count)); ?>"><?php echo esc_html(self::format_number($db_sources_count)); ?></div>
							<div class="crtoday-kpi-label crtoday-kpi-label--lg"><?php esc_html_e('Sources tracked', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
						<div class="crtoday-kpi-card crtoday-kpi-card--lg">
							<div class="crtoday-kpi-value crtoday-kpi-value--lg"><?php echo esc_html(self::format_bytes($db_total_bytes)); ?></div>
							<div class="crtoday-kpi-label crtoday-kpi-label--lg"><?php esc_html_e('Storage used', 'currencyrate-today-currency-blocks'); ?></div>
						</div>
					</div>

					<!-- Breakdown bars -->
					<table class="crtoday-table-borderless widefat">
						<tbody>
							<tr style="margin-bottom:0;">
								<td class="crtoday-td-label--w140"><?php esc_html_e('Rates table', 'currencyrate-today-currency-blocks'); ?></td>
								<td>
									<?php $pct_rates = ($db_total_rows > 0) ? (int) round(($db_rates_rows / $db_total_rows) * 100) : 0; ?>
									<div style="display:flex;align-items:center;gap:8px;">
										<div class="crtoday-progress-track" style="flex:1;">
											<div class="crtoday-progress-bar" style="width:<?php echo esc_attr((string) $pct_rates); ?>%;"></div>
										</div>
										<span style="font-size:16px;color:#6366f1;white-space:nowrap;" title="<?php echo esc_attr(number_format_i18n($db_rates_rows)); ?>">
											<?php echo esc_html(self::format_number($db_rates_rows)); ?>
										</span>
									</div>
								</td>
							</tr>
						</tbody>
					</table>

					<?php if (! empty($db_storage['by_source'])) : ?>
						<h4 style="margin:16px 0 8px;font-size:16px;text-transform:uppercase;letter-spacing:0.3px;color:#1d2327;padding:16px">
							<?php esc_html_e('Per source (DB)', 'currencyrate-today-currency-blocks'); ?>
						</h4>
						<table class="crtoday-table-borderless widefat striped">
							<thead>
								<tr>
									<th><?php esc_html_e('Source', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="crtoday-text-right"><?php esc_html_e('Rows', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="crtoday-text-right"><?php esc_html_e('Snapshots', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="crtoday-text-right"><?php esc_html_e('Days', 'currencyrate-today-currency-blocks'); ?></th>
									<th><?php esc_html_e('Date range', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:25%;"><?php esc_html_e('Share', 'currencyrate-today-currency-blocks'); ?></th>
								</tr>
							</thead>
							<tbody>
								<?php
								$source_colors = ['#6366f1', '#7c3aed', '#f97316', '#ec4899', '#dc2626', '#0891b2'];
								$color_idx = 0;
								foreach ($db_storage['by_source'] as $src_name => $src_data) :
									$src_color = $source_colors[$color_idx % count($source_colors)];
									$src_rows = max(0, (int) ($src_data['rows'] ?? 0));
									$src_snapshots = max(0, (int) ($src_data['snapshots'] ?? 0));
									$src_days = max(0, (int) ($src_data['days'] ?? 0));
									$src_min_date = (string) ($src_data['min_date'] ?? '');
									$src_max_date = (string) ($src_data['max_date'] ?? '');
									$src_pct   = ($db_total_rows > 0) ? (int) round(($src_rows / $db_total_rows) * 100) : 0;
									$color_idx++;
									$src_view = sprintf('<strong>%s</strong>', esc_html($src_name));

									$range_label = '—';
									if ($src_min_date !== '' && $src_max_date !== '') {
										$range_label = $src_min_date === $src_max_date
											? $src_max_date
											: $src_min_date . ' — ' . $src_max_date;
									}
								?>
									<tr>
										<td>
											<span class="crtoday-color-dot" style="background:<?php echo esc_attr($src_color); ?>;"></span>
											<?php echo $src_view; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above ?>
										</td>
										<td class="crtoday-text-md crtoday-text-right" title="<?php echo esc_attr(number_format_i18n($src_rows)); ?>"><?php echo esc_html(self::format_number($src_rows)); ?></td>
										<td class="crtoday-text-md crtoday-text-right" title="<?php echo esc_attr(number_format_i18n($src_snapshots)); ?>"><?php echo esc_html(self::format_number($src_snapshots)); ?></td>
										<td class="crtoday-text-md crtoday-text-right"><?php echo esc_html((string) $src_days); ?></td>
										<td class="crtoday-text-sm" style="white-space:nowrap;"><?php echo esc_html($range_label); ?></td>
										<td>
											<div style="display:flex;align-items:center;gap:6px;">
												<div class="crtoday-progress-track crtoday-progress-track--sm" style="flex:1;">
													<div class="crtoday-progress-bar" style="width:<?php echo esc_attr((string) $src_pct); ?>%;background:<?php echo esc_attr($src_color); ?>;"></div>
												</div>
												<span class="crtoday-text-xs" style="color:#1d2327;"><?php echo esc_html($src_pct . '%'); ?></span>
											</div>
										</td>
									</tr>
								<?php endforeach; ?>
							</tbody>
						</table>
					<?php endif; ?>

					<?php if ($db_total_rows > 0) : ?>
						<div style="display:flex;gap:16px;padding:16px;font-size:11.5px;color:#1d2327;">
							<span>
								<strong><?php esc_html_e('Stored rates:', 'currencyrate-today-currency-blocks'); ?></strong>
								<?php echo esc_html(self::format_number($db_rates_rows)); ?>
							</span>
							<span>
								<strong><?php esc_html_e('Sources tracked:', 'currencyrate-today-currency-blocks'); ?></strong>
								<?php echo esc_html(self::format_number($db_sources_count)); ?>
							</span>
							<span>
								<strong><?php esc_html_e('Storage used:', 'currencyrate-today-currency-blocks'); ?></strong>
								<?php echo esc_html(self::format_bytes($db_total_bytes)); ?>
							</span>
						</div>
					<?php endif; ?>

					<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:16px" onsubmit="return confirm('<?php echo esc_js(__('Permanently delete stored rates and temporary data? Your settings will be kept.', 'currencyrate-today-currency-blocks')); ?>');">
						<input type="hidden" name="action" value="crtoday_delete_data" />
						<?php wp_nonce_field('crtoday_delete_data'); ?>
						<button type="submit" class="button crtoday-btn-danger-fill">
							<span class="dashicons dashicons-warning"></span>
							<?php esc_html_e('Delete all data', 'currencyrate-today-currency-blocks'); ?>
						</button>
					</form>
				</div>

			</div>
		</div>
	<?php
	}

	/* ================================================================
	 * Sources tab
	 * ================================================================ */

	private static function render_sources_tab(): void
	{
		$locals     = self::get_local_sources();
		$builtin_metrics = self::get_builtin_source_metrics_with_fallback();
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only flash flags from admin redirect.
		$flash_saved     = isset($_GET['crtoday_source_saved']);
		$flash_deleted   = isset($_GET['crtoday_source_deleted']);
		$flash_refreshed = isset($_GET['crtoday_source_refreshed']);
		// phpcs:enable
		$now = time();
		$wp_timezone_label = self::wp_timezone_label();
	?>

		<?php self::render_flash($flash_saved, __('Source saved successfully.', 'currencyrate-today-currency-blocks')); ?>
		<?php self::render_flash($flash_deleted, __('Source deleted.', 'currencyrate-today-currency-blocks'), 'warning'); ?>
		<?php self::render_flash($flash_refreshed, __('Source date refreshed.', 'currencyrate-today-currency-blocks')); ?>

		<div class="crtoday-sources-wrap crtoday-admin-tab-container">
			<div class="crtoday-sc-folder">
				<nav class="crtoday-sc-nav" role="tablist">
					<button type="button" role="tab" class="crtoday-sc-nav-btn is-active" aria-selected="true" data-pane="builtin">
						<span class="dashicons dashicons-cloud"></span>
						<?php esc_html_e('Built-in Sources', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="local">
						<span class="dashicons dashicons-database-view"></span>
						<?php esc_html_e('Local Sources', 'currencyrate-today-currency-blocks'); ?>
						<?php if ($locals !== []) : ?>
							<span class="crtoday-nav-count">(<?php echo esc_html((string) count($locals)); ?>)</span>
						<?php endif; ?>
					</button>
				</nav>

				<!-- ═══ Pane: Built-in Sources ═══ -->
				<div class="crtoday-sc-pane is-active crtoday-pane-flush" data-pane="builtin" role="tabpanel">
					<table class="crtoday-table-noborder widefat striped">
						<thead>
							<tr>
								<th style="width:100px;"><?php esc_html_e('ID', 'currencyrate-today-currency-blocks'); ?></th>
								<th><?php esc_html_e('Name', 'currencyrate-today-currency-blocks'); ?></th>
								<th style="width:70px;"><?php esc_html_e('Base', 'currencyrate-today-currency-blocks'); ?></th>
								<th><?php esc_html_e('Deal', 'currencyrate-today-currency-blocks'); ?></th>
								<th style="width:160px;"><?php esc_html_e('Last requested', 'currencyrate-today-currency-blocks'); ?></th>
								<th style="width:220px;"><?php esc_html_e('Source updated', 'currencyrate-today-currency-blocks'); ?></th>
							</tr>
						</thead>
						<tbody>
							<?php foreach (self::sources_registry() as $id => $meta) : ?>
								<?php
								$deals_count = count($meta['deals']);
								$deal_index = 0;
								?>
								<?php foreach ($meta['deals'] as $deal_id => $deal_meta) : ?>
									<?php
									$metric_key = $id . ':' . $deal_id;
									$metric = $builtin_metrics[$metric_key] ?? null;
									$requested_at = (int) ($metric['requested_at'] ?? 0);
									$api_ts = (int) ($metric['api_ts'] ?? 0);

									$requested_relative = '—';
									if ($requested_at > 0) {
										if ($requested_at <= $now) {
											$requested_relative = sprintf(
												/* translators: %s: human-readable relative time */
												__('%s ago', 'currencyrate-today-currency-blocks'),
												human_time_diff($requested_at, $now)
											);
										} else {
											$requested_relative = sprintf(
												/* translators: %s: human-readable relative time */
												__('in %s', 'currencyrate-today-currency-blocks'),
												human_time_diff($now, $requested_at)
											);
										}
									}

									$requested_local = self::format_wp_local_datetime($requested_at);
									$api_local = self::format_wp_local_datetime($api_ts);
									$is_first_deal = ($deal_index === 0);
									$deal_index++;
									?>
									<tr>
										<?php if ($is_first_deal) : ?>
											<td<?php echo $deals_count > 1 ? ' rowspan="' . esc_attr((string) $deals_count) . '" style="vertical-align:middle;"' : ''; ?>><code style="font-size:16px;font-weight:700;"><?php echo esc_html($id); ?></code></td>
											<td<?php echo $deals_count > 1 ? ' rowspan="' . esc_attr((string) $deals_count) . '" style="vertical-align:middle;"' : ''; ?>><?php echo (isset($meta['emoji']) ? wp_kses_post(wp_staticize_emoji($meta['emoji'])) . ' ' : '') . esc_html((string) $meta['label']); ?></td>
											<td<?php echo $deals_count > 1 ? ' rowspan="' . esc_attr((string) $deals_count) . '" style="vertical-align:middle;"' : ''; ?>><code><?php echo esc_html($meta['base']); ?></code></td>
										<?php endif; ?>
										<td><?php echo esc_html(self::translate_deal_label((string) $deal_meta['label']) . ' (' . $deal_id . ')'); ?></td>
										<td>
											<span title="<?php echo esc_attr($requested_local); ?>"><?php echo esc_html($requested_relative); ?></span>
										</td>
										<td><code><?php echo esc_html($api_local); ?></code></td>
									</tr>
								<?php endforeach; ?>
							<?php endforeach; ?>
						</tbody>
					</table>

					<p class="description" style="margin:10px 16px 16px;">
						<?php
						echo esc_html(
								sprintf(
									/* translators: %s: WordPress timezone label */
									__('Timestamps are stored in UTC and shown here in your WordPress timezone (%s).', 'currencyrate-today-currency-blocks'),
								$wp_timezone_label
							)
						);
						?>
					</p>

					<div style="padding:0 16px 16px;">
						<form action="options.php" method="post" class="crtoday-interval-form crtoday-pane-flush" id="crtoday-interval-form">
							<?php settings_fields('crtoday_settings_group'); ?>
							<p class="crtoday-interval-label" id="crtoday-api-interval-label" style="margin:0;">
								<?php esc_html_e('Update interval', 'currencyrate-today-currency-blocks'); ?>
							</p>
							<div class="crtoday-interval-row">
								<div class="crtoday-interval-group" role="radiogroup" aria-labelledby="crtoday-api-interval-label" aria-describedby="crtoday-api-interval-help">
										<?php
										$config = self::get_config();
										foreach (self::ALLOWED_API_INTERVAL as $min) :
											$input_id = 'crtoday-api-interval-' . (string) $min;
										?>
											<label class="crtoday-interval-option" for="<?php echo esc_attr($input_id); ?>">
												<input
													type="radio"
													name="<?php echo esc_attr(self::OPTION_NAME); ?>[api_interval]"
													id="<?php echo esc_attr($input_id); ?>"
													value="<?php echo esc_attr((string) $min); ?>"
													<?php checked($config['api_interval'], $min); ?>
													onchange="this.form.submit()" />
												<span class="crtoday-interval-chip"><?php
												/* translators: %d: number of minutes */
												echo esc_html(sprintf(__('%d min', 'currencyrate-today-currency-blocks'), $min));
												?></span>
											</label>
										<?php endforeach; ?>
								</div>
								<div class="crtoday-interval-meta">
									<p class="description" id="crtoday-api-interval-help" style="margin:0;"><?php esc_html_e('How often the plugin checks for new rates. Takes effect immediately.', 'currencyrate-today-currency-blocks'); ?></p>
								</div>
							</div>
						</form>
					</div>
				</div>

				<!-- ═══ Pane: Local Sources ═══ -->
				<div class="crtoday-sc-pane crtoday-pane-flush" data-pane="local" role="tabpanel">
					<?php if ($locals === []) : ?>
						<p style="color:#666;margin:0;padding:16px;">
							<?php esc_html_e('No local sources yet. Add one to enter your own rates.', 'currencyrate-today-currency-blocks'); ?>
							<br />
							<?php esc_html_e('Useful for currency exchange offices, banks, and online stores.', 'currencyrate-today-currency-blocks'); ?>
						</p>
					<?php else : ?>
						<table class="widefat striped" style="border:none">
							<thead>
								<tr>
									<th style="width:100px;"><?php esc_html_e('ID', 'currencyrate-today-currency-blocks'); ?></th>
									<th><?php esc_html_e('Name', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:70px;"><?php esc_html_e('Base', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:80px;"><?php esc_html_e('Mode', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:60px;text-align:center;"><?php esc_html_e('Pairs', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:140px;"><?php esc_html_e('Freshness', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:110px;text-align:right;"><?php esc_html_e('Actions', 'currencyrate-today-currency-blocks'); ?></th>
								</tr>
							</thead>
							<tbody>
								<?php foreach ($locals as $local_id => $ls) :
									$age_s = $now - (int) ($ls['updated_at'] ?? 0);
									$age_h = $age_s / 3600;
									if ($age_h < 24) {
										$freshness_color = '#00a32a'; /* green */
										$freshness_label = __('Fresh', 'currencyrate-today-currency-blocks');
									} elseif ($age_h < 72) {
										$freshness_color = '#dba617'; /* yellow */
										$freshness_label = __('Aging', 'currencyrate-today-currency-blocks');
									} else {
										$freshness_color = '#d63638'; /* red */
										$freshness_label = __('Stale', 'currencyrate-today-currency-blocks');
									}
									$updated_human = esc_html(human_time_diff((int) ($ls['updated_at'] ?? 0)) . ' ' . __('ago', 'currencyrate-today-currency-blocks'));
									$updated_exact = esc_attr(wp_date('Y-m-d H:i:s', (int) ($ls['updated_at'] ?? 0)));
								?>
									<tr>
										<td><code style="font-size:16px;font-weight:700;">LOCAL_<?php echo esc_html((string) $local_id); ?></code></td>
										<td><?php echo esc_html($ls['name']); ?></td>
										<td><code><?php echo esc_html($ls['base']); ?></code></td>
										<td class="crtoday-col-hide-mobile"><?php echo esc_html($ls['pricing_mode'] === 'bidask' ? 'Bid/Ask' : 'Mid'); ?></td>
										<td class="crtoday-col-hide-mobile" style="text-align:center;"><?php echo esc_html((string) count($ls['rates'])); ?></td>
										<td>
											<span class="crtoday-label-inline" title="<?php echo esc_attr( $updated_exact ); ?>">
												<span class="crtoday-color-dot" style="background:<?php echo esc_attr($freshness_color); ?>;flex-shrink:0;"></span>
												<span style="font-size:16px;color:#50575e;white-space:nowrap;"><?php echo esc_html( $updated_human ); ?></span>
											</span>
										</td>
										<td class="crtoday-text-right">
											<div style="display:inline-flex;gap:4px;">
												<button type="button" class="button button-small crtoday-edit-local-source"
													title="<?php esc_attr_e('Edit', 'currencyrate-today-currency-blocks'); ?>"
													data-source-id="<?php echo esc_attr((string) $local_id); ?>"
													data-source-json="<?php echo esc_attr(wp_json_encode(array_merge($ls, ['id' => $local_id]))); ?>">
													<span class="dashicons dashicons-edit"></span>
												</button>
												<button type="button" class="button button-small crtoday-refresh-local-source"
													title="<?php esc_attr_e('Refresh', 'currencyrate-today-currency-blocks'); ?>"
													data-source-id="<?php echo esc_attr((string) $local_id); ?>">
													<span class="dashicons dashicons-update"></span>
												</button>
												<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="crtoday-action-form--inline"
													onsubmit="return confirm('<?php echo esc_js(__('Delete this source? This cannot be undone.', 'currencyrate-today-currency-blocks')); ?>');">
													<input type="hidden" name="action" value="crtoday_delete_local_source" />
													<input type="hidden" name="local_source_id" value="<?php echo esc_attr((string) $local_id); ?>" />
													<?php wp_nonce_field('crtoday_delete_local_source'); ?>
													<button type="submit" class="button button-small" title="<?php esc_attr_e('Delete', 'currencyrate-today-currency-blocks'); ?>" style="color:#d63638;border-color:#d63638;">
														<span class="dashicons dashicons-trash"></span>
													</button>
												</form>
											</div>
										</td>
									</tr>
								<?php endforeach; ?>
							</tbody>
						</table>
					<?php endif; ?>

					<?php
					$local_count   = count(self::get_local_sources());
					$limit_reached = $local_count >= self::MAX_SOURCES;
					?>
					<button type="button" class="button button-primary" style="margin:16px;" id="crtoday-add-local-source"<?php echo $limit_reached ? ' disabled' : ''; ?>>
						<span class="dashicons dashicons-plus-alt2"></span>
						<?php esc_html_e('Add local source', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<?php if ($limit_reached) : ?>
						<p class="description" style="margin:0 16px 16px;">
							<?php
							printf(
								/* translators: %d: maximum number of custom sources allowed */
								esc_html__('You can create up to %d local sources.', 'currencyrate-today-currency-blocks'),
								absint( self::MAX_SOURCES )
							);
							?>
						</p>
					<?php endif; ?>
				</div>
			</div>
		</div>

		<!-- ═══ Local source dialog (hidden by default) ═══ -->
		<div id="crtoday-local-source-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;z-index:100000;background:rgba(0,0,0,.6);overflow:auto;">
			<div style="background:#fff;max-width:700px;margin:40px auto;border-radius:8px;box-shadow:0 20px 60px rgba(0,0,0,.3);overflow:hidden;">
				<div style="background:#1d2327;padding:16px 20px;display:flex;justify-content:space-between;align-items:center;">
					<h2 id="crtoday-dialog-title" style="margin:0;font-size:16px;font-weight:600;color:#fff;">
						<?php esc_html_e('Add local source', 'currencyrate-today-currency-blocks'); ?>
					</h2>
					<button type="button" id="crtoday-dialog-close" style="background:none;border:none;color:#ccc;font-size:24px;cursor:pointer;line-height:1;padding:0;">&times;</button>
				</div>
				<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" id="crtoday-local-source-form">
					<input type="hidden" name="action" value="crtoday_save_local_source" />
					<?php wp_nonce_field('crtoday_save_local_source'); ?>
					<input type="hidden" name="local_source_id" id="crtoday-ls-id" value="0" />
					<div style="padding:20px;">
						<table class="form-table" role="presentation">
							<tr>
								<th scope="row"><label for="crtoday-ls-name"><?php esc_html_e('Source name', 'currencyrate-today-currency-blocks'); ?></label></th>
								<td><input type="text" name="local_source_name" id="crtoday-ls-name" class="regular-text" required maxlength="100" placeholder="<?php esc_attr_e('e.g. My exchange', 'currencyrate-today-currency-blocks'); ?>" /></td>
							</tr>
							<tr>
								<th scope="row"><?php esc_html_e('Base currency', 'currencyrate-today-currency-blocks'); ?></th>
								<td>
									<button type="button" class="crtoday-currency-picker-btn" id="crtoday-ls-base-btn" data-crtoday-picker="true" data-target="crtoday-ls-base" data-value="" aria-haspopup="listbox" aria-expanded="false">
										<img class="is-hidden crtoday-picker-flag" src="" width="20" height="20" alt="" />
										<span class="crtoday-picker-code"><?php esc_html_e('Select…', 'currencyrate-today-currency-blocks'); ?></span>
										<span class="crtoday-picker-chevron">▾</span>
									</button>
									<input type="hidden" name="local_source_base" id="crtoday-ls-base" value="" />
								</td>
							</tr>
							<tr>
								<th scope="row"><?php esc_html_e('Pricing mode', 'currencyrate-today-currency-blocks'); ?></th>
								<td>
									<fieldset>
										<label style="margin-right:16px;">
											<input type="radio" name="local_source_mode" value="mid" checked onchange="crtodayTogglePricingMode()" />
											<?php esc_html_e('Mid-rate only', 'currencyrate-today-currency-blocks'); ?>
										</label>
										<label>
											<input type="radio" name="local_source_mode" value="bidask" onchange="crtodayTogglePricingMode()" />
											<?php esc_html_e('Bid / Ask', 'currencyrate-today-currency-blocks'); ?>
										</label>
									</fieldset>
								</td>
							</tr>
						</table>

						<hr style="margin:16px 0;border:none;border-top:1px solid #f0f0f1;" />

						<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
							<h3 style="margin:0;font-size:14px;">
								<span class="dashicons dashicons-chart-line"></span>
								<?php esc_html_e('Exchange rates', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<button type="button" class="button button-small" id="crtoday-add-rate-row">
								<span class="dashicons dashicons-plus"></span>
								<?php esc_html_e('Add currency', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>

						<table class="crtoday-table widefat" id="crtoday-rates-table">
							<thead>
								<tr>
									<th style="width:220px;"><?php esc_html_e('Currency', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="crtoday-col-mid"><?php esc_html_e('Rate (mid)', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="is-hidden crtoday-col-bid"><?php esc_html_e('Bid', 'currencyrate-today-currency-blocks'); ?></th>
									<th class="is-hidden crtoday-col-ask"><?php esc_html_e('Ask', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:40px;"></th>
								</tr>
							</thead>
							<tbody id="crtoday-rates-body">
								<!-- Rows added via JS -->
							</tbody>
						</table>
					</div>
					<div style="padding:16px 20px;background:#f6f7f7;border-top:1px solid #ddd;display:flex;justify-content:flex-end;gap:8px;">
						<button type="button" class="button" id="crtoday-dialog-cancel"><?php esc_html_e('Cancel', 'currencyrate-today-currency-blocks'); ?></button>
						<button type="submit" class="button button-primary" id="crtoday-dialog-save" disabled><?php esc_html_e('Save source', 'currencyrate-today-currency-blocks'); ?></button>
					</div>
				</form>
			</div>
		</div>

		<?php
		$refresh_nonce           = wp_create_nonce('crtoday_refresh_local_source');
		$add_local_source_label  = __('Add local source', 'currencyrate-today-currency-blocks');
		$edit_local_source_label = __('Edit local source', 'currencyrate-today-currency-blocks');
		$select_label            = __('Select…', 'currencyrate-today-currency-blocks');

		ob_start();
		?>(function() {
	var dialog = document.getElementById('crtoday-local-source-dialog');
	var form = document.getElementById('crtoday-local-source-form');
	var tbody = document.getElementById('crtoday-rates-body');
	var title = document.getElementById('crtoday-dialog-title');
	var idField = document.getElementById('crtoday-ls-id');
	var baseBtnEl = document.getElementById('crtoday-ls-base-btn');
	var baseInputEl = document.getElementById('crtoday-ls-base');

	var currencies = window.crtodayAdminCurrencies || [];
	var flagBaseUrl = window.crtodayFlagBaseUrl || '';
	var allowedCodes = {};
	currencies.forEach(function(c) { allowedCodes[c.code] = c; });
	var selectLabel = <?php echo wp_json_encode( $select_label ); ?>;
	var rateRowIdCounter = 0;

	function findCurrency(code) {
		return allowedCodes[code ? code.toUpperCase() : ''] || null;
	}

	/* Update a picker button to show a specific currency (or reset to placeholder). */
	function setPickerValue(btn, hiddenInput, code) {
		var info = findCurrency(code);
		var flagImg = btn.querySelector('.crtoday-picker-flag');
		var codeSpan = btn.querySelector('.crtoday-picker-code');
		if (info) {
			btn.dataset.value = info.code;
			if (flagImg) {
				flagImg.src = flagBaseUrl + info.flag + '.webp';
				flagImg.style.display = '';
			}
			if (codeSpan) codeSpan.textContent = info.code;
		} else {
			btn.dataset.value = '';
			if (flagImg) flagImg.style.display = 'none';
			if (codeSpan) codeSpan.textContent = selectLabel;
		}
		if (hiddenInput) hiddenInput.value = info ? info.code : '';
	}

	function openDialog() {
		dialog.style.display = '';
		document.body.style.overflow = 'hidden';
	}

	function closeDialog() {
		dialog.style.display = 'none';
		document.body.style.overflow = '';
	}

	document.getElementById('crtoday-add-local-source').addEventListener('click', function() {
		title.textContent = <?php echo wp_json_encode( $add_local_source_label ); ?>;
		idField.value = '0';
		form.querySelector('[name="local_source_name"]').value = '';
		setPickerValue(baseBtnEl, baseInputEl, '');
		form.querySelector('[name="local_source_mode"][value="mid"]').checked = true;
		tbody.innerHTML = '';
		addRateRow('', '', '', '');
		addRateRow('', '', '', '');
		addRateRow('', '', '', '');
		crtodayTogglePricingMode();
		validateForm();
		openDialog();
	});

	document.querySelectorAll('.crtoday-edit-local-source').forEach(function(btn) {
		btn.addEventListener('click', function() {
			var data = JSON.parse(this.getAttribute('data-source-json'));
			title.textContent = <?php echo wp_json_encode( $edit_local_source_label ); ?>;
			idField.value = data.id;
			form.querySelector('[name="local_source_name"]').value = data.name || '';
			setPickerValue(baseBtnEl, baseInputEl, data.base || '');
			var modeRadio = form.querySelector('[name="local_source_mode"][value="' + (data.pricing_mode || 'mid') + '"]');
			if (modeRadio) modeRadio.checked = true;
			tbody.innerHTML = '';
			if (data.rates && typeof data.rates === 'object') {
				Object.keys(data.rates).forEach(function(code) {
					var r = data.rates[code];
					addRateRow(code, r.mid || '', r.bid || '', r.ask || '');
				});
			}
			if (tbody.children.length === 0) {
				addRateRow('', '', '', '');
			}
			crtodayTogglePricingMode();
			validateForm();
			openDialog();
		});
	});

	document.getElementById('crtoday-dialog-close').addEventListener('click', closeDialog);
	document.getElementById('crtoday-dialog-cancel').addEventListener('click', closeDialog);
	dialog.addEventListener('click', function(e) {
		if (e.target === dialog) closeDialog();
	});

	document.getElementById('crtoday-add-rate-row').addEventListener('click', function() {
		addRateRow('', '', '', '');
	});

	var saveBtn = document.getElementById('crtoday-dialog-save');

	function validateForm() {
		var name = form.querySelector('[name="local_source_name"]').value.trim();
		var baseVal = (baseInputEl.value || '').trim().toUpperCase();
		var baseOk = baseVal && !!findCurrency(baseVal);
		var isBidAsk = document.querySelector('[name="local_source_mode"][value="bidask"]').checked;
		var hasValidRate = false;
		var rows = tbody.querySelectorAll('tr');
		for (var i = 0; i < rows.length; i++) {
			var codeInput = rows[i].querySelector('[name="rate_code[]"]');
			if (!codeInput || !codeInput.value.trim()) continue;
			if (!findCurrency(codeInput.value.trim())) continue;
			if (isBidAsk) {
				var bid = rows[i].querySelector('[name="rate_bid[]"]');
				var ask = rows[i].querySelector('[name="rate_ask[]"]');
				if (bid && ask && (bid.value.trim() || ask.value.trim())) {
					hasValidRate = true;
					break;
				}
			} else {
				var mid = rows[i].querySelector('[name="rate_mid[]"]');
				if (mid && mid.value.trim()) {
					hasValidRate = true;
					break;
				}
			}
		}
		saveBtn.disabled = !(name && baseOk && hasValidRate);
	}

	form.addEventListener('input', validateForm);
	form.addEventListener('change', validateForm);

	var ratesObserver = new MutationObserver(validateForm);
	ratesObserver.observe(tbody, { childList: true });

	function addRateRow(code, mid, bid, ask) {
		var rowId = 'crtoday-ls-rate-' + (++rateRowIdCounter);
		var tr = document.createElement('tr');

		/* Currency picker cell */
		var tdCurrency = document.createElement('td');
		var pickerBtn = document.createElement('button');
		pickerBtn.type = 'button';
		pickerBtn.className = 'crtoday-currency-picker-btn';
		pickerBtn.setAttribute('data-crtoday-picker', 'true');
		pickerBtn.setAttribute('data-target', rowId);
		pickerBtn.setAttribute('data-value', '');
		pickerBtn.setAttribute('aria-haspopup', 'listbox');
		pickerBtn.setAttribute('aria-expanded', 'false');

		var flagImg = document.createElement('img');
		flagImg.className = 'crtoday-picker-flag';
		flagImg.width = 20;
		flagImg.height = 20;
		flagImg.alt = '';
		flagImg.style.display = 'none';
		pickerBtn.appendChild(flagImg);

		var codeSpan = document.createElement('span');
		codeSpan.className = 'crtoday-picker-code';
		codeSpan.textContent = selectLabel;
		pickerBtn.appendChild(codeSpan);

		var chevron = document.createElement('span');
		chevron.className = 'crtoday-picker-chevron';
		chevron.textContent = '▾';
		pickerBtn.appendChild(chevron);

		var hiddenInput = document.createElement('input');
		hiddenInput.type = 'hidden';
		hiddenInput.name = 'rate_code[]';
		hiddenInput.id = rowId;
		hiddenInput.value = '';

		tdCurrency.appendChild(pickerBtn);
		tdCurrency.appendChild(hiddenInput);

		/* Set initial value if provided */
		if (code) {
			setPickerValue(pickerBtn, hiddenInput, code);
		}

		/* Bind picker via global function exposed by currency-picker.js */
		if (window.crtodayBindPicker) {
			window.crtodayBindPicker(pickerBtn);
		}

		/* Re-validate when currency changes */
		hiddenInput.addEventListener('input', validateForm);

		/* Rate cells */
		var tdMid = document.createElement('td');
		tdMid.className = 'crtoday-col-mid';
		tdMid.innerHTML = '<input type="text" name="rate_mid[]" value="' + esc(mid) + '" style="width:100%;max-width:160px;min-width:80px;box-sizing:border-box;" placeholder="0.0000" />';

		var tdBid = document.createElement('td');
		tdBid.className = 'crtoday-col-bid';
		tdBid.style.display = 'none';
		tdBid.innerHTML = '<input type="text" name="rate_bid[]" value="' + esc(bid) + '" style="width:100%;max-width:160px;min-width:80px;box-sizing:border-box;" placeholder="0.0000" />';

		var tdAsk = document.createElement('td');
		tdAsk.className = 'crtoday-col-ask';
		tdAsk.style.display = 'none';
		tdAsk.innerHTML = '<input type="text" name="rate_ask[]" value="' + esc(ask) + '" style="width:100%;max-width:160px;min-width:80px;box-sizing:border-box;" placeholder="0.0000" />';

		var tdRemove = document.createElement('td');
		var removeBtn = document.createElement('button');
		removeBtn.type = 'button';
		removeBtn.className = 'button button-link-delete crtoday-remove-row';
		removeBtn.style.color = '#d63638';
		removeBtn.textContent = '×';
		removeBtn.addEventListener('click', function() { tr.remove(); });
		tdRemove.appendChild(removeBtn);

		tr.appendChild(tdCurrency);
		tr.appendChild(tdMid);
		tr.appendChild(tdBid);
		tr.appendChild(tdAsk);
		tr.appendChild(tdRemove);
		tbody.appendChild(tr);
		validateForm();
	}

	function esc(s) {
		var d = document.createElement('div');
		d.appendChild(document.createTextNode(s));
		return d.innerHTML;
	}
})();

(function() {
	var refreshNonce = <?php echo wp_json_encode( $refresh_nonce ); ?>;
	var ajaxUrl = <?php echo wp_json_encode( admin_url('admin-ajax.php') ); ?>;

	document.querySelectorAll('.crtoday-refresh-local-source').forEach(function(btn) {
		btn.addEventListener('click', function() {
			var sourceId = btn.dataset.sourceId;
			var icon = btn.querySelector('.dashicons');
			btn.disabled = true;
			if (icon) icon.style.animation = 'rotation 1s linear infinite';

			var body = new URLSearchParams();
			body.append('action', 'crtoday_refresh_local_source');
			body.append('local_source_id', sourceId);
			body.append('_wpnonce', refreshNonce);

			fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: body })
				.then(function(r) { return r.json(); })
				.then(function(data) {
					if (data.success) {
						var row = btn.closest('tr');
						if (row) {
							var dot = row.querySelector('.crtoday-color-dot');
							var timeSpan = row.querySelector('.crtoday-label-inline > span:last-child');
							if (dot) dot.style.background = '#00a32a';
							if (timeSpan) timeSpan.textContent = data.data.human;
						}
					}
				})
				.catch(function() {})
				.finally(function() {
					btn.disabled = false;
					if (icon) icon.style.animation = '';
				});
		});
	});
})();

function crtodayTogglePricingMode() {
	var isBidAsk = document.querySelector('[name="local_source_mode"][value="bidask"]').checked;
	document.querySelectorAll('.crtoday-col-mid').forEach(function(el) {
		el.style.display = isBidAsk ? 'none' : '';
	});
	document.querySelectorAll('.crtoday-col-bid, .crtoday-col-ask').forEach(function(el) {
		el.style.display = isBidAsk ? '' : 'none';
	});
}
<?php
		$sources_js = ob_get_clean();
		wp_add_inline_script( 'crtoday-admin-inline', $sources_js );
	}

	/* ================================================================
	 * About tab
	 * ================================================================ */

	private static function render_admin_footer(): void
	{
	?>
		<footer class="crtoday-admin-footer">
			<div class="crtoday-admin-footer__links">
				<a href="https://wordpress.org/plugins/currencyrate-today-currency-blocks/" target="_blank" rel="noopener"><?php esc_html_e('Plugin page', 'currencyrate-today-currency-blocks'); ?><span class="dashicons dashicons-external"></span></a>
				<span class="crtoday-admin-footer__sep">&middot;</span>
				<a href="https://wordpress.org/support/plugin/currencyrate-today-currency-blocks/" target="_blank" rel="noopener"><?php esc_html_e('Support', 'currencyrate-today-currency-blocks'); ?><span class="dashicons dashicons-external"></span></a>
				<span class="crtoday-admin-footer__sep">&middot;</span>
				<a href="https://wordpress.org/support/plugin/currencyrate-today-currency-blocks/reviews/#new-post" target="_blank" rel="noopener"><?php esc_html_e('Review', 'currencyrate-today-currency-blocks'); ?><span class="dashicons dashicons-external"></span></a>
				<span class="crtoday-admin-footer__sep">&middot;</span>
				<a href="https://currencyrate.today/" target="_blank" rel="noopener"><?php esc_html_e('CurrencyRate.Today', 'currencyrate-today-currency-blocks'); ?><span class="dashicons dashicons-external"></span></a>
			</div>
		</footer>
	<?php
	}

	/* ================================================================
	 * Currencies tab
	 * ================================================================ */

	/**
	 * Load ISO 4217 data from JSON file.
	 * @return array<string,array{number:string,minorUnit:string}>
	 */
	private static function get_iso_currency_data(): array
	{
		static $data = null;
		if ($data !== null) {
			return $data;
		}

		$path = CRTODAY_PLUGIN_DIR . 'assets/data/iso-currencies.json';
		if (! file_exists($path)) {
			$data = [];
			return $data;
		}

		$json = file_get_contents($path);
		if ($json === false) {
			$data = [];
			return $data;
		}

		$parsed = json_decode($json, true);
		if (! is_array($parsed) || ! isset($parsed['active'])) {
			$data = [];
			return $data;
		}

		$data = [];
		foreach ($parsed['active'] as $entry) {
			$code = $entry['code'] ?? '';
			if ($code !== '') {
				$data[$code] = [
					'number'    => $entry['number'] ?? '',
					'minorUnit' => $entry['minorUnit'] ?? '',
				];
			}
		}

		return $data;
	}

	/**
	 * Get merged currency data for the Currencies tab.
	 * Our built-in registry is primary; ISO data supplements it.
	 * @return list<array{code:string,name:string,symbol:string|null,flag:string,group:string,number:string,minorUnit:string}>
	 */
	public static function get_merged_currency_list(): array
	{
		$registry = self::currencies();
		$isoData  = self::get_iso_currency_data();

		$result = [];
		foreach ($registry as $code => $meta) {
			$iso = $isoData[$code] ?? [];
			$result[] = [
				'code'      => $code,
				'name'      => $meta['name'],
				'symbol'    => self::get_currency_symbol($code),
				'flag'      => $meta['flag'],
				'group'     => $meta['group'],
				'number'    => $iso['number'] ?? '',
				'minorUnit' => $iso['minorUnit'] ?? '',
			];
		}

		return $result;
	}

	private static function render_currencies_tab(): void
	{
		$currencies = self::get_merged_currency_list();
		$flags_url  = CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/';
		$group_labels = self::get_currency_group_labels();
		$copy_failed_message = __('Copy failed', 'currencyrate-today-currency-blocks');
	?>
		<div class="crtoday-admin-tab-container">
		<div class="crtoday-currencies-tab">
			<div class="postbox" style="margin:0;">
				<div class="postbox-header">
					<h2 style="padding:8px 16px;margin:0;display:flex;align-items:center;gap:8px;">
						<span class="dashicons dashicons-money-alt"></span>
						<?php esc_html_e('Currency Registry', 'currencyrate-today-currency-blocks'); ?>
						<span style="font-weight:400;font-size:16px;color:#646970;">
							(<?php echo esc_html((string) count($currencies)); ?>)
						</span>
					</h2>
				</div>
				<div class="inside" style="margin:0;padding:0;">
					<div style="padding:16px;border-bottom:1px solid #c3c4c7;background:#f6f7f7;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
						<input type="search" id="crtoday-currency-search" class="crtoday-input"
							placeholder="<?php esc_attr_e('Search currencies...', 'currencyrate-today-currency-blocks'); ?>"
							style="width:100%;max-width:320px;" />
						<select id="crtoday-currency-group-filter" class="crtoday-input" style="width:100%;max-width:240px;">
							<option value=""><?php esc_html_e('All groups', 'currencyrate-today-currency-blocks'); ?></option>
							<?php foreach ($group_labels as $key => $label) : ?>
								<option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($label); ?></option>
							<?php endforeach; ?>
						</select>
					</div>
					<div style="overflow-x:auto;">
						<table class="wp-list-table widefat striped" id="crtoday-currencies-table" style="margin:0;">
							<thead>
								<tr>
									<th style="width:50px;text-align:center;"><?php esc_html_e('Flag', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:60px;"><?php esc_html_e('Code', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:70px;"><?php esc_html_e('Symbol', 'currencyrate-today-currency-blocks'); ?></th>
									<th><?php esc_html_e('Name', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:60px;"><?php esc_html_e('ISO №', 'currencyrate-today-currency-blocks'); ?></th>
									<th style="width:60px;"><?php esc_html_e('Dec', 'currencyrate-today-currency-blocks'); ?></th>
								</tr>
							</thead>
							<tbody>
								<?php foreach ($currencies as $c) : ?>
									<tr data-group="<?php echo esc_attr($c['group']); ?>" data-search="<?php echo esc_attr(strtolower($c['code'] . ' ' . $c['name'] . ' ' . ($c['symbol'] ?? ''))); ?>">
										<td class="crtoday-text-center">
											<img src="<?php echo esc_url($flags_url . $c['flag'] . '.webp'); ?>"
												alt="<?php echo esc_attr($c['code']); ?>"
												style="width:24px;height:24px;border-radius:2px;vertical-align:middle;" />
										</td>
										<td>
											<code style="background:#f0f0f1;padding:2px 6px;border-radius:3px;font-weight:600;">
												<?php echo esc_html($c['code']); ?>
											</code>
										</td>
										<td>
											<?php if ($c['symbol']) : ?>
												<span class="crtoday-symbol-cell" style="display:inline-flex;align-items:center;gap:4px;">
													<code style="background:#f0f0f1;padding:2px 6px;border-radius:3px;font-size:14px;">
														<?php echo esc_html($c['symbol']); ?>
													</code>
													<button type="button"
														class="crtoday-copy-symbol"
														data-symbol="<?php echo esc_attr($c['symbol']); ?>"
														title="<?php esc_attr_e('Copy symbol', 'currencyrate-today-currency-blocks'); ?>"
														style="padding:2px 4px;font-size:16px;line-height:1;cursor:pointer;border:1px solid #c3c4c7;border-radius:3px;background:#fff;">
														<span class="dashicons dashicons-clipboard"></span>
													</button>
												</span>
											<?php else : ?>
												<span style="color:#a7aaad;">—</span>
											<?php endif; ?>
										</td>
										<td><?php echo esc_html($c['name']); ?></td>
										<td>
											<?php if ($c['number']) : ?>
												<code style="background:#f6f7f7;padding:2px 4px;border-radius:2px;font-size:16px;">
													<?php echo esc_html($c['number']); ?>
												</code>
											<?php else : ?>
												<span style="color:#a7aaad;">—</span>
											<?php endif; ?>
										</td>
										<td class="crtoday-text-center">
											<?php if ($c['minorUnit'] !== '') : ?>
												<?php echo esc_html($c['minorUnit']); ?>
											<?php else : ?>
												<span style="color:#a7aaad;">—</span>
											<?php endif; ?>
										</td>
									</tr>
								<?php endforeach; ?>
							</tbody>
						</table>
					</div>
				</div>
			</div>
		</div>
		</div>

		<?php
		ob_start();
		?>(function() {
	var copyFailedMessage = <?php echo wp_json_encode( $copy_failed_message ); ?>;
	var searchInput = document.getElementById('crtoday-currency-search');
	var groupFilter = document.getElementById('crtoday-currency-group-filter');
	var table = document.getElementById('crtoday-currencies-table');
	if (!searchInput || !groupFilter || !table) return;

	var rows = Array.from(table.querySelectorAll('tbody tr'));

	function applyFilters() {
		var query = searchInput.value.toLowerCase().trim();
		var group = groupFilter.value;
		rows.forEach(function(row) {
			var searchText = row.getAttribute('data-search') || '';
			var rowGroup = row.getAttribute('data-group') || '';
			var matchSearch = !query || searchText.includes(query);
			var matchGroup = !group || rowGroup === group;
			row.style.display = matchSearch && matchGroup ? '' : 'none';
		});
	}

	searchInput.addEventListener('input', applyFilters);
	groupFilter.addEventListener('change', applyFilters);

	document.querySelectorAll('.crtoday-copy-symbol').forEach(function(btn) {
		btn.addEventListener('click', function() {
			var symbol = this.getAttribute('data-symbol');
			if (!symbol) return;

			navigator.clipboard.writeText(symbol).then(function() {
				var icon = btn.querySelector('.dashicons');
				if (icon) {
					icon.classList.remove('dashicons-clipboard');
					icon.classList.add('dashicons-yes');
					setTimeout(function() {
						icon.classList.remove('dashicons-yes');
						icon.classList.add('dashicons-clipboard');
					}, 1500);
				}
			}).catch(function(err) {
				console.error(copyFailedMessage, err);
			});
		});
	});
})();
<?php
		$currencies_js = ob_get_clean();
		wp_add_inline_script( 'crtoday-admin-inline', $currencies_js );
	}

	/* ================================================================
	 * Debug tab (CRTODAY_DEBUG only)
	 * ================================================================ */

	private static function render_debug_tab(): void
	{
		$review  = CRToday_Review_Banner::get_state();
		$diag    = self::get_diagnostics();
		$nonce   = wp_create_nonce('crtoday_debug_action');
		$ajax    = admin_url('admin-ajax.php');

		$installed_ago = $review['installed_at'] > 0
			? human_time_diff($review['installed_at']) . ' ago'
			: '—';
		$remind_label  = $review['remind_at'] > 0
			? wp_date('Y-m-d H:i:s', $review['remind_at'])
			: '—';
	?>
		<div class="crtoday-admin-tab-container">

			<div class="notice notice-warning" style="margin:0 0 16px;padding:10px 14px;">
				<p style="margin:0;">
					<span class="dashicons dashicons-warning" style="color:#dba617;margin-right:4px;"></span>
					<strong><?php esc_html_e('Debug mode — visible only when CRTODAY_DEBUG is enabled.', 'currencyrate-today-currency-blocks'); ?></strong>
				</p>
			</div>

			<!-- ── Review Banner ── -->
			<div class="postbox" style="margin-bottom:16px;">
				<div class="postbox-header">
					<h2 class="crtoday-postbox-heading">
						<span class="dashicons dashicons-star-filled"></span>
						<?php esc_html_e('Review Banner', 'currencyrate-today-currency-blocks'); ?>
					</h2>
				</div>
				<div class="inside crtoday-pad-16">
					<table class="widefat striped" style="max-width:460px;">
						<tbody>
							<tr>
								<td class="crtoday-td-bold" style="width:150px;"><?php esc_html_e('Installed', 'currencyrate-today-currency-blocks'); ?></td>
								<td><?php echo esc_html($installed_ago); ?></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Delay (days)', 'currencyrate-today-currency-blocks'); ?></td>
								<td><?php echo esc_html((string) $review['delay_days']); ?></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Dismissed', 'currencyrate-today-currency-blocks'); ?></td>
								<td>
									<span style="color:<?php echo $review['dismissed'] ? '#d63638' : '#00a32a'; ?>;">
										<?php echo $review['dismissed'] ? '✓ Yes' : '✗ No'; ?>
									</span>
								</td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Remind at', 'currencyrate-today-currency-blocks'); ?></td>
								<td><?php echo esc_html($remind_label); ?></td>
							</tr>
						</tbody>
					</table>

					<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;">
						<button type="button" class="button button-primary crtoday-debug-btn" data-action="trigger_review_banner">
							<span class="dashicons dashicons-bell"></span>
							<?php esc_html_e('Trigger banner now', 'currencyrate-today-currency-blocks'); ?>
						</button>
						<button type="button" class="button crtoday-debug-btn" data-action="reset_review_banner">
							<span class="dashicons dashicons-image-rotate"></span>
							<?php esc_html_e('Reset banner state', 'currencyrate-today-currency-blocks'); ?>
						</button>
					</div>
					<p class="description" style="margin-top:8px;">
						<?php esc_html_e('"Trigger" shows the banner on this page. "Reset" clears dismissed/remind state so the banner reappears naturally.', 'currencyrate-today-currency-blocks'); ?>
					</p>

					<div id="crtoday-debug-banner-container" style="margin-top:16px;"></div>
				</div>
			</div>

			<!-- ── Environment ── -->
			<div class="postbox" style="margin-bottom:16px;">
				<div class="postbox-header">
					<h2 class="crtoday-postbox-heading">
						<span class="dashicons dashicons-admin-site-alt3"></span>
						<?php esc_html_e('Environment', 'currencyrate-today-currency-blocks'); ?>
					</h2>
				</div>
				<div class="inside crtoday-pad-16">
					<table class="widefat striped" style="max-width:460px;">
						<tbody>
							<tr>
								<td class="crtoday-td-bold" style="width:150px;"><?php esc_html_e('Plugin version', 'currencyrate-today-currency-blocks'); ?></td>
								<td><code><?php echo esc_html(CRTODAY_PLUGIN_VERSION); ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold">PHP</td>
								<td><code><?php echo esc_html(PHP_VERSION); ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold">WordPress</td>
								<td><code><?php echo esc_html(get_bloginfo('version')); ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold">bcmath</td>
								<td>
									<span style="color:<?php echo extension_loaded('bcmath') ? '#00a32a' : '#d63638'; ?>;">
										<?php echo extension_loaded('bcmath') ? '✓ loaded' : '✗ missing'; ?>
									</span>
								</td>
							</tr>
							<tr>
								<td class="crtoday-td-bold">WP_DEBUG</td>
								<td><code><?php echo defined('WP_DEBUG') && WP_DEBUG ? 'true' : 'false'; ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold">WP_DEBUG_LOG</td>
								<td><code><?php echo defined('WP_DEBUG_LOG') && WP_DEBUG_LOG ? 'true' : 'false'; ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Cache entries', 'currencyrate-today-currency-blocks'); ?></td>
								<td><code><?php echo esc_html((string) $diag['total']); ?></code></td>
							</tr>
							<tr>
								<td class="crtoday-td-bold"><?php esc_html_e('Storage used', 'currencyrate-today-currency-blocks'); ?></td>
								<td><code><?php echo esc_html(self::format_bytes($diag['total_size'])); ?></code></td>
							</tr>
						</tbody>
					</table>
				</div>
			</div>

			<!-- ── Options Inspector ── -->
			<div class="postbox" style="margin-bottom:16px;">
				<div class="postbox-header">
					<h2 class="crtoday-postbox-heading">
						<span class="dashicons dashicons-search"></span>
						<?php esc_html_e('Options Inspector', 'currencyrate-today-currency-blocks'); ?>
					</h2>
				</div>
				<div class="inside crtoday-pad-16">
					<p class="description" style="margin:0 0 10px;">
						<?php esc_html_e('Raw wp_options values used by the plugin. Source/deal keys are omitted from display.', 'currencyrate-today-currency-blocks'); ?>
					</p>
					<?php
					$option_keys = [
						self::OPTION_NAME,
						'crtoday_installed_at',
						'crtoday_review_dismissed',
						'crtoday_review_remind_at',
					];
					?>
					<table class="widefat striped">
						<thead>
							<tr>
								<th style="width:220px;"><?php esc_html_e('Option', 'currencyrate-today-currency-blocks'); ?></th>
								<th><?php esc_html_e('Value', 'currencyrate-today-currency-blocks'); ?></th>
							</tr>
						</thead>
						<tbody>
							<?php foreach ($option_keys as $key) :
								$val = get_option($key, null);
								if ($key === self::OPTION_NAME && is_array($val)) {
									unset($val['source_id'], $val['deal'], $val['source_deal']);
								}
								$display = $val === null ? '<em>not set</em>' : '<code>' . esc_html(is_array($val) ? wp_json_encode($val, JSON_PRETTY_PRINT) : (string) $val) . '</code>';
							?>
								<tr>
									<td style="font-weight:600;font-family:monospace;font-size:16px;"><?php echo esc_html($key); ?></td>
									<td style="font-family:monospace;font-size:16px;word-break:break-all;"><?php echo $display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above 
																											?></td>
								</tr>
							<?php endforeach; ?>
						</tbody>
					</table>
				</div>
			</div>
		</div>

		<?php
		$ajax_json  = $ajax;
		$nonce_json = $nonce;

		ob_start();
		?>(function() {
	var ajaxUrl = <?php echo wp_json_encode( $ajax_json ); ?>;
	var nonce = <?php echo wp_json_encode( $nonce_json ); ?>;

	document.querySelectorAll('.crtoday-debug-btn').forEach(function(btn) {
		btn.addEventListener('click', function() {
			var action = btn.getAttribute('data-action');

			if (action === 'trigger_review_banner') {
				var xhr = new XMLHttpRequest();
				xhr.open('POST', ajaxUrl);
				xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
				xhr.onload = function() {
					if (xhr.status === 200) {
						try {
							var res = JSON.parse(xhr.responseText);
							if (res.success && res.data.html) {
								var container = document.getElementById('crtoday-debug-banner-container');
								container.innerHTML = res.data.html;
								container.querySelectorAll('script').forEach(function(old) {
									var s = document.createElement('script');
									s.textContent = old.textContent;
									old.parentNode.replaceChild(s, old);
								});
							}
						} catch (e) {}
					}
				};
				xhr.send('action=crtoday_debug_action&debug_action=' + action + '&nonce=' + nonce);
				return;
			}

			var xhr2 = new XMLHttpRequest();
			xhr2.open('POST', ajaxUrl);
			xhr2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
			xhr2.onload = function() {
				if (xhr2.status === 200) {
					location.reload();
				}
			};
			xhr2.send('action=crtoday_debug_action&debug_action=' + action + '&nonce=' + nonce);
		});
	});
})();
<?php
		$debug_js = ob_get_clean();
		wp_add_inline_script( 'crtoday-admin-inline', $debug_js );
	}

	/* ================================================================
	 * Shortcode generator tab
	 * ================================================================ */

	/**
	 * Render grouped <option> elements for a source <select>.
	 *
	 * @param array<array{id:string,label:string,group:string}> $options   Source options from get_all_source_options().
	 * @param string                                              $selected Currently selected compound id (e.g. 'FEX:STANDARD').
	 */
	private static function render_source_options(array $options, string $selected): void
	{
		$current_group = '';
		foreach ($options as $opt) {
			if ($opt['group'] !== $current_group) {
				if ($current_group !== '') {
					echo '</optgroup>';
				}
				$current_group = $opt['group'];
				$group_label   = $current_group === 'builtin'
					? __('Built-in', 'currencyrate-today-currency-blocks')
					: __('Local', 'currencyrate-today-currency-blocks');
				printf('<optgroup label="%s">', esc_attr($group_label));
			}
			printf(
				'<option value="%s"%s>%s</option>',
				esc_attr($opt['id']),
				selected($selected, $opt['id'], false),
				esc_html($opt['label'])
			);
		}
		if ($current_group !== '') {
			echo '</optgroup>';
		}
	}

	private static function render_shortcodes_tab(array $config): void
	{
		$currencies = [];
		$currencies_full = self::currencies();
		foreach ($currencies_full as $code => $meta) {
			$currencies[$code] = $meta['name'];
		}

		$flag_base_url = CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/';

		$theme_presets = array_map(
			static function (string $key, array $preset): array {
				return [
					'key'   => $key,
					'name'  => ucfirst($key),
					'color' => $preset['light']['primary'],
				];
			},
			array_keys(self::get_accent_theme_presets()),
			self::get_accent_theme_presets()
		);

		$defaults = CRToday_Exchange_Rates_Block::DEFAULT_ATTRIBUTES;
		$converter_defaults = CRToday_Converter_Block::DEFAULT_ATTRIBUTES;
		$converter_defaults['source'] = $converter_defaults['sourceId'] . ':' . $converter_defaults['deal'];
		$converter_defaults['from']   = $converter_defaults['fromCurrency'];
		$converter_defaults['to']     = $converter_defaults['toCurrency'];
		$defaults['attribution'] = (bool) ($defaults['attribution'] ?? true);

		$ticker_defaults = CRToday_Ticker_Block::DEFAULT_ATTRIBUTES;

		$rate_card_defaults = CRToday_Rate_Card_Block::DEFAULT_ATTRIBUTES;
		$rate_card_defaults['source'] = $rate_card_defaults['sourceId'] . ':' . $rate_card_defaults['deal'];

		$pb_defaults = CRToday_Price_Badge_Block::DEFAULT_ATTRIBUTES;
		$pb_defaults['source'] = $pb_defaults['sourceId'] . ':' . $pb_defaults['deal'];
	?>
		<input type="hidden" id="crtoday-preview-nonce" value="<?php echo esc_attr(wp_create_nonce('crtoday_shortcode_preview')); ?>" />
		<input type="hidden" id="crtoday-conv-preview-nonce" value="<?php echo esc_attr(wp_create_nonce('crtoday_currency_converter_shortcode_preview')); ?>" />
		<input type="hidden" id="crtoday-ticker-preview-nonce" value="<?php echo esc_attr(wp_create_nonce('crtoday_ticker_shortcode_preview')); ?>" />
		<input type="hidden" id="crtoday-rc-preview-nonce" value="<?php echo esc_attr(wp_create_nonce('crtoday_rate_card_shortcode_preview')); ?>" />
		<input type="hidden" id="crtoday-pb-preview-nonce" value="<?php echo esc_attr(wp_create_nonce('crtoday_price_badge_shortcode_preview')); ?>" />

		<div class="crtoday-admin-shortcodes crtoday-admin-tab-container">
			<div class="crtoday-sc-folder">
				<nav class="crtoday-sc-nav" role="tablist">
					<button type="button" role="tab" class="crtoday-sc-nav-btn is-active" aria-selected="true" data-pane="exchange-rates">
						<img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'assets/icons/exchange-rates.svg'); ?>" alt="" width="18" height="18" />
						<?php esc_html_e('Exchange Rates', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="converter">
						<img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'assets/icons/currency-converter.svg'); ?>" alt="" width="18" height="18" />
						<?php esc_html_e('Currency Converter', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="ticker">
						<img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'assets/icons/ticker.svg'); ?>" alt="" width="18" height="18" />
						<?php esc_html_e('Ticker', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="rate-card">
						<img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'assets/icons/rate-card.svg'); ?>" alt="" width="18" height="18" />
						<?php esc_html_e('Rate Card', 'currencyrate-today-currency-blocks'); ?>
					</button>
					<button type="button" role="tab" class="crtoday-sc-nav-btn" aria-selected="false" data-pane="price-badge">
						<img src="<?php echo esc_url(CRTODAY_PLUGIN_URL . 'assets/icons/price-badge.svg'); ?>" alt="" width="18" height="18" />
						<?php esc_html_e('Price Badge', 'currencyrate-today-currency-blocks'); ?>
					</button>
				</nav>

				<!-- Exchange Rates Block Generator -->
				<div class="crtoday-sc-pane is-active" data-pane="exchange-rates" role="tabpanel">
					<div id="crtoday-shortcode-form" class="crtoday-sc-layout">

						<!-- Left: Live Preview -->
						<div class="crtoday-sc-preview-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-visibility crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Preview', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div id="crtoday-shortcode-preview" class="crtoday-sc-preview">
								<span class="spinner is-active"></span>
							</div>
						</div>

						<!-- Right: Settings -->
						<div class="crtoday-sc-settings-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-admin-generic crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div class="crtoday-sc-tabs" data-crtoday-settings-tabs>
								<button type="button" class="button button-primary" data-crtoday-tab="data"><?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="presentation"><?php esc_html_e('Display', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="button"><?php esc_html_e('Button', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="style"><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></button>
							</div>

							<!-- Data Settings -->
							<div class="postbox crtoday-panel crtoday-sc-panel" data-crtoday-panel="data" data-crtoday-pane="data">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-database"></span><?php esc_html_e('Data settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-sc-source"><?php esc_html_e('Data source', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-sc-source" data-attr="source" class="regular-text">
													<?php
													$source_options = self::get_all_source_options();
													self::render_source_options($source_options, 'FEX:STANDARD');
													?>
												</select>
												<p class="description"><?php esc_html_e('Manage sources in the Sources tab.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label><?php esc_html_e('Base currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$base_code = $defaults['base'];
												$base_flag = $currencies_full[$base_code]['flag'] ?? strtolower($base_code);
												?>
												<input type="hidden" id="crtoday-sc-base" data-attr="base" value="<?php echo esc_attr($base_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-sc-base" data-value="<?php echo esc_attr($base_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $base_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($base_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('Target currencies', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<div class="crtoday-multi-picker-wrap" data-crtoday-multi-picker="true">
													<input type="hidden" id="crtoday-sc-symbols" data-attr="symbols" value="<?php echo esc_attr(implode(',', $defaults['symbols'])); ?>" />
													<div class="crtoday-multi-picker-tags">
														<?php foreach ($defaults['symbols'] as $sym) :
															$sym_flag = isset($currencies_full[$sym]['flag']) ? $currencies_full[$sym]['flag'] : strtolower($sym);
														?>
															<span class="crtoday-tag" data-code="<?php echo esc_attr($sym); ?>">
																<img class="crtoday-tag-flag" src="<?php echo esc_url($flag_base_url . $sym_flag . '.webp'); ?>" alt="" width="16" height="16" />
																<span><?php echo esc_html($sym); ?></span>
																<button type="button" class="crtoday-tag-remove" aria-label="<?php
																	/* translators: %s: currency code or pair (e.g. USD, EUR/USD) */
																	echo esc_attr(sprintf(__('Remove %s', 'currencyrate-today-currency-blocks'), $sym)); ?>">×</button>
															</span>
														<?php endforeach; ?>
														<button type="button" class="crtoday-multi-picker-add" aria-haspopup="listbox" aria-expanded="false">
															<span class="dashicons dashicons-plus-alt2" style="font-size:14px;width:14px;height:14px;"></span>
															<?php esc_html_e('Add', 'currencyrate-today-currency-blocks'); ?>
														</button>
													</div>
												</div>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-invertRates"><?php esc_html_e('Invert rates', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<label><input type="checkbox" id="crtoday-sc-invertRates" data-attr="invertRates" <?php checked($defaults['invertRates']); ?> />
													<?php esc_html_e('Invert: target → base', 'currencyrate-today-currency-blocks'); ?></label>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-amount"><?php esc_html_e('Amount', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-sc-amount" data-attr="amount" value="<?php echo esc_attr($defaults['amount']); ?>" step="1" class="crtoday-input-sm" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-adjustmentPercent"><?php esc_html_e('Adjustment (%)', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-sc-adjustmentPercent" data-attr="adjustmentPercent" value="0" min="-1000000" max="1000000" step="0.05" class="crtoday-input-sm" />
												<p class="description crtoday-field-hint"><?php esc_html_e('Apply minus or plus percentage to final values.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Presentation Settings -->
							<div class="is-hidden postbox crtoday-panel crtoday-sc-panel" data-crtoday-panel="presentation" data-crtoday-pane="presentation">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-desktop"></span><?php esc_html_e('Presentation settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-sc-decimals"><?php esc_html_e('Decimals', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-sc-decimals" data-attr="decimals" value="<?php echo esc_attr((string) $defaults['decimals']); ?>" min="0" max="8" step="1" class="crtoday-range-input" />
												<span id="crtoday-decimals-value" class="crtoday-range-value"><?php echo esc_html((string) $defaults['decimals']); ?></span>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-theme"><?php esc_html_e('Theme', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-sc-theme" data-attr="theme">
													<option value="auto" <?php selected($defaults['theme'], 'auto'); ?>><?php esc_html_e('Auto (system)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="light" <?php selected($defaults['theme'], 'light'); ?>><?php esc_html_e('Light', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="dark" <?php selected($defaults['theme'], 'dark'); ?>><?php esc_html_e('Dark', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-radius"><?php esc_html_e('Corner radius', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-sc-radius" data-attr="radius" value="<?php echo esc_attr((string) $defaults['radius']); ?>" min="0" max="32" step="1" class="crtoday-range-input" />
												<span id="crtoday-radius-value" class="crtoday-range-value"><?php echo esc_html((string) $defaults['radius']); ?>px</span>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-shadow"><?php esc_html_e('Shadow', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-sc-shadow" data-attr="shadow" <?php checked($defaults['shadow']); ?> /> <?php esc_html_e('Enable drop shadow', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-currencyLabelMode"><?php esc_html_e('Currency labels', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-sc-currencyLabelMode" data-attr="currencyLabelMode">
													<option value="code" <?php selected($defaults['currencyLabelMode'], 'code'); ?>><?php esc_html_e('Code (USD)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="name" <?php selected($defaults['currencyLabelMode'], 'name'); ?>><?php esc_html_e('Name (US Dollar)', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
												<label class="crtoday-check-label crtoday-check-label--mt"><input type="checkbox" data-attr="showCurrencySymbols" <?php checked((bool) ($defaults['showCurrencySymbols'] ?? false)); ?> /> <?php esc_html_e('Show currency symbols', 'currencyrate-today-currency-blocks'); ?></label>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Display options', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<fieldset>
													<div style="margin-bottom:6px;">
														<label for="crtoday-sc-sourceLabel"><?php esc_html_e('Source label', 'currencyrate-today-currency-blocks'); ?></label>
														<input type="text" id="crtoday-sc-sourceLabel" data-attr="sourceLabel" value="" maxlength="20" placeholder="<?php esc_attr_e('Auto (source name)', 'currencyrate-today-currency-blocks'); ?>" style="width:180px;font-size:16px;margin-left:8px;" />
														<p class="description" style="margin:2px 0 0;"><?php esc_html_e('Custom label (max 20 chars). Empty = source name.', 'currencyrate-today-currency-blocks'); ?></p>
													</div>
													<label class="crtoday-check-label"><input type="checkbox" data-attr="showTimestamp" <?php checked($defaults['showTimestamp']); ?> /> <?php esc_html_e('Show timestamp', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-attr="showChange" <?php checked($defaults['showChange']); ?> /> <?php esc_html_e('Show 24h change', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-attr="showFlags" <?php checked($defaults['showFlags']); ?> /> <?php esc_html_e('Show flags', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-attr="attribution" <?php checked((bool) ($defaults['attribution'] ?? true)); ?> /> <?php esc_html_e('Attribution', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-attr="compactMode" <?php checked($defaults['compactMode']); ?> /> <?php esc_html_e('Compact mode', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label crtoday-check-label--mt"><input type="checkbox" data-attr="fullWidth" <?php checked((bool) ($defaults['fullWidth'] ?? false)); ?> /> <?php esc_html_e('Full width', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label crtoday-check-label--mt"><input type="checkbox" data-attr="enableStructuredData" <?php checked((bool) ($defaults['enableStructuredData'] ?? false)); ?> /> <?php esc_html_e('Search engine data', 'currencyrate-today-currency-blocks'); ?></label>
												</fieldset>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Style Settings -->
							<div class="is-hidden postbox crtoday-panel crtoday-sc-panel" data-crtoday-panel="button" data-crtoday-pane="button">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-button"></span><?php esc_html_e('Button settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-sc-showCtaButton"><?php esc_html_e('Row button', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-sc-showCtaButton" data-attr="showCtaButton" <?php checked($defaults['showCtaButton'] ?? false); ?> /> <?php esc_html_e('Add button to each row', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-ctaButtonText"><?php esc_html_e('Button text', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="text" id="crtoday-sc-ctaButtonText" data-attr="ctaButtonText" value="<?php echo esc_attr($defaults['ctaButtonText'] ?? __('Convert', 'currencyrate-today-currency-blocks')); ?>" maxlength="40" class="regular-text" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-ctaUrlTemplate"><?php esc_html_e('URL template', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="text" id="crtoday-sc-ctaUrlTemplate" data-attr="ctaUrlTemplate" value="<?php echo esc_attr($defaults['ctaUrlTemplate'] ?? ''); ?>" class="large-text" placeholder="https://example.com/convert?amount={amount}&from={from}&to={to}&result={result}&bid={bid}&ask={ask}" />
												<p class="description crtoday-field-hint"><?php esc_html_e('Use placeholders: {amount}, {from}, {to}, {result}, {bid}, {ask}.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<div class="is-hidden postbox crtoday-panel crtoday-sc-panel" data-crtoday-panel="style" data-crtoday-pane="style">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-art"></span><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-sc-seamless"><?php esc_html_e('Transparent', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-sc-seamless" data-attr="seamless" <?php checked($defaults['seamless']); ?> /> <?php esc_html_e('Transparent — for placing inside a custom container', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-sc-accentMode"><?php esc_html_e('Accent display', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-sc-accentMode" data-attr="accentMode">
													<option value="stripe" <?php selected($defaults['accentMode'], 'stripe'); ?>><?php esc_html_e('Top stripe', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="color-border" <?php selected($defaults['accentMode'], 'color-border'); ?>><?php esc_html_e('Solid color border', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="none" <?php selected($defaults['accentMode'], 'none'); ?>><?php esc_html_e('None', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-accentTheme"><?php esc_html_e('Theme preset', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="hidden" id="crtoday-sc-accentTheme" data-attr="accentTheme" value="<?php echo esc_attr($defaults['accentTheme'] ?? 'default'); ?>" />
												<div class="crtoday-theme-grid" data-crtoday-theme-grid data-crtoday-theme-target="#crtoday-sc-accentTheme">
													<?php foreach ($theme_presets as $preset) : ?>
														<?php $is_active = ($defaults['accentTheme'] ?? 'default') === $preset['key']; ?>
														<button type="button" class="crtoday-theme-swatch<?php echo $is_active ? ' is-active' : ''; ?>" data-crtoday-theme-value="<?php echo esc_attr($preset['key']); ?>" aria-label="<?php echo esc_attr($preset['name']); ?>" title="<?php echo esc_attr($preset['name']); ?>">
															<span class="crtoday-theme-swatch-dot" style="background:<?php echo esc_attr($preset['color']); ?>;"></span>
															<span class="crtoday-theme-swatch-name"><?php echo esc_html($preset['name']); ?></span>
														</button>
													<?php endforeach; ?>
												</div>
											</td>
										</tr>
										<tr id="crtoday-row-accentAngle">
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-sc-accentAngle"><?php esc_html_e('Stripe angle', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-sc-accentAngle" data-attr="accentAngle" value="<?php echo esc_attr((string) ($defaults['accentAngle'] ?? 135)); ?>" min="0" max="360" step="1" class="crtoday-range-input" />
												<span id="crtoday-accent-angle-value" class="crtoday-range-value"><?php echo esc_html((string) ($defaults['accentAngle'] ?? 135)); ?>°</span>
											</td>
										</tr>
									</table>
								</div>
							</div>

						</div>
					</div>

					<!-- Generated Shortcode -->
					<div class="crtoday-admin-shortcode-output">
						<h3 class="crtoday-sc-heading">
							<span class="dashicons dashicons-shortcode crtoday-sc-heading-icon"></span>
							<?php esc_html_e('Generated Shortcode', 'currencyrate-today-currency-blocks'); ?>
						</h3>
						<div class="crtoday-admin-shortcode-output-row">
							<code id="crtoday-shortcode-output">
								[crtoday_exchange_rates]
							</code>
							<button type="button" id="crtoday-copy-shortcode" class="button button-primary"
								data-copied-label="<?php echo esc_attr(__('Copied!', 'currencyrate-today-currency-blocks')); ?>">
								<span class="dashicons dashicons-clipboard"></span>
								<?php esc_html_e('Copy', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>
					</div>
				</div>

				<!-- Currency Converter Block Generator -->
				<div class="crtoday-sc-pane" data-pane="converter" role="tabpanel">
					<div id="crtoday-conv-shortcode-form" class="crtoday-sc-layout">

						<div class="crtoday-sc-preview-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-visibility crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Preview', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div id="crtoday-conv-shortcode-preview" class="crtoday-sc-preview">
								<span class="spinner is-active"></span>
							</div>
						</div>

						<div class="crtoday-sc-settings-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-admin-generic crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div class="crtoday-sc-tabs" data-crtoday-settings-tabs>
								<button type="button" class="button button-primary" data-crtoday-tab="data"><?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="presentation"><?php esc_html_e('Display', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="button"><?php esc_html_e('Button', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="style"><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></button>
							</div>
							<div class="postbox crtoday-sc-panel" data-crtoday-pane="data">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-database"></span><?php esc_html_e('Data settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-conv-sc-source"><?php esc_html_e('Data source', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-conv-sc-source" data-conv-attr="source" class="regular-text">
													<?php self::render_source_options($source_options, $converter_defaults['source']); ?>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('From currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$from_code = $converter_defaults['from'];
												$from_flag = $currencies_full[$from_code]['flag'] ?? strtolower($from_code);
												?>
												<input type="hidden" id="crtoday-conv-sc-from" data-conv-attr="from" value="<?php echo esc_attr($from_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-conv-sc-from" data-value="<?php echo esc_attr($from_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $from_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($from_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('To currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$to_code = $converter_defaults['to'];
												$to_flag = $currencies_full[$to_code]['flag'] ?? strtolower($to_code);
												?>
												<input type="hidden" id="crtoday-conv-sc-to" data-conv-attr="to" value="<?php echo esc_attr($to_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-conv-sc-to" data-value="<?php echo esc_attr($to_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $to_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($to_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-amount"><?php esc_html_e('Amount', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-conv-sc-amount" data-conv-attr="amount" value="<?php echo esc_attr((string) $converter_defaults['amount']); ?>" min="0.00001" max="999999999" step="1" class="crtoday-input-sm" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-adjustmentPercent"><?php esc_html_e('Adjustment (%)', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-conv-sc-adjustmentPercent" data-conv-attr="adjustmentPercent" value="<?php echo esc_attr((string) $converter_defaults['adjustmentPercent']); ?>" min="-1000000" max="1000000" step="0.05" class="crtoday-input-sm" />
											</td>
										</tr>
									</table>
								</div>
							</div>

							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="presentation">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-desktop"></span><?php esc_html_e('Presentation settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-conv-sc-decimals"><?php esc_html_e('Decimals', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-conv-sc-decimals" data-conv-attr="decimals" value="<?php echo esc_attr((string) $converter_defaults['decimals']); ?>" min="0" max="8" step="1" class="crtoday-range-input" /> <span id="crtoday-conv-decimals-value" class="crtoday-range-value"><?php echo esc_html((string) $converter_defaults['decimals']); ?></span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-theme"><?php esc_html_e('Theme', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-conv-sc-theme" data-conv-attr="theme">
													<option value="auto" <?php selected($converter_defaults['theme'], 'auto'); ?>><?php esc_html_e('Auto (system)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="light" <?php selected($converter_defaults['theme'], 'light'); ?>><?php esc_html_e('Light', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="dark" <?php selected($converter_defaults['theme'], 'dark'); ?>><?php esc_html_e('Dark', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-radius"><?php esc_html_e('Corner radius', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-conv-sc-radius" data-conv-attr="radius" value="<?php echo esc_attr((string) $converter_defaults['radius']); ?>" min="0" max="32" step="1" class="crtoday-range-input" /> <span id="crtoday-conv-radius-value" class="crtoday-range-value"><?php echo esc_html((string) $converter_defaults['radius']); ?>px</span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Display options', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="shadow" <?php checked($converter_defaults['shadow']); ?> /> <?php esc_html_e('Enable shadow', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="showFlags" <?php checked($converter_defaults['showFlags']); ?> /> <?php esc_html_e('Show flags', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="attribution" <?php checked((bool) ($converter_defaults['attribution'] ?? true)); ?> /> <?php esc_html_e('Attribution', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="showTimestamp" <?php checked($converter_defaults['showTimestamp']); ?> /> <?php esc_html_e('Show timestamp', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="compactMode" <?php checked($converter_defaults['compactMode']); ?> /> <?php esc_html_e('Compact mode', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-conv-attr="fullWidth" <?php checked((bool) ($converter_defaults['fullWidth'] ?? false)); ?> /> <?php esc_html_e('Full width', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label crtoday-check-label--mt"><input type="checkbox" data-conv-attr="enableStructuredData" <?php checked((bool) ($converter_defaults['enableStructuredData'] ?? false)); ?> /> <?php esc_html_e('Search engine data', 'currencyrate-today-currency-blocks'); ?></label>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="style">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-art"></span><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-conv-sc-seamless"><?php esc_html_e('Transparent', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-conv-sc-seamless" data-conv-attr="seamless" <?php checked($converter_defaults['seamless']); ?> /> <?php esc_html_e('Transparent — for placing inside a custom container', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-conv-sc-accentMode"><?php esc_html_e('Accent display', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-conv-sc-accentMode" data-conv-attr="accentMode">
													<option value="stripe" <?php selected($converter_defaults['accentMode'], 'stripe'); ?>><?php esc_html_e('Top stripe', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="color-border" <?php selected($converter_defaults['accentMode'], 'color-border'); ?>><?php esc_html_e('Solid color border', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="none" <?php selected($converter_defaults['accentMode'], 'none'); ?>><?php esc_html_e('None', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-accentTheme"><?php esc_html_e('Theme preset', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="hidden" id="crtoday-conv-sc-accentTheme" data-conv-attr="accentTheme" value="<?php echo esc_attr($converter_defaults['accentTheme'] ?? 'default'); ?>" />
												<div class="crtoday-theme-grid" data-crtoday-theme-grid data-crtoday-theme-target="#crtoday-conv-sc-accentTheme">
													<?php foreach ($theme_presets as $preset) : ?>
														<?php $is_conv_active = ($converter_defaults['accentTheme'] ?? 'default') === $preset['key']; ?>
														<button type="button" class="crtoday-theme-swatch<?php echo $is_conv_active ? ' is-active' : ''; ?>" data-crtoday-theme-value="<?php echo esc_attr($preset['key']); ?>" aria-label="<?php echo esc_attr($preset['name']); ?>" title="<?php echo esc_attr($preset['name']); ?>">
															<span class="crtoday-theme-swatch-dot" style="background:<?php echo esc_attr($preset['color']); ?>;"></span>
															<span class="crtoday-theme-swatch-name"><?php echo esc_html($preset['name']); ?></span>
														</button>
													<?php endforeach; ?>
												</div>
											</td>
										</tr>
										<tr id="crtoday-conv-row-accentAngle">
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-accentAngle"><?php esc_html_e('Stripe angle', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-conv-sc-accentAngle" data-conv-attr="accentAngle" value="<?php echo esc_attr((string) ($converter_defaults['accentAngle'] ?? 135)); ?>" min="0" max="360" step="1" class="crtoday-range-input" />
												<span id="crtoday-conv-accent-angle-value" class="crtoday-range-value"><?php echo esc_html((string) ($converter_defaults['accentAngle'] ?? 135)); ?>°</span>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="button">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-button"></span><?php esc_html_e('Button settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-conv-sc-showCtaButton"><?php esc_html_e('Enable custom button', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-conv-sc-showCtaButton" data-conv-attr="showCtaButton" <?php checked($converter_defaults['showCtaButton'] ?? false); ?> /> <?php esc_html_e('Add custom button', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-ctaButtonText"><?php esc_html_e('Button text', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="text" id="crtoday-conv-sc-ctaButtonText" data-conv-attr="ctaButtonText" value="<?php echo esc_attr($converter_defaults['ctaButtonText'] ?? __('Convert', 'currencyrate-today-currency-blocks')); ?>" maxlength="40" class="regular-text" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-conv-sc-ctaUrlTemplate"><?php esc_html_e('URL template', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="text" id="crtoday-conv-sc-ctaUrlTemplate" data-conv-attr="ctaUrlTemplate" value="<?php echo esc_attr($converter_defaults['ctaUrlTemplate'] ?? ''); ?>" class="large-text" placeholder="https://example.com/convert?amount={amount}&from={from}&to={to}&result={result}" />
												<p class="description crtoday-field-hint"><?php esc_html_e('Use placeholders: {amount}, {from}, {to}, {result}.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
									</table>
								</div>
							</div>
						</div>
					</div>

					<div class="crtoday-admin-shortcode-output">
						<h3 class="crtoday-sc-heading">
							<span class="dashicons dashicons-shortcode crtoday-sc-heading-icon"></span>
							<?php esc_html_e('Generated Shortcode', 'currencyrate-today-currency-blocks'); ?>
						</h3>
						<div class="crtoday-admin-shortcode-output-row">
							<code id="crtoday-conv-shortcode-output">[crtoday_currency_converter]</code>
							<button type="button" id="crtoday-conv-copy-shortcode" class="button button-primary"
								data-copied-label="<?php echo esc_attr(__('Copied!', 'currencyrate-today-currency-blocks')); ?>">
								<span class="dashicons dashicons-clipboard"></span>
								<?php esc_html_e('Copy', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>
					</div>
				</div>

				<!-- Exchange Rates Ticker Block Generator -->
				<div class="crtoday-sc-pane" data-pane="ticker" role="tabpanel">
					<div id="crtoday-ticker-shortcode-form" class="crtoday-sc-layout">

						<!-- Left: Live Preview -->
						<div class="crtoday-sc-preview-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-visibility crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Preview', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div id="crtoday-ticker-shortcode-preview" class="crtoday-sc-preview" style="min-height:80px;">
								<span class="spinner is-active crtoday-spinner-centered"></span>
							</div>
						</div>

						<!-- Right: Settings -->
						<div class="crtoday-sc-settings-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-admin-generic crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div class="crtoday-sc-tabs" data-crtoday-settings-tabs>
								<button type="button" class="button button-primary" data-crtoday-tab="data"><?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="presentation"><?php esc_html_e('Display', 'currencyrate-today-currency-blocks'); ?></button>
							</div>

							<!-- Data Settings -->
							<div class="postbox crtoday-panel crtoday-sc-panel" data-crtoday-pane="data">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-database"></span><?php esc_html_e('Data settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-ticker-sc-source"><?php esc_html_e('Data source', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-ticker-sc-source" data-ticker-attr="source" class="regular-text">
													<?php self::render_source_options($source_options, 'FEX:STANDARD'); ?>
												</select>
												<p class="description"><?php esc_html_e('Manage sources in the Sources tab.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Currency pairs', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<input type="hidden" id="crtoday-ticker-sc-pairs" data-ticker-attr="pairs" value="<?php
													$pair_strings = [];
													foreach ($ticker_defaults['pairs'] as $p) {
														$pair_strings[] = $p['from'] . '/' . $p['to'];
													}
													echo esc_attr(implode(',', $pair_strings));
												?>" />
												<div class="crtoday-pairs-picker-wrap" data-crtoday-pairs-picker="true" data-target="crtoday-ticker-sc-pairs">
													<div class="crtoday-pairs-picker-tags">
														<?php foreach ($ticker_defaults['pairs'] as $pair) :
															$from_flag = $currencies_full[$pair['from']]['flag'] ?? strtolower($pair['from']);
															$to_flag   = $currencies_full[$pair['to']]['flag'] ?? strtolower($pair['to']);
														?>
															<span class="crtoday-tag crtoday-pair-tag" data-from="<?php echo esc_attr($pair['from']); ?>" data-to="<?php echo esc_attr($pair['to']); ?>">
																<img class="crtoday-tag-flag" src="<?php echo esc_url($flag_base_url . $from_flag . '.webp'); ?>" alt="" width="16" height="16" />
																<span><?php echo esc_html($pair['from'] . '/' . $pair['to']); ?></span>
																<img class="crtoday-tag-flag" src="<?php echo esc_url($flag_base_url . $to_flag . '.webp'); ?>" alt="" width="16" height="16" />
																<button type="button" class="crtoday-tag-remove" aria-label="<?php
																	echo esc_attr(sprintf(
																		/* translators: %s: currency code or pair (e.g. USD, EUR/USD) */
																		__('Remove %s', 'currencyrate-today-currency-blocks'),
																		$pair['from'] . '/' . $pair['to']
																	)); ?>">×</button>
															</span>
														<?php endforeach; ?>
													</div>
													<div class="crtoday-pairs-picker-add-row">
														<input type="hidden" id="crtoday-ticker-pair-from" value="" />
														<button type="button" class="crtoday-pair-from-btn crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-ticker-pair-from" data-value="" aria-haspopup="listbox" aria-expanded="false">
															<span class="crtoday-picker-code"><?php esc_html_e('From', 'currencyrate-today-currency-blocks'); ?></span>
															<span class="crtoday-picker-chevron">▾</span>
														</button>
														<span style="color:#50575e;">/</span>
														<input type="hidden" id="crtoday-ticker-pair-to" value="" />
														<button type="button" class="crtoday-pair-to-btn crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-ticker-pair-to" data-value="" aria-haspopup="listbox" aria-expanded="false">
															<span class="crtoday-picker-code"><?php esc_html_e('To', 'currencyrate-today-currency-blocks'); ?></span>
															<span class="crtoday-picker-chevron">▾</span>
														</button>
														<button type="button" class="button crtoday-pair-add-btn" disabled>
															<span class="dashicons dashicons-plus-alt2"></span>
															<?php esc_html_e('Add', 'currencyrate-today-currency-blocks'); ?>
														</button>
													</div>
												</div>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-amount"><?php esc_html_e('Amount', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-ticker-sc-amount" data-ticker-attr="amount" value="<?php echo esc_attr($ticker_defaults['amount']); ?>" step="1" class="crtoday-input-sm" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-adjustmentPercent"><?php esc_html_e('Adjustment (%)', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-ticker-sc-adjustmentPercent" data-ticker-attr="adjustmentPercent" value="0" min="-1000000" max="1000000" step="0.05" class="crtoday-input-sm" />
												<p class="description crtoday-field-hint"><?php esc_html_e('Apply minus or plus percentage to final values.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Presentation Settings -->
							<div class="is-hidden postbox crtoday-panel crtoday-sc-panel" data-crtoday-pane="presentation">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-desktop"></span><?php esc_html_e('Presentation settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-panel-body crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-ticker-sc-decimals"><?php esc_html_e('Decimals', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-ticker-sc-decimals" data-ticker-attr="decimals" value="<?php echo esc_attr((string) $ticker_defaults['decimals']); ?>" min="0" max="8" step="1" class="crtoday-range-input" />
												<span id="crtoday-ticker-decimals-value" class="crtoday-range-value"><?php echo esc_html((string) $ticker_defaults['decimals']); ?></span>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-theme"><?php esc_html_e('Theme', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-ticker-sc-theme" data-ticker-attr="theme">
													<option value="auto"><?php esc_html_e('Auto (system)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="light" selected><?php esc_html_e('Light', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="dark"><?php esc_html_e('Dark', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-currencyLabelMode"><?php esc_html_e('Currency labels', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-ticker-sc-currencyLabelMode" data-ticker-attr="currencyLabelMode">
													<option value="code" <?php selected($ticker_defaults['currencyLabelMode'], 'code'); ?>><?php esc_html_e('Code (USD)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="name" <?php selected($ticker_defaults['currencyLabelMode'], 'name'); ?>><?php esc_html_e('Name (US Dollar)', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-speed"><?php esc_html_e('Speed', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-ticker-sc-speed" data-ticker-attr="speed" value="<?php echo esc_attr((string) $ticker_defaults['speed']); ?>" min="10" max="200" step="5" class="crtoday-range-input" />
												<span id="crtoday-ticker-speed-value" class="crtoday-range-value"><?php echo esc_html((string) $ticker_defaults['speed']); ?></span>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-direction"><?php esc_html_e('Direction', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-ticker-sc-direction" data-ticker-attr="direction">
													<option value="left" <?php selected($ticker_defaults['direction'], 'left'); ?>><?php esc_html_e('Left', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="right" <?php selected($ticker_defaults['direction'], 'right'); ?>><?php esc_html_e('Right', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-ticker-sc-separator"><?php esc_html_e('Separator', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-ticker-sc-separator" data-ticker-attr="separator">
													<option value="dot" <?php selected($ticker_defaults['separator'], 'dot'); ?>><?php esc_html_e('Dot ( · )', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="pipe" <?php selected($ticker_defaults['separator'], 'pipe'); ?>><?php esc_html_e('Pipe ( | )', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="slash" <?php selected($ticker_defaults['separator'], 'slash'); ?>><?php esc_html_e('Slash ( / )', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="none" <?php selected($ticker_defaults['separator'], 'none'); ?>><?php esc_html_e('None', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Display options', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<fieldset>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="showFlags" <?php checked($ticker_defaults['showFlags']); ?> /> <?php esc_html_e('Show flags', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="showChange" <?php checked($ticker_defaults['showChange']); ?> /> <?php esc_html_e('Show 24h change', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="showTimestamp" <?php checked($ticker_defaults['showTimestamp']); ?> /> <?php esc_html_e('Show timestamp', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="pauseOnHover" <?php checked($ticker_defaults['pauseOnHover']); ?> /> <?php esc_html_e('Pause on hover', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="attribution" <?php checked($ticker_defaults['attribution']); ?> /> <?php esc_html_e('Attribution', 'currencyrate-today-currency-blocks'); ?></label>
												</fieldset>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Borders', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<fieldset>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="borderX" <?php checked($ticker_defaults['borderX']); ?> /> <?php esc_html_e('Horizontal borders', 'currencyrate-today-currency-blocks'); ?></label>
													<label class="crtoday-check-label"><input type="checkbox" data-ticker-attr="borderY" <?php checked($ticker_defaults['borderY']); ?> /> <?php esc_html_e('Vertical borders', 'currencyrate-today-currency-blocks'); ?></label>
												</fieldset>
											</td>
										</tr>
									</table>
								</div>
							</div>

						</div>
					</div>

					<!-- Generated Shortcode -->
					<div class="crtoday-admin-shortcode-output">
						<h3 class="crtoday-sc-heading">
							<span class="dashicons dashicons-shortcode crtoday-sc-heading-icon"></span>
							<?php esc_html_e('Generated Shortcode', 'currencyrate-today-currency-blocks'); ?>
						</h3>
						<div class="crtoday-admin-shortcode-output-row">
							<code id="crtoday-ticker-shortcode-output">[crtoday_ticker]</code>
							<button type="button" id="crtoday-ticker-copy-shortcode" class="button button-primary"
								data-copied-label="<?php echo esc_attr(__('Copied!', 'currencyrate-today-currency-blocks')); ?>">
								<span class="dashicons dashicons-clipboard"></span>
								<?php esc_html_e('Copy', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>
					</div>
				</div>

				<!-- Exchange Rate Card Block Generator -->
				<div class="crtoday-sc-pane" data-pane="rate-card" role="tabpanel">
					<div id="crtoday-rc-shortcode-form" class="crtoday-sc-layout">

						<div class="crtoday-sc-preview-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-visibility crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Preview', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div id="crtoday-rc-shortcode-preview" class="crtoday-sc-preview">
								<span class="spinner is-active"></span>
							</div>
						</div>

						<div class="crtoday-sc-settings-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-admin-generic crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div class="crtoday-sc-tabs" data-crtoday-settings-tabs>
								<button type="button" class="button button-primary" data-crtoday-tab="data"><?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="presentation"><?php esc_html_e('Display', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="style"><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></button>
							</div>

							<!-- Settings pane -->
							<div class="postbox crtoday-sc-panel" data-crtoday-pane="data">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-database"></span><?php esc_html_e('Data settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-rc-sc-source"><?php esc_html_e('Data source', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-rc-sc-source" data-rc-attr="source" class="regular-text">
													<?php self::render_source_options($source_options, $rate_card_defaults['source']); ?>
												</select>
												<p class="description"><?php esc_html_e('Manage sources in the Sources tab.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('From currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$rc_from_code = $rate_card_defaults['from'];
												$rc_from_flag = $currencies_full[$rc_from_code]['flag'] ?? strtolower($rc_from_code);
												?>
												<input type="hidden" id="crtoday-rc-sc-from" data-rc-attr="from" value="<?php echo esc_attr($rc_from_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-rc-sc-from" data-value="<?php echo esc_attr($rc_from_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $rc_from_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($rc_from_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('To currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$rc_to_code = $rate_card_defaults['to'];
												$rc_to_flag = $currencies_full[$rc_to_code]['flag'] ?? strtolower($rc_to_code);
												?>
												<input type="hidden" id="crtoday-rc-sc-to" data-rc-attr="to" value="<?php echo esc_attr($rc_to_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-rc-sc-to" data-value="<?php echo esc_attr($rc_to_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $rc_to_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($rc_to_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-amount"><?php esc_html_e('Amount', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-rc-sc-amount" data-rc-attr="amount" value="<?php echo esc_attr((string) $rate_card_defaults['amount']); ?>" min="0.00001" max="999999999" step="1" class="crtoday-input-sm" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-adjustmentPercent"><?php esc_html_e('Adjustment (%)', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-rc-sc-adjustmentPercent" data-rc-attr="adjustmentPercent" value="<?php echo esc_attr((string) $rate_card_defaults['adjustmentPercent']); ?>" min="-100000" max="1000000" step="0.05" class="crtoday-input-sm" />
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Display pane -->
							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="presentation">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-desktop"></span><?php esc_html_e('Presentation settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-rc-sc-decimals"><?php esc_html_e('Decimals', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-rc-sc-decimals" data-rc-attr="decimals" value="<?php echo esc_attr((string) $rate_card_defaults['decimals']); ?>" min="0" max="8" step="1" class="crtoday-range-input" /> <span id="crtoday-rc-decimals-value" class="crtoday-range-value"><?php echo esc_html((string) $rate_card_defaults['decimals']); ?></span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-theme"><?php esc_html_e('Theme', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-rc-sc-theme" data-rc-attr="theme">
													<option value="auto" <?php selected($rate_card_defaults['theme'], 'auto'); ?>><?php esc_html_e('Auto (system)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="light" <?php selected($rate_card_defaults['theme'], 'light'); ?>><?php esc_html_e('Light', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="dark" <?php selected($rate_card_defaults['theme'], 'dark'); ?>><?php esc_html_e('Dark', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-radius"><?php esc_html_e('Corner radius', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-rc-sc-radius" data-rc-attr="radius" value="<?php echo esc_attr((string) $rate_card_defaults['radius']); ?>" min="0" max="32" step="1" class="crtoday-range-input" /> <span id="crtoday-rc-radius-value" class="crtoday-range-value"><?php echo esc_html((string) $rate_card_defaults['radius']); ?>px</span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-currencyLabelMode"><?php esc_html_e('Currency label', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-rc-sc-currencyLabelMode" data-rc-attr="currencyLabelMode">
													<option value="code" <?php selected($rate_card_defaults['currencyLabelMode'], 'code'); ?>><?php esc_html_e('Code (USD)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="name" <?php selected($rate_card_defaults['currencyLabelMode'], 'name'); ?>><?php esc_html_e('Name', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="both" <?php selected($rate_card_defaults['currencyLabelMode'], 'both'); ?>><?php esc_html_e('Both', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Display options', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="shadow" <?php checked($rate_card_defaults['shadow']); ?> /> <?php esc_html_e('Enable shadow', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="showFlags" <?php checked($rate_card_defaults['showFlags']); ?> /> <?php esc_html_e('Show flags', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="showChange" <?php checked($rate_card_defaults['showChange']); ?> /> <?php esc_html_e('Show change', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="showChart" <?php checked($rate_card_defaults['showChart']); ?> /> <?php esc_html_e('Show chart', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="showRange" <?php checked($rate_card_defaults['showRange']); ?> /> <?php esc_html_e("Show day's range", 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="showTimestamp" <?php checked($rate_card_defaults['showTimestamp']); ?> /> <?php esc_html_e('Show timestamp', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="attribution" <?php checked((bool) $rate_card_defaults['attribution']); ?> /> <?php esc_html_e('Attribution', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="compactMode" <?php checked($rate_card_defaults['compactMode']); ?> /> <?php esc_html_e('Compact mode', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-rc-attr="fullWidth" <?php checked((bool) ($rate_card_defaults['fullWidth'] ?? false)); ?> /> <?php esc_html_e('Full width', 'currencyrate-today-currency-blocks'); ?></label>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Style pane -->
							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="style">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-art"></span><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-rc-sc-seamless"><?php esc_html_e('Transparent', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><label><input type="checkbox" id="crtoday-rc-sc-seamless" data-rc-attr="seamless" <?php checked($rate_card_defaults['seamless']); ?> /> <?php esc_html_e('Transparent — for placing inside a custom container', 'currencyrate-today-currency-blocks'); ?></label></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-accentMode"><?php esc_html_e('Accent display', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-rc-sc-accentMode" data-rc-attr="accentMode">
													<option value="stripe" <?php selected($rate_card_defaults['accentMode'], 'stripe'); ?>><?php esc_html_e('Top stripe', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="color-border" <?php selected($rate_card_defaults['accentMode'], 'color-border'); ?>><?php esc_html_e('Solid color border', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="none" <?php selected($rate_card_defaults['accentMode'], 'none'); ?>><?php esc_html_e('None', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-accentTheme"><?php esc_html_e('Theme preset', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="hidden" id="crtoday-rc-sc-accentTheme" data-rc-attr="accentTheme" value="<?php echo esc_attr($rate_card_defaults['accentTheme']); ?>" />
												<div class="crtoday-theme-grid" data-crtoday-theme-grid data-crtoday-theme-target="#crtoday-rc-sc-accentTheme">
													<?php foreach ($theme_presets as $preset) : ?>
														<?php $is_rc_active = $rate_card_defaults['accentTheme'] === $preset['key']; ?>
														<button type="button" class="crtoday-theme-swatch<?php echo $is_rc_active ? ' is-active' : ''; ?>" data-crtoday-theme-value="<?php echo esc_attr($preset['key']); ?>" aria-label="<?php echo esc_attr($preset['name']); ?>" title="<?php echo esc_attr($preset['name']); ?>">
															<span class="crtoday-theme-swatch-dot" style="background:<?php echo esc_attr($preset['color']); ?>;"></span>
															<span class="crtoday-theme-swatch-name"><?php echo esc_html($preset['name']); ?></span>
														</button>
													<?php endforeach; ?>
												</div>
											</td>
										</tr>
										<tr id="crtoday-rc-row-accentAngle">
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-rc-sc-accentAngle"><?php esc_html_e('Stripe angle', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="range" id="crtoday-rc-sc-accentAngle" data-rc-attr="accentAngle" value="<?php echo esc_attr((string) $rate_card_defaults['accentAngle']); ?>" min="0" max="360" step="1" class="crtoday-range-input" />
												<span id="crtoday-rc-accent-angle-value" class="crtoday-range-value"><?php echo esc_html((string) $rate_card_defaults['accentAngle']); ?>°</span>
											</td>
										</tr>
									</table>
								</div>
							</div>
						</div>
					</div>

					<div class="crtoday-admin-shortcode-output">
						<h3 class="crtoday-sc-heading">
							<span class="dashicons dashicons-shortcode crtoday-sc-heading-icon"></span>
							<?php esc_html_e('Generated Shortcode', 'currencyrate-today-currency-blocks'); ?>
						</h3>
						<div class="crtoday-admin-shortcode-output-row">
							<code id="crtoday-rc-shortcode-output">[crtoday_rate_card]</code>
							<button type="button" id="crtoday-rc-copy-shortcode" class="button button-primary"
								data-copied-label="<?php echo esc_attr(__('Copied!', 'currencyrate-today-currency-blocks')); ?>">
								<span class="dashicons dashicons-clipboard"></span>
								<?php esc_html_e('Copy', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>
					</div>
				</div>

				<!-- Price Badge Block Generator -->
				<div class="crtoday-sc-pane" data-pane="price-badge" role="tabpanel">
					<div id="crtoday-pb-shortcode-form" class="crtoday-sc-layout">

						<div class="crtoday-sc-preview-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-visibility crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Preview', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div id="crtoday-pb-shortcode-preview" class="crtoday-sc-preview" style="min-height:120px;">
								<span class="spinner is-active crtoday-spinner-centered"></span>
							</div>
						</div>

						<div class="crtoday-sc-settings-column">
							<h3 class="crtoday-sc-heading">
								<span class="dashicons dashicons-admin-generic crtoday-sc-heading-icon"></span>
								<?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?>
							</h3>
							<div class="crtoday-sc-tabs" data-crtoday-settings-tabs>
								<button type="button" class="button button-primary" data-crtoday-tab="data"><?php esc_html_e('Settings', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="presentation"><?php esc_html_e('Display', 'currencyrate-today-currency-blocks'); ?></button>
								<button type="button" class="button button-secondary" data-crtoday-tab="style"><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></button>
							</div>

							<!-- Settings pane -->
							<div class="postbox crtoday-sc-panel" data-crtoday-pane="data">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-database"></span><?php esc_html_e('Data settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-pb-sc-source"><?php esc_html_e('Data source', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-pb-sc-source" data-pb-attr="source" class="regular-text">
													<?php self::render_source_options($source_options, $pb_defaults['source']); ?>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('Primary currency', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<?php
												$pb_from_code = $pb_defaults['primaryCurrency'];
												$pb_from_flag = $currencies_full[$pb_from_code]['flag'] ?? strtolower($pb_from_code);
												?>
												<input type="hidden" id="crtoday-pb-sc-primaryCurrency" data-pb-attr="primaryCurrency" value="<?php echo esc_attr($pb_from_code); ?>" />
												<button type="button" class="crtoday-currency-picker-btn" data-crtoday-picker="true" data-target="crtoday-pb-sc-primaryCurrency" data-value="<?php echo esc_attr($pb_from_code); ?>" aria-haspopup="listbox" aria-expanded="false">
													<img class="crtoday-picker-flag" src="<?php echo esc_url($flag_base_url . $pb_from_flag . '.webp'); ?>" width="20" height="20" alt="" />
													<span class="crtoday-picker-code"><?php echo esc_html($pb_from_code); ?></span>
													<span class="crtoday-picker-chevron">▾</span>
												</button>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label><?php esc_html_e('Target currencies', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<div class="crtoday-multi-picker-wrap" data-crtoday-multi-picker="true">
													<input type="hidden" id="crtoday-pb-sc-targetCurrencies" data-pb-attr="targetCurrencies" value="<?php echo esc_attr(implode(',', $pb_defaults['targetCurrencies'])); ?>" />
													<div class="crtoday-multi-picker-tags">
														<?php foreach ($pb_defaults['targetCurrencies'] as $sym) :
															$sym_flag = isset($currencies_full[$sym]['flag']) ? $currencies_full[$sym]['flag'] : strtolower($sym);
														?>
															<span class="crtoday-tag" data-code="<?php echo esc_attr($sym); ?>">
																<img class="crtoday-tag-flag" src="<?php echo esc_url($flag_base_url . $sym_flag . '.webp'); ?>" alt="" width="16" height="16" />
																<span><?php echo esc_html($sym); ?></span>
																<button type="button" class="crtoday-tag-remove" aria-label="<?php
																	/* translators: %s: currency code or pair (e.g. USD, EUR/USD) */
																	echo esc_attr(sprintf(__('Remove %s', 'currencyrate-today-currency-blocks'), $sym)); ?>">×</button>
															</span>
														<?php endforeach; ?>
														<button type="button" class="crtoday-multi-picker-add" aria-haspopup="listbox" aria-expanded="false">
															<span class="dashicons dashicons-plus-alt2" style="font-size:14px;width:14px;height:14px;"></span>
															<?php esc_html_e('Add', 'currencyrate-today-currency-blocks'); ?>
														</button>
													</div>
												</div>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-primaryPrice"><?php esc_html_e('Price', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-pb-sc-primaryPrice" data-pb-attr="primaryPrice" value="<?php echo esc_attr((string) $pb_defaults['primaryPrice']); ?>" min="0.00001" max="999999999" step="1" class="crtoday-input-sm" />
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-adjustmentPercent"><?php esc_html_e('Adjustment (%)', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="number" id="crtoday-pb-sc-adjustmentPercent" data-pb-attr="adjustmentPercent" value="<?php echo esc_attr((string) $pb_defaults['adjustmentPercent']); ?>" min="-1000000" max="1000000" step="0.05" class="crtoday-input-sm" />
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Display pane -->
							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="presentation">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-desktop"></span><?php esc_html_e('Presentation settings', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-pb-sc-decimals"><?php esc_html_e('Decimals', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-pb-sc-decimals" data-pb-attr="decimals" value="<?php echo esc_attr((string) $pb_defaults['decimals']); ?>" min="0" max="8" step="1" class="crtoday-range-input" /> <span id="crtoday-pb-decimals-value" class="crtoday-range-value"><?php echo esc_html((string) $pb_defaults['decimals']); ?></span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-theme"><?php esc_html_e('Theme', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-pb-sc-theme" data-pb-attr="theme">
													<option value="auto" <?php selected($pb_defaults['theme'], 'auto'); ?>><?php esc_html_e('Auto (system)', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="light" <?php selected($pb_defaults['theme'], 'light'); ?>><?php esc_html_e('Light', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="dark" <?php selected($pb_defaults['theme'], 'dark'); ?>><?php esc_html_e('Dark', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><?php esc_html_e('Display options', 'currencyrate-today-currency-blocks'); ?></th>
											<td class="crtoday-sc-td">
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="seamless" <?php checked($pb_defaults['seamless']); ?> /> <?php esc_html_e('Transparent — for placing inside a custom container', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="showFlags" <?php checked($pb_defaults['showFlags']); ?> /> <?php esc_html_e('Show flags', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="showCurrencySymbols" <?php checked($pb_defaults['showCurrencySymbols']); ?> /> <?php esc_html_e('Show currency symbols', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="alwaysExpanded" <?php checked($pb_defaults['alwaysExpanded']); ?> /> <?php esc_html_e('Always expanded', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="fullWidth" <?php checked($pb_defaults['fullWidth']); ?> /> <?php esc_html_e('Full width', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="compactMode" <?php checked($pb_defaults['compactMode']); ?> /> <?php esc_html_e('Compact mode', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="showTimestamp" <?php checked($pb_defaults['showTimestamp']); ?> /> <?php esc_html_e('Show timestamp', 'currencyrate-today-currency-blocks'); ?></label>
												<label class="crtoday-check-label"><input type="checkbox" data-pb-attr="shadow" <?php checked($pb_defaults['shadow']); ?> /> <?php esc_html_e('Enable shadow', 'currencyrate-today-currency-blocks'); ?></label>
											</td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-sourceLabel"><?php esc_html_e('Source label', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="text" id="crtoday-pb-sc-sourceLabel" data-pb-attr="sourceLabel" value="<?php echo esc_attr($pb_defaults['sourceLabel']); ?>" maxlength="20" class="crtoday-input-md" />
												<p class="description"><?php esc_html_e('Custom label for the data source footer.', 'currencyrate-today-currency-blocks'); ?></p>
											</td>
										</tr>
									</table>
								</div>
							</div>

							<!-- Style pane -->
							<div class="is-hidden postbox crtoday-sc-panel" data-crtoday-pane="style">
								<div class="postbox-header">
									<h3 class="crtoday-sc-panel-heading"><span class="dashicons dashicons-art"></span><?php esc_html_e('Style', 'currencyrate-today-currency-blocks'); ?></h3>
								</div>
								<div class="inside crtoday-sc-panel-body">
									<table class="crtoday-sc-form-table form-table" role="presentation">
										<tr>
											<th scope="row" class="crtoday-sc-th crtoday-sc-th--w130"><label for="crtoday-pb-sc-radius"><?php esc_html_e('Corner radius', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-pb-sc-radius" data-pb-attr="radius" value="<?php echo esc_attr((string) $pb_defaults['radius']); ?>" min="0" max="32" step="1" class="crtoday-range-input" /> <span id="crtoday-pb-radius-value" class="crtoday-range-value"><?php echo esc_html((string) $pb_defaults['radius']); ?>px</span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-accentMode"><?php esc_html_e('Accent display', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<select id="crtoday-pb-sc-accentMode" data-pb-attr="accentMode">
													<option value="stripe" <?php selected($pb_defaults['accentMode'], 'stripe'); ?>><?php esc_html_e('Top stripe', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="color-border" <?php selected($pb_defaults['accentMode'], 'color-border'); ?>><?php esc_html_e('Solid color border', 'currencyrate-today-currency-blocks'); ?></option>
													<option value="none" <?php selected($pb_defaults['accentMode'], 'none'); ?>><?php esc_html_e('None', 'currencyrate-today-currency-blocks'); ?></option>
												</select>
											</td>
										</tr>
										<tr id="crtoday-pb-row-accentAngle">
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-accentAngle"><?php esc_html_e('Accent angle', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td"><input type="range" id="crtoday-pb-sc-accentAngle" data-pb-attr="accentAngle" value="<?php echo esc_attr((string) $pb_defaults['accentAngle']); ?>" min="0" max="360" step="5" class="crtoday-range-input" /> <span id="crtoday-pb-accent-angle-value" class="crtoday-range-value"><?php echo esc_html((string) $pb_defaults['accentAngle']); ?>°</span></td>
										</tr>
										<tr>
											<th scope="row" class="crtoday-sc-th"><label for="crtoday-pb-sc-accentTheme"><?php esc_html_e('Theme preset', 'currencyrate-today-currency-blocks'); ?></label></th>
											<td class="crtoday-sc-td">
												<input type="hidden" id="crtoday-pb-sc-accentTheme" data-pb-attr="accentTheme" value="<?php echo esc_attr($pb_defaults['accentTheme']); ?>" />
												<div class="crtoday-theme-grid" data-crtoday-theme-grid data-crtoday-theme-target="#crtoday-pb-sc-accentTheme">
													<?php foreach ($theme_presets as $preset) : ?>
														<?php $is_pb_active = $pb_defaults['accentTheme'] === $preset['key']; ?>
														<button type="button" class="crtoday-theme-swatch<?php echo $is_pb_active ? ' is-active' : ''; ?>" data-crtoday-theme-value="<?php echo esc_attr($preset['key']); ?>" aria-label="<?php echo esc_attr($preset['name']); ?>" title="<?php echo esc_attr($preset['name']); ?>">
															<span class="crtoday-theme-swatch-dot" style="background:<?php echo esc_attr($preset['color']); ?>;"></span>
															<span class="crtoday-theme-swatch-name"><?php echo esc_html($preset['name']); ?></span>
														</button>
													<?php endforeach; ?>
												</div>
											</td>
										</tr>
									</table>
								</div>
							</div>
						</div>
					</div>

					<div class="crtoday-admin-shortcode-output">
						<h3 class="crtoday-sc-heading">
							<span class="dashicons dashicons-shortcode crtoday-sc-heading-icon"></span>
							<?php esc_html_e('Generated Shortcode', 'currencyrate-today-currency-blocks'); ?>
						</h3>
						<div class="crtoday-admin-shortcode-output-row">
							<code id="crtoday-pb-shortcode-output">[crtoday_price_badge]</code>
							<button type="button" id="crtoday-pb-copy-shortcode" class="button button-primary"
								data-copied-label="<?php echo esc_attr(__('Copied!', 'currencyrate-today-currency-blocks')); ?>">
								<span class="dashicons dashicons-clipboard"></span>
								<?php esc_html_e('Copy', 'currencyrate-today-currency-blocks'); ?>
							</button>
						</div>
					</div>

				</div>
			</div>
		</div>
<?php
	}

	/* ================================================================
	 * Admin assets
	 * ================================================================ */

	public static function enqueue_admin_assets(string $hook): void
	{
		if ($hook !== 'settings_page_' . self::PAGE_SLUG) {
			return;
		}

		wp_enqueue_style(
			'crtoday-admin-style',
			CRTODAY_PLUGIN_URL . 'assets/admin/admin.css',
			[],
			CRTODAY_PLUGIN_VERSION
		);

		/* Inline-only handle for tab-specific scripts added during render. */
		$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page tab switch, no data mutation.

		$inline_deps = [];

		/* Currency picker is used on both shortcodes and sources tabs. */
		if ($tab === 'shortcodes' || $tab === 'sources') {
			$inline_deps[] = 'crtoday-currency-picker';
			wp_enqueue_script(
				'crtoday-currency-picker',
				CRTODAY_PLUGIN_URL . 'assets/admin/currency-picker.js',
				[],
				CRTODAY_PLUGIN_VERSION,
				true
			);

			wp_localize_script(
				'crtoday-currency-picker',
				'crtodayAdminCurrencies',
				self::get_currencies_for_editor()
			);

			wp_localize_script(
				'crtoday-currency-picker',
				'crtodayCurrencyPickerI18n',
				[
					'groupLabels' => self::get_currency_group_labels(),
					'groupOrder'  => self::get_currency_group_order(),
					'labels'      => [
						'selectCurrency'       => __('Select currency', 'currencyrate-today-currency-blocks'),
						'selectCurrencies'     => __('Select currencies', 'currencyrate-today-currency-blocks'),
						'searchCurrency'       => __('Search currency…', 'currencyrate-today-currency-blocks'),
						'searchCurrencies'     => __('Search currencies', 'currencyrate-today-currency-blocks'),
						/* translators: %s: currency code or pair (e.g. USD, EUR/USD) */
						'removeCurrencyPattern'=> __('Remove %s', 'currencyrate-today-currency-blocks'),
					],
				]
			);

			wp_add_inline_script(
				'crtoday-currency-picker',
				'window.crtodayFlagBaseUrl = ' . wp_json_encode(CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/') . ';',
				'before'
			);
		}

		wp_register_script('crtoday-admin-inline', false, $inline_deps, CRTODAY_PLUGIN_VERSION, ['in_footer' => true]); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
		wp_enqueue_script('crtoday-admin-inline');

		if ($tab === 'storage' || $tab === 'sources') {
			wp_enqueue_script(
				'crtoday-shortcode-generator',
				CRTODAY_PLUGIN_URL . 'assets/admin/shortcode-generator.js',
				[],
				CRTODAY_PLUGIN_VERSION,
				true
			);
			return;
		}

		if ($tab !== 'shortcodes') {
			return;
		}

		/* Block frontend CSS — needed to render the widget preview */
		$style_file = null;
		if (file_exists(CRTODAY_PLUGIN_DIR . 'build/style-index.css')) {
			$style_file = 'style-index.css';
		} elseif (file_exists(CRTODAY_PLUGIN_DIR . 'build/index.css')) {
			$style_file = 'index.css';
		}
		if ($style_file !== null) {
			wp_enqueue_style(
				'crtoday-block-style-admin',
				CRTODAY_PLUGIN_URL . 'build/' . $style_file,
				[],
				CRTODAY_PLUGIN_VERSION
			);
		}

		/* Shortcode generator logic */
		wp_enqueue_script(
			'crtoday-shortcode-generator',
			CRTODAY_PLUGIN_URL . 'assets/admin/shortcode-generator.js',
			[],
			CRTODAY_PLUGIN_VERSION,
			true
		);

		wp_localize_script(
			'crtoday-shortcode-generator',
			'crtodayShortcodeGeneratorConfig',
			[
				'defaults' => [
					'ctaButtonText'          => __('Convert', 'currencyrate-today-currency-blocks') . " \u{203A}",
					'ctaButtonTextExchange'  => __('Action', 'currencyrate-today-currency-blocks') . " \u{203A}",
				],
			]
		);

		/* Rate-card chart frontend — needed to render sparklines in preview */
		if (wp_script_is('crtoday-rate-card-frontend', 'registered')) {
			wp_enqueue_script('crtoday-rate-card-frontend');
		}

		/* Currency picker is already enqueued above for both shortcodes and sources tabs. */
	}

	/* ================================================================
	 * Action handlers
	 * ================================================================ */

	public static function handle_invalidate_cache(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_invalidate_cache');
		self::invalidate_cache();
		self::redirect_to_storage('crtoday_cache_invalidated');
	}

	public static function handle_invalidate_expired(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_invalidate_expired');
		self::invalidate_expired_cache();
		self::redirect_to_storage('crtoday_expired_cleared');
	}

	public static function handle_force_fetch(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_force_fetch');
		self::invalidate_cache();
		self::redirect_with('crtoday_force_fetched');
	}

	public static function handle_delete_data(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_delete_data');
		self::delete_all_data();
		self::redirect_to_storage('crtoday_data_deleted');
	}

	/* ── Debug AJAX handler (CRTODAY_DEBUG only) ── */

	public static function handle_debug_action(): void
	{
		if (! defined('CRTODAY_DEBUG') || ! CRTODAY_DEBUG) {
			wp_send_json_error('Debug mode is disabled.', 403);
		}

		if (! current_user_can('manage_options')) {
			wp_send_json_error('Forbidden', 403);
		}

		check_ajax_referer('crtoday_debug_action', 'nonce');

		$action = isset($_POST['debug_action'])
			? sanitize_key(wp_unslash($_POST['debug_action']))
			: '';

		if ($action === 'trigger_review_banner') {
			/* Render the review banner HTML and return it */
			ob_start();
			CRToday_Review_Banner::force_render();
			$html = ob_get_clean();
			wp_send_json_success(['html' => $html]);
		}

		if ($action === 'reset_review_banner') {
			CRToday_Review_Banner::reset_state();
			wp_send_json_success();
		}

		wp_send_json_error('Unknown debug action.', 400);
	}

	/* ================================================================
	 * Helpers
	 * ================================================================ */

	/**
	 * Verify that the current user has admin capability.
	 *
	 * Terminates with wp_die() on failure — shared guard for all admin action handlers.
	 * Each handler must also call check_admin_referer() with its own nonce action.
	 */
	private static function require_admin_cap(): void
	{
		if (! current_user_can('manage_options')) {
			wp_die(esc_html__('You do not have permission to perform this action.', 'currencyrate-today-currency-blocks'));
		}
	}

	private static function redirect_with(string $flag): void
	{
		wp_safe_redirect(
			add_query_arg(
				['page' => self::PAGE_SLUG, $flag => '1'],
				admin_url('options-general.php')
			)
		);
		exit;
	}

	private static function redirect_to_storage(string $flag = ''): void
	{
		$args = ['page' => self::PAGE_SLUG, 'tab' => 'storage'];
		if ($flag !== '') {
			$args[$flag] = '1';
		}
		wp_safe_redirect(add_query_arg($args, admin_url('options-general.php')));
		exit;
	}

	private static function redirect_to_sources(string $flag = ''): void
	{
		$args = ['page' => self::PAGE_SLUG, 'tab' => 'sources'];
		if ($flag !== '') {
			$args[$flag] = '1';
		}
		wp_safe_redirect(add_query_arg($args, admin_url('options-general.php')));
		exit;
	}

	/* ================================================================
	 * Local source admin handlers
	 * ================================================================ */

	public static function handle_save_local_source(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_save_local_source');

		$id   = isset($_POST['local_source_id']) ? absint(wp_unslash($_POST['local_source_id'])) : 0;
		$name = isset($_POST['local_source_name']) ? sanitize_text_field(wp_unslash($_POST['local_source_name'])) : '';
		$base = isset($_POST['local_source_base']) ? strtoupper(sanitize_text_field(wp_unslash($_POST['local_source_base']))) : '';
		$mode = ( isset($_POST['local_source_mode']) && sanitize_key(wp_unslash($_POST['local_source_mode'])) === 'bidask' ) ? 'bidask' : 'mid';

		if ($name === '' || ! isset(self::currencies()[$base])) {
			wp_die(esc_html__('Invalid source name or base currency.', 'currencyrate-today-currency-blocks'));
		}

		/* Enforce local source limit (new sources only) */
		if ($id === 0) {
			$existing = self::get_local_sources();
			if (count($existing) >= self::MAX_SOURCES) {
				wp_die(
					sprintf(
						/* translators: %d: maximum number of custom sources allowed */
						esc_html__('You can create up to %d local sources.', 'currencyrate-today-currency-blocks'),
						absint( self::MAX_SOURCES )
					)
				);
			}
		}

		/* Parse rates from POST */
		$raw_codes = isset($_POST['rate_code']) && is_array($_POST['rate_code']) ? array_map('sanitize_key', wp_unslash($_POST['rate_code'])) : [];
		$raw_mids  = isset($_POST['rate_mid'])  && is_array($_POST['rate_mid'])  ? array_map('sanitize_text_field', wp_unslash($_POST['rate_mid']))  : [];
		$raw_bids  = isset($_POST['rate_bid'])  && is_array($_POST['rate_bid'])  ? array_map('sanitize_text_field', wp_unslash($_POST['rate_bid']))  : [];
		$raw_asks  = isset($_POST['rate_ask'])  && is_array($_POST['rate_ask'])  ? array_map('sanitize_text_field', wp_unslash($_POST['rate_ask']))  : [];

		$rates = [];
		foreach ($raw_codes as $i => $code) {
			$code = strtoupper($code);
			if (! isset(self::currencies()[$code]) || $code === $base) {
				continue;
			}

			$mid = $raw_mids[$i] ?? '0';
			$bid = $raw_bids[$i] ?? '0';
			$ask = $raw_asks[$i] ?? '0';

			if ($mode === 'bidask') {
				if (! is_numeric($bid) || ! is_numeric($ask) || (float) $bid <= 0 || (float) $ask <= 0) {
					continue;
				}
				/* Compute mid from bid/ask */
				$mid = (string) (((float) $bid + (float) $ask) / 2);
			} else {
				if (! is_numeric($mid) || (float) $mid <= 0) {
					continue;
				}
				$bid = '0';
				$ask = '0';
			}

			$rates[$code] = [
				'mid' => $mid,
				'bid' => $bid,
				'ask' => $ask,
			];
		}

		if ($rates === []) {
			wp_die(esc_html__('At least one valid currency rate is required.', 'currencyrate-today-currency-blocks'));
		}

		if ($id === 0) {
			$id = self::next_local_id();
		}

		$source_data = [
			'name'         => mb_substr($name, 0, 100),
			'base'         => $base,
			'pricing_mode' => $mode,
			'rates'        => $rates,
			'updated_at'   => time(),
		];

		self::save_local_source($id, $source_data);
		self::persist_local_source_to_db($id, $source_data);

		self::redirect_to_sources('crtoday_source_saved');
	}

	public static function handle_delete_local_source(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_delete_local_source');

		$id = isset($_POST['local_source_id']) ? absint(wp_unslash($_POST['local_source_id'])) : 0;
		if ($id > 0) {
			self::delete_local_source($id);
		}

		self::redirect_to_sources('crtoday_source_deleted');
	}

	public static function handle_refresh_local_source(): void
	{
		self::require_admin_cap();
		check_admin_referer('crtoday_refresh_local_source');

		$id = isset($_POST['local_source_id']) ? absint(wp_unslash($_POST['local_source_id'])) : 0;
		self::touch_local_source($id);

		self::redirect_to_sources('crtoday_source_refreshed');
	}

	public static function handle_ajax_refresh_local_source(): void
	{
		self::require_admin_cap();
		check_ajax_referer('crtoday_refresh_local_source');

		$id = isset($_POST['local_source_id']) ? absint(wp_unslash($_POST['local_source_id'])) : 0;
		if (! self::touch_local_source($id)) {
			wp_send_json_error('Source not found', 404);
		}

		$locals = self::get_local_sources();
		$human  = human_time_diff($locals[$id]['updated_at']) . ' ' . __('ago', 'currencyrate-today-currency-blocks');
		wp_send_json_success(['human' => $human]);
	}

	private static function touch_local_source(int $id): bool
	{
		$locals = self::get_local_sources();
		if ($id <= 0 || ! isset($locals[$id])) {
			return false;
		}

		$locals[$id]['updated_at'] = time();
		update_option(self::LOCAL_SOURCES_KEY, $locals, false);
		self::persist_local_source_to_db($id, $locals[$id]);

		return true;
	}

	private static function format_number(int $n): string
	{
		if ($n < 1000) {
			return (string) $n;
		}
		if ($n < 1000000) {
			$v = $n / 1000;
			return (fmod($v, 1.0) === 0.0 ? (int) $v : number_format($v, 1)) . 'k';
		}
		$v = $n / 1000000;
		return (fmod($v, 1.0) === 0.0 ? (int) $v : number_format($v, 1)) . 'M';
	}

	/**
	 * Render an admin flash notice if the flag is truthy.
	 */
	private static function render_flash(bool $flag, string $message, string $type = 'success'): void
	{
		if (! $flag) {
			return;
		}
		printf(
			'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
			esc_attr($type),
			esc_html($message)
		);
	}

	private static function format_bytes(int $bytes): string
	{
		if ($bytes < 1024) {
			return $bytes . ' B';
		}
		if ($bytes < 1048576) {
			return round($bytes / 1024, 1) . ' KB';
		}
		return round($bytes / 1048576, 2) . ' MB';
	}
}
