Песочница

Демонстрация и готовые решения: смарт-контракты, шорткоды WordPress, плагины WooCommerce, кастомные шапки, галереи, калькуляторы и другие полезные фрагменты кода.

📝 Примеры кода

📰

News shortcode with AJAX load more

Shortcode [ferrum_news] displays a news grid with a Load More button.

/**
 * Шорткод новостей с кнопкой "Загрузить ещё"
 * Использование: [ferrum_news posts="6" cols="3" cat="novosti" show_image="yes" excerpt_length="20" read_more="Подробнее"]
 * 
 * Если параметр cat не указан, шорткод пытается автоматически определить текущую рубрику (категорию)
 * на страницах архивов. Для меток, таксономий и других архивов требуется отдельная доработка.
 */

function ferrum_pro_news_shortcode($atts) {
    // Параметры по умолчанию
    $atts = shortcode_atts(array(
        'posts'          => 6,          // количество постов на загрузку
        'cols'           => 3,          // колонок на десктопе
        'cat'            => '',
        'show_image'     => 'yes',
        'excerpt_length' => 20,
        'read_more'      => 'Подробнее',
        'load_more'      => 'yes',      // включать кнопку "Загрузить ещё"
    ), $atts, 'ferrum_news');

    $posts_per_page = intval($atts['posts']);
    $cols = intval($atts['cols']);
    $category = sanitize_text_field($atts['cat']);
    $show_image = $atts['show_image'] === 'yes' ? 'yes' : 'no';
    $excerpt_length = intval($atts['excerpt_length']);
    $read_more_text = sanitize_text_field($atts['read_more']);
    $load_more_enabled = $atts['load_more'] === 'yes';

    // === АВТООПРЕДЕЛЕНИЕ КАТЕГОРИИ, ЕСЛИ CAT НЕ ЗАДАН ===
    if (empty($category)) {
        if (is_category()) {
            $current_cat = get_queried_object();
            if (!empty($current_cat->slug)) {
                $category = $current_cat->slug;
            }
        }
        // Если нужно поддержать метки:
        // elseif (is_tag()) {
        //     $current_tag = get_queried_object();
        //     if (!empty($current_tag->slug)) {
        //         // Для меток потребуется изменить $args на 'tag' => $slug
        //     }
        // }
        // Для произвольных таксономий можно добавить аналогично.
    }
    // === КОНЕЦ АВТООПРЕДЕЛЕНИЯ ===

    // Запрос для первой порции
    $args = array(
        'posts_per_page' => $posts_per_page,
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'offset'         => 0,
    );
    if (!empty($category)) {
        $args['category_name'] = $category;
    }

    $query = new WP_Query($args);
    $total_posts = $query->found_posts;
    $has_more = ($total_posts > $posts_per_page);

    if (!$query->have_posts()) {
        return '<p class="ferrum-news-empty">Новостей не найдено.</p>';
    }

    static $grid_id = 0;
    $grid_id++;
    $wrapper_id = 'ferrum-news-wrapper-' . $grid_id;
    $grid_selector = 'ferrum-news-grid-' . $grid_id;

    // Начинаем вывод
    $output = '<div class="ferrum-news-wrapper" id="' . esc_attr($wrapper_id) . '">';
    $output .= '<div class="ferrum-news-grid" id="' . esc_attr($grid_selector) . '" style="--ferrum-cols: ' . esc_attr($cols) . ';">';

    while ($query->have_posts()) {
        $query->the_post();
        $output .= ferrum_render_news_item($show_image, $excerpt_length, $read_more_text);
    }

    $output .= '</div>'; // .ferrum-news-grid

    // Кнопка "Загрузить ещё"
    if ($load_more_enabled && $has_more) {
        $load_more_data = array(
            'ajax_url'        => admin_url('admin-ajax.php'),
            'action'          => 'ferrum_load_more',
            'nonce'           => wp_create_nonce('ferrum_load_more_nonce'),
            'offset'          => $posts_per_page,
            'posts_per_page'  => $posts_per_page,
            'total'           => $total_posts,
            'cat'             => $category, // теперь $category либо явный, либо автоопределённый
            'show_image'      => $show_image,
            'excerpt_length'  => $excerpt_length,
            'read_more_text'  => $read_more_text,
        );
        $output .= '<div class="ferrum-news-loadmore-container" data-loadmore="' . esc_attr(json_encode($load_more_data)) . '">';
        $output .= '<button class="ferrum-news-loadmore-btn">Загрузить ещё</button>';
        $output .= '</div>';
    }

    $output .= '</div>'; // .ferrum-news-wrapper

    wp_reset_postdata();

    // Подключаем CSS и JS один раз
    add_action('wp_footer', 'ferrum_news_assets');
    
    return $output;
}
add_shortcode('ferrum_news', 'ferrum_pro_news_shortcode');

/**
 * Вспомогательная функция для генерации HTML одного поста
 */
function ferrum_render_news_item($show_image, $excerpt_length, $read_more_text) {
    $post_id = get_the_ID();
    $title = get_the_title();
    $permalink = get_permalink();
    $date = get_the_date('d.m.Y');
    $excerpt = wp_trim_words(get_the_excerpt(), $excerpt_length, '...');
    $image = '';
    if ($show_image === 'yes' && has_post_thumbnail()) {
        $image = get_the_post_thumbnail($post_id, 'medium', array('class' => 'ferrum-news-img', 'loading' => 'lazy'));
    }

    $item = '<div class="ferrum-news-item">';
    if ($image) {
        $item .= '<div class="ferrum-news-image">';
        $item .= '<a href="' . esc_url($permalink) . '">' . $image . '</a>';
        $item .= '</div>';
    }
    $item .= '<div class="ferrum-news-content">';
    $item .= '<div class="ferrum-news-date">' . esc_html($date) . '</div>';
    $item .= '<h3 class="ferrum-news-title"><a href="' . esc_url($permalink) . '">' . esc_html($title) . '</a></h3>';
    $item .= '<div class="ferrum-news-excerpt">' . wp_kses_post($excerpt) . '</div>';
    $item .= '<a href="' . esc_url($permalink) . '" class="ferrum-news-readmore">' . esc_html($read_more_text) . '</a>';
    $item .= '</div>';
    $item .= '</div>';
    return $item;
}

/**
 * AJAX-обработчик загрузки следующих постов
 */
function ferrum_ajax_load_more() {
    // Проверка nonce
    if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'ferrum_load_more_nonce')) {
        wp_die('Ошибка безопасности');
    }

    $offset = intval($_POST['offset']);
    $posts_per_page = intval($_POST['posts_per_page']);
    $category = sanitize_text_field($_POST['cat']);
    $show_image = sanitize_text_field($_POST['show_image']);
    $excerpt_length = intval($_POST['excerpt_length']);
    $read_more_text = sanitize_text_field($_POST['read_more_text']);

    // AJAX-запрос приходит уже с определённой категорией (явной или автоматически определённой в шорткоде)
    $args = array(
        'posts_per_page' => $posts_per_page,
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'offset'         => $offset,
    );
    if (!empty($category)) {
        $args['category_name'] = $category;
    }

    $query = new WP_Query($args);
    $total_posts = $query->found_posts;
    $has_more = ($offset + $posts_per_page) < $total_posts;

    $html = '';
    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            $html .= ferrum_render_news_item($show_image, $excerpt_length, $read_more_text);
        }
    }

    wp_reset_postdata();

    wp_send_json(array(
        'html'     => $html,
        'has_more' => $has_more,
        'new_offset' => $offset + $posts_per_page,
    ));
}
add_action('wp_ajax_ferrum_load_more', 'ferrum_ajax_load_more');
add_action('wp_ajax_nopriv_ferrum_load_more', 'ferrum_ajax_load_more');

/**
 * Вывод CSS и JS в подвале (один раз)
 */
function ferrum_news_assets() {
    static $assets_loaded = false;
    if ($assets_loaded) {
        return;
    }
    $assets_loaded = true;
    ?>
    <style>
        /* === Профессиональный стиль новостей FerrumLeks === */
        .ferrum-news-wrapper {
            margin: 0 auto;
            padding: 20px 0;
        }
        .ferrum-news-grid {
            display: grid;
            grid-template-columns: repeat(var(--ferrum-cols, 3), 1fr);
            gap: 30px;
        }
        .ferrum-news-item {
            background: #fff;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
            transition: box-shadow 0.3s ease;
        }
        .ferrum-news-item:hover {
            box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
            transform: none;
        }
        .ferrum-news-image {
            width: 100%;
            height: 240px;
            overflow: hidden;
            background: #f5f5f5;
        }
        .ferrum-news-image img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.4s ease;
        }
        .ferrum-news-item:hover .ferrum-news-image img {
            transform: scale(1.05);
        }
        .ferrum-news-content {
            padding: 20px;
        }
        .ferrum-news-date {
            font-size: 13px;
            color: #8c8f94;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            margin-bottom: 10px;
            font-weight: 500;
        }
        .ferrum-news-title {
            font-size: 20px !important;
            margin: 0 0 12px !important;
            line-height: 1.4 !important;
            text-align: justify;
        }
        .ferrum-news-title a {
            color: #1e2a36;
            text-decoration: none;
            transition: color 0.2s;
        }
        .ferrum-news-title a:hover {
            color: #d35400;
        }
        .ferrum-news-excerpt {
            font-size: 15px;
            color: #4a5562;
            line-height: 1.5;
            margin-bottom: 20px;
            text-align: justify;
        }
        /* Кнопка с анимацией подчёркивания и отложенной сменой стрелки */
        .ferrum-news-readmore {
            position: relative;
            display: inline-block;
            font-size: 18px;
            font-weight: 600;
            color: #d35400;
            text-decoration: none;
            background: transparent;
            border: none;
            cursor: pointer;
        }
        .ferrum-news-readmore:hover {
            color: #d35400;
        }
        .ferrum-news-readmore::before {
            content: '';
            position: absolute;
            bottom: -2px;
            left: 0;
            width: 0;
            height: 1px;
            background: #d35400;
            transition: width 0.6s ease-out, opacity 0.2s ease-out 0.6s;
        }
        .ferrum-news-readmore:hover::before {
            width: 100%;
            opacity: 0;
        }
        .ferrum-news-readmore::after {
            content: '→';
            margin-left: 0.3em;
            display: inline-block;
            transition: none;
        }
        .ferrum-news-readmore.hover::after {
            content: '>';
        }

        /* Кнопка "Загрузить ещё" */
        .ferrum-news-loadmore-container {
            text-align: center;
            margin-top: 40px;
        }
        .ferrum-news-loadmore-btn {
            background: #d35400;
            color: #fff;
            border: none;
            padding: 12px 30px;
            font-size: 16px;
            font-weight: 600;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.3s;
        }
        .ferrum-news-loadmore-btn:hover {
            background: #b84300;
        }
        .ferrum-news-loadmore-btn.loading {
            opacity: 0.7;
            cursor: default;
        }

        /* Адаптивность */
        @media (max-width: 992px) {
            .ferrum-news-grid {
                gap: 20px;
            }
            .ferrum-news-title {
                font-size: 18px;
            }
        }
        @media (min-width: 769px) and (max-width: 992px) {
            .ferrum-news-grid {
                grid-template-columns: repeat(2, 1fr);
            }
        }
        @media (max-width: 768px) {
            .ferrum-news-grid {
                grid-template-columns: 1fr !important;
            }
        }
    </style>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // Функция инициализации анимации для ссылок "Подробнее"
            function initReadmoreButtons(container) {
                var buttons = container.querySelectorAll('.ferrum-news-readmore:not(.initialized)');
                buttons.forEach(function(btn) {
                    var timeout;
                    btn.addEventListener('mouseenter', function() {
                        if (timeout) clearTimeout(timeout);
                        timeout = setTimeout(function() {
                            btn.classList.add('hover');
                        }, 600);
                    });
                    btn.addEventListener('mouseleave', function() {
                        if (timeout) clearTimeout(timeout);
                        btn.classList.remove('hover');
                    });
                    btn.classList.add('initialized');
                });
            }

            // Функция настройки кнопок "Загрузить ещё" для каждого wrapper'а
            function setupLoadMore(wrapper) {
                var container = wrapper.querySelector('.ferrum-news-loadmore-container');
                if (!container) return;

                var btn = container.querySelector('.ferrum-news-loadmore-btn');
                if (!btn) return;

                var dataAttr = container.getAttribute('data-loadmore');
                if (!dataAttr) return;

                var data = JSON.parse(dataAttr);
                var grid = wrapper.querySelector('.ferrum-news-grid');
                if (!grid) return;

                btn.addEventListener('click', function(e) {
                    e.preventDefault();
                    if (btn.classList.contains('loading')) return;

                    btn.classList.add('loading');
                    btn.textContent = 'Загрузка...';

                    var params = new URLSearchParams();
                    params.append('action', data.action);
                    params.append('nonce', data.nonce);
                    params.append('offset', data.offset);
                    params.append('posts_per_page', data.posts_per_page);
                    params.append('cat', data.cat);
                    params.append('show_image', data.show_image);
                    params.append('excerpt_length', data.excerpt_length);
                    params.append('read_more_text', data.read_more_text);

                    fetch(data.ajax_url, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                        },
                        body: params.toString()
                    })
                    .then(response => response.json())
                    .then(response => {
                        if (response.html) {
                            // Вставляем новые посты
                            grid.insertAdjacentHTML('beforeend', response.html);
                            // Инициализируем анимацию для новых ссылок
                            initReadmoreButtons(grid);
                        }
                        // Обновляем offset
                        data.offset = response.new_offset;
                        // Обновляем data-атрибут
                        container.setAttribute('data-loadmore', JSON.stringify(data));
                        // Если больше нет постов, скрываем кнопку
                        if (!response.has_more) {
                            container.style.display = 'none';
                        }
                        btn.classList.remove('loading');
                        btn.textContent = 'Загрузить ещё';
                    })
                    .catch(error => {
                        console.error('Ошибка загрузки:', error);
                        btn.classList.remove('loading');
                        btn.textContent = 'Загрузить ещё';
                    });
                });
            }

            // Находим все обёртки шорткода и настраиваем для каждой
            var wrappers = document.querySelectorAll('.ferrum-news-wrapper');
            wrappers.forEach(function(wrapper) {
                // Инициализируем анимацию для уже существующих ссылок
                initReadmoreButtons(wrapper);
                // Настраиваем кнопку загрузки
                setupLoadMore(wrapper);
            });
        });
    </script>
    <?php
}
Paste into your theme's functions.php or an MU-plugin.
Use shortcode: [ferrum_news posts="6" cols="3" cat="news" show_image="yes" excerpt_length="20" read_more="Read more"].
Parameters:
- posts: number of posts,
- cols: columns,
- cat: category slug,
- show_image: yes/no,
- excerpt_length: words in excerpt,
- read_more: button text.
The Load More button fetches next posts via AJAX.
⚙️

Bulk Price Update for WooCommerce

Plugin for bulk price updates (increase or decrease) with exclusions and rollback.

<?php
/**
 * Plugin Name: WooCommerce Bulk Price Increase
 * Description: Массовое изменение цен в WooCommerce с исключениями, пакетной обработкой, блокировкой повторного запуска и откатом.
 * Version: 2.0.0
 * Author: OpenAI
 */

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

if ( ! class_exists( 'BPI_Bulk_Price_Increase' ) ) :

final class BPI_Bulk_Price_Increase {

	const PAGE_SLUG              = 'bulk-price-increase';
	const TASK_OPTION            = 'bpi_task';
	const LAST_OPERATION_OPTION  = 'bpi_last_operation';
	const BATCH_SIZE             = 25;
	const MAX_PERCENT            = 1000;
	const TASK_TTL               = 6 * HOUR_IN_SECONDS;
	const LAST_OPERATION_TTL     = 7 * DAY_IN_SECONDS;
	const LOCK_TTL               = 10 * MINUTE_IN_SECONDS;
	const AJAX_NONCE_ACTION      = 'bpi_ajax_nonce';
	const PRODUCT_NONCE_ACTION   = 'bpi_product_metabox';
	const TERM_NONCE_ACTION      = 'bpi_term_metabox';
	const LOG_SOURCE             = 'bpi-bulk-price';

	/**
	 * Boot.
	 */
	public static function init() {
		add_action( 'admin_menu', array( __CLASS__, 'add_admin_page' ) );
		add_action( 'wp_ajax_bpi_init_task', array( __CLASS__, 'ajax_init_task' ) );
		add_action( 'wp_ajax_bpi_process_batch', array( __CLASS__, 'ajax_process_batch' ) );
		add_action( 'wp_ajax_bpi_rollback', array( __CLASS__, 'ajax_rollback' ) );

		add_action( 'add_meta_boxes', array( __CLASS__, 'add_product_metabox' ) );
		add_action( 'save_post_product', array( __CLASS__, 'save_product_metabox' ) );

		add_action( 'product_cat_add_form_fields', array( __CLASS__, 'render_term_add_field' ) );
		add_action( 'product_cat_edit_form_fields', array( __CLASS__, 'render_term_edit_field' ) );
		add_action( 'created_product_cat', array( __CLASS__, 'save_term_field' ) );
		add_action( 'edited_product_cat', array( __CLASS__, 'save_term_field' ) );
	}

	/**
	 * Returns true when WooCommerce is available.
	 */
	private static function is_woocommerce_ready() {
		return class_exists( 'WooCommerce' ) && function_exists( 'wc_get_price_decimals' );
	}

	/**
	 * Current user capability required by this tool.
	 */
	private static function required_capability() {
		return current_user_can( 'manage_woocommerce' ) ? 'manage_woocommerce' : 'manage_options';
	}

	/**
	 * Tiny logger wrapper.
	 */
	private static function log( $level, $message, array $context = array() ) {
		$context = array_merge(
			array(
				'source' => self::LOG_SOURCE,
			),
			$context
		);

		if ( function_exists( 'wc_get_logger' ) ) {
			$logger = wc_get_logger();
			if ( is_object( $logger ) && method_exists( $logger, $level ) ) {
				$logger->{$level}( $message, $context );
				return;
			}
		}

		error_log( sprintf( '[%s] %s %s', self::LOG_SOURCE, $message, wp_json_encode( $context ) ) );
	}

	/**
	 * Admin submenu.
	 */
	public static function add_admin_page() {
		if ( ! current_user_can( self::required_capability() ) ) {
			return;
		}

		add_submenu_page(
			'woocommerce',
			'Массовое изменение цен',
			'Массовое изменение цен',
			self::required_capability(),
			self::PAGE_SLUG,
			array( __CLASS__, 'render_admin_page' )
		);
	}

	/**
	 * Retrieve current task.
	 */
	private static function get_task() {
		$task = get_option( self::TASK_OPTION, false );

		return is_array( $task ) ? $task : false;
	}

	/**
	 * Save task without autoload.
	 */
	private static function save_task( array $task ) {
		if ( false === get_option( self::TASK_OPTION, false ) ) {
			add_option( self::TASK_OPTION, $task, '', 'no' );
			return;
		}

		update_option( self::TASK_OPTION, $task );
	}

	/**
	 * Delete task.
	 */
	private static function delete_task() {
		delete_option( self::TASK_OPTION );
		delete_transient( self::TASK_OPTION );
		delete_transient( self::TASK_OPTION . '_lock' );
	}

	/**
	 * Retrieve last operation.
	 */
	private static function get_last_operation() {
		$last = get_option( self::LAST_OPERATION_OPTION, false );

		return is_array( $last ) ? $last : false;
	}

	/**
	 * Save rollback data without autoload.
	 */
	private static function save_last_operation( array $operation ) {
		if ( false === get_option( self::LAST_OPERATION_OPTION, false ) ) {
			add_option( self::LAST_OPERATION_OPTION, $operation, '', 'no' );
			return;
		}

		update_option( self::LAST_OPERATION_OPTION, $operation );
	}

	/**
	 * Delete rollback data.
	 */
	private static function delete_last_operation() {
		delete_option( self::LAST_OPERATION_OPTION );
	}

	/**
	 * Check whether the task expired.
	 */
	private static function is_task_stale( array $task ) {
		if ( empty( $task['started_at'] ) ) {
			return true;
		}

		$started_at = absint( $task['started_at'] );
		if ( ! $started_at ) {
			return true;
		}

		return ( time() - $started_at ) > self::TASK_TTL;
	}

	/**
	 * Clear lock if task vanished or expired.
	 */
	private static function maybe_cleanup_stale_task() {
		$task = self::get_task();

		if ( ! $task ) {
			self::delete_task();
			return;
		}

		if ( ! isset( $task['status'] ) || 'in_progress' !== $task['status'] ) {
			return;
		}

		if ( self::is_task_stale( $task ) ) {
			self::log( 'warning', 'Stale task removed automatically', array( 'task_id' => isset( $task['task_id'] ) ? $task['task_id'] : '' ) );
			self::delete_task();
		}
	}

	/**
	 * Get a user-facing error message from a response.
	 */
	private static function error_message( $message, $default = 'Произошла ошибка.' ) {
		if ( is_string( $message ) && '' !== trim( $message ) ) {
			return $message;
		}

		if ( is_array( $message ) && isset( $message['message'] ) ) {
			return (string) $message['message'];
		}

		return $default;
	}

	/**
	 * Render admin page.
	 */
	public static function render_admin_page() {
		if ( ! current_user_can( self::required_capability() ) ) {
			return;
		}

		self::maybe_cleanup_stale_task();

		$task         = self::get_task();
		$in_progress   = ( $task && isset( $task['status'] ) && 'in_progress' === $task['status'] );
		$last_op       = self::get_last_operation();
		$can_rollback  = ( $last_op && ! empty( $last_op['prices'] ) && is_array( $last_op['prices'] ) );
		$categories    = array();
		$woo_is_ready  = self::is_woocommerce_ready();

		if ( $woo_is_ready ) {
			$categories = get_terms(
				array(
					'taxonomy'   => 'product_cat',
					'hide_empty' => false,
					'orderby'    => 'name',
					'order'      => 'ASC',
				)
			);

			if ( is_wp_error( $categories ) ) {
				$categories = array();
			}
		}
		?>
		<div class="wrap">
			<h1>Массовое изменение цен</h1>

			<?php if ( ! $woo_is_ready ) : ?>
				<div class="notice notice-error">
					<p>WooCommerce не активен или недоступен. Плагин не может работать без WooCommerce.</p>
				</div>
			<?php endif; ?>

			<?php if ( $can_rollback ) : ?>
				<div class="notice notice-success is-dismissible">
					<p>Есть сохранённая операция от <?php echo esc_html( $last_op['time'] ); ?>. Доступен откат последних изменений.</p>
					<p>
						<button class="button button-secondary" id="bpi-rollback-btn" type="button">Откатить последнее изменение</button>
						<span id="bpi-rollback-spinner" style="display:none; margin-left:10px;">Восстановление…</span>
					</p>
				</div>
			<?php endif; ?>

			<?php if ( $in_progress ) : ?>
				<div class="notice notice-warning">
					<p>В данный момент уже выполняется обработка. Страница покажет актуальный статус после обновления.</p>
				</div>
			<?php endif; ?>

			<form id="bpi-main-form" method="post" onsubmit="return false;">
				<table class="form-table">
					<tr>
						<th scope="row"><label for="bpi-percent">Процент увеличения</label></th>
						<td>
							<input type="number" step="0.01" min="0.01" max="<?php echo esc_attr( self::MAX_PERCENT ); ?>" id="bpi-percent" name="bpi_percent" value="20" required style="width:120px;"> %
							<p class="description">Увеличит обычную и распродажную цену на одинаковый процент.</p>
						</td>
					</tr>
					<tr>
						<th scope="row"><label for="bpi-categories">Категории товаров</label></th>
						<td>
							<?php if ( ! empty( $categories ) ) : ?>
								<select name="bpi_categories[]" id="bpi-categories" multiple size="8" style="min-width:300px; max-width:100%;">
									<?php foreach ( $categories as $cat ) : ?>
										<option value="<?php echo esc_attr( $cat->term_id ); ?>"><?php echo esc_html( $cat->name ); ?></option>
									<?php endforeach; ?>
								</select>
								<p class="description">Можно выбрать несколько категорий. Товары из выбранных категорий будут обработаны с учётом исключений.</p>
							<?php else : ?>
								<p>Категории не найдены.</p>
							<?php endif; ?>
						</td>
					</tr>
				</table>

				<p class="submit">
					<button type="button" id="bpi-start-btn" class="button button-primary" <?php echo $in_progress || ! $woo_is_ready ? 'disabled' : ''; ?>>Изменить цены на указанный %</button>
					<span id="bpi-global-spinner" style="display:none; margin-left:10px;">Подготовка…</span>
				</p>
			</form>

			<div id="bpi-progress" style="display:none; margin-top:20px;">
				<h3>Прогресс обработки</h3>
				<div style="background:#f0f0f0; border:1px solid #ccc; border-radius:3px; overflow:hidden; max-width:500px;">
					<div id="bpi-progress-bar" style="width:0%; height:24px; background:#007cba; transition: width 0.3s;"></div>
				</div>
				<p><span id="bpi-progress-text">0 / 0 товаров обработано</span></p>
			</div>
		</div>

		<script type="text/javascript">
		jQuery(document).ready(function($) {
			var bpiConfig = {
				nonce: '<?php echo esc_js( wp_create_nonce( self::AJAX_NONCE_ACTION ) ); ?>',
				ajaxUrl: '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>',
				maxPercent: <?php echo (int) self::MAX_PERCENT; ?>
			};

			var currentTaskId = null;
			var batchRetryCount = 0;
			var maxBatchRetries = 2;

			function getErrorMessage(response, fallback) {
				fallback = fallback || 'Произошла ошибка.';
				if (!response) {
					return fallback;
				}
				if (typeof response === 'string') {
					return response;
				}
				if (response.data) {
					if (typeof response.data === 'string') {
						return response.data;
					}
					if (response.data.message) {
						return response.data.message;
					}
				}
				if (response.message) {
					return response.message;
				}
				return fallback;
			}

			function resetUI() {
				$('#bpi-start-btn').prop('disabled', false);
				$('#bpi-global-spinner').hide();
				$('#bpi-rollback-btn').prop('disabled', false);
				$('#bpi-rollback-spinner').hide();
			}

			function updateProgress(done, total) {
				var safeTotal = parseInt(total, 10) || 0;
				var safeDone = parseInt(done, 10) || 0;
				var percent = safeTotal > 0 ? Math.min(100, Math.round((safeDone / safeTotal) * 100)) : 0;
				$('#bpi-progress-bar').css('width', percent + '%');
				$('#bpi-progress-text').text(safeDone + ' / ' + safeTotal + ' товаров');
			}

			function processBatch(taskId) {
				$.post(bpiConfig.ajaxUrl, {
					action: 'bpi_process_batch',
					nonce: bpiConfig.nonce,
					task_id: taskId
				}, function(response) {
					if (!response || !response.success) {
						alert('Ошибка обработки: ' + getErrorMessage(response, 'Не удалось обработать партию.'));
						resetUI();
						return;
					}

					var data = response.data || {};
					updateProgress(data.done, data.total);

					if (data.warnings && data.warnings.length) {
						console.warn('BPI warnings:', data.warnings);
					}

					if (data.finished) {
						alert('Цены успешно обновлены.');
						resetUI();
						location.reload();
						return;
					}

					batchRetryCount = 0;
					processBatch(taskId);
				}).fail(function() {
					if (batchRetryCount < maxBatchRetries) {
						batchRetryCount++;
						setTimeout(function() {
							processBatch(taskId);
						}, 1000 * batchRetryCount);
						return;
					}

					alert('Сетевая ошибка при обработке партии.');
					resetUI();
				});
			}

			$('#bpi-start-btn').on('click', function(e) {
				e.preventDefault();

				if (!<?php echo $woo_is_ready ? 'true' : 'false'; ?>) {
					alert('WooCommerce не активен.');
					return;
				}

				var percent = parseFloat($('#bpi-percent').val());
				if (isNaN(percent) || percent <= 0 || percent > bpiConfig.maxPercent) {
					alert('Введите корректный процент в диапазоне от 0.01 до ' + bpiConfig.maxPercent + '.');
					return;
				}

				var categories = $('#bpi-categories').val();
				if (!categories || categories.length === 0) {
					alert('Выберите хотя бы одну категорию.');
					return;
				}

				$('#bpi-start-btn').prop('disabled', true);
				$('#bpi-global-spinner').show();
				$('#bpi-progress').show();
				$('#bpi-progress-bar').css('width', '0%');
				$('#bpi-progress-text').text('Инициализация…');

				$.post(bpiConfig.ajaxUrl, {
					action: 'bpi_init_task',
					nonce: bpiConfig.nonce,
					percent: percent,
					categories: categories
				}, function(response) {
					if (!response || !response.success) {
						alert('Ошибка: ' + getErrorMessage(response, 'Не удалось инициализировать задачу.'));
						resetUI();
						return;
					}

					var data = response.data || {};
					if (!data.total || data.total <= 0) {
						alert('Нет товаров для обработки.');
						resetUI();
						return;
					}

					currentTaskId = data.task_id;
					updateProgress(0, data.total);
					processBatch(currentTaskId);
				}).fail(function() {
					alert('Сетевая ошибка при инициализации.');
					resetUI();
				});
			});

			$('#bpi-rollback-btn').on('click', function() {
				if (!confirm('Вы уверены, что хотите откатить последнее изменение цен?')) {
					return;
				}

				var btn = $(this);
				btn.prop('disabled', true);
				$('#bpi-rollback-spinner').show();

				$.post(bpiConfig.ajaxUrl, {
					action: 'bpi_rollback',
					nonce: bpiConfig.nonce
				}, function(response) {
					if (response && response.success) {
						alert('Откат выполнен успешно.');
						location.reload();
						return;
					}

					alert('Ошибка отката: ' + getErrorMessage(response, 'Не удалось выполнить откат.'));
					btn.prop('disabled', false);
					$('#bpi-rollback-spinner').hide();
				}).fail(function() {
					alert('Сетевая ошибка при откате.');
					btn.prop('disabled', false);
					$('#bpi-rollback-spinner').hide();
				});
			});
		});
		</script>
		<?php
	}

	/**
	 * Ajax: init task.
	 */
	public static function ajax_init_task() {
		check_ajax_referer( self::AJAX_NONCE_ACTION, 'nonce' );

		if ( ! current_user_can( self::required_capability() ) ) {
			wp_send_json_error( 'Недостаточно прав.', 403 );
		}

		if ( ! self::is_woocommerce_ready() ) {
			wp_send_json_error( 'WooCommerce недоступен.', 500 );
		}

		self::maybe_cleanup_stale_task();

		$existing = self::get_task();
		if ( $existing && isset( $existing['status'] ) && 'in_progress' === $existing['status'] ) {
			wp_send_json_error( 'Уже выполняется другая обработка. Дождитесь окончания или обновите страницу.', 409 );
		}

		$percent = isset( $_POST['percent'] ) ? (float) wp_unslash( $_POST['percent'] ) : 0.0;
		$percent = round( $percent, 2 );

		if ( $percent <= 0 || $percent > self::MAX_PERCENT ) {
			wp_send_json_error( 'Неверный процент. Допустимый диапазон: от 0.01 до ' . self::MAX_PERCENT . '.', 400 );
		}

		$categories = isset( $_POST['categories'] ) ? (array) wp_unslash( $_POST['categories'] ) : array();
		$categories = array_values( array_unique( array_filter( array_map( 'absint', $categories ) ) ) );

		if ( empty( $categories ) ) {
			wp_send_json_error( 'Выберите хотя бы одну категорию.', 400 );
		}

		$task = self::build_task( $percent, $categories );
		if ( is_wp_error( $task ) ) {
			self::log( 'error', 'Failed to build task', array( 'error' => $task->get_error_message() ) );
			wp_send_json_error( $task->get_error_message(), 500 );
		}

		if ( empty( $task['ids'] ) ) {
			wp_send_json_error( 'Нет товаров для обработки — все товары исключены или не найдены.', 400 );
		}

		self::save_task( $task );

		self::log(
			'info',
			'Task initialized',
			array(
				'task_id'  => $task['task_id'],
				'total'    => $task['total'],
				'percent'  => $task['percent'],
				'user_id'  => get_current_user_id(),
			)
		);

		wp_send_json_success(
			array(
				'total'   => $task['total'],
				'task_id' => $task['task_id'],
			)
		);
	}

	/**
	 * Build task payload.
	 */
	private static function build_task( $percent, array $categories ) {
		$selected_product_ids = self::query_product_ids_by_categories( $categories );
		if ( is_wp_error( $selected_product_ids ) ) {
			return $selected_product_ids;
		}

		$term_exclude_cache = array();
		$product_excluded_cache = array();
		$eligible_products = array();
		$eligible_variations = array();

		foreach ( $selected_product_ids as $product_id ) {
			$product_id = absint( $product_id );
			if ( ! $product_id ) {
				continue;
			}

			if ( self::is_product_excluded( $product_id, $term_exclude_cache, $product_excluded_cache ) ) {
				continue;
			}

			$eligible_products[] = $product_id;
		}

		if ( ! empty( $eligible_products ) ) {
			$eligible_variations = self::query_child_variations( $eligible_products );
		}

		$ids = array_merge( $eligible_products, $eligible_variations );
		$ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) );

		sort( $ids, SORT_NUMERIC );

		$task = array(
			'status'      => 'in_progress',
			'task_id'     => wp_generate_uuid4(),
			'percent'     => (float) $percent,
			'categories'  => array_values( array_map( 'absint', $categories ) ),
			'ids'         => $ids,
			'offset'      => 0,
			'total'       => count( $ids ),
			'old_prices'  => array(),
			'started_at'  => time(),
			'started_by'  => get_current_user_id(),
			'updated_at'  => time(),
		);

		return $task;
	}

	/**
	 * Query products by categories in batches.
	 */
	private static function query_product_ids_by_categories( array $categories ) {
		$categories = array_values( array_unique( array_filter( array_map( 'absint', $categories ) ) ) );
		if ( empty( $categories ) ) {
			return array();
		}

		$product_ids = array();
		$page        = 1;
		$per_page    = 500;

		do {
			$query = new WP_Query(
				array(
					'post_type'              => 'product',
					'post_status'            => 'publish',
					'fields'                 => 'ids',
					'posts_per_page'         => $per_page,
					'paged'                  => $page,
					'orderby'                => 'ID',
					'order'                  => 'ASC',
					'ignore_sticky_posts'    => true,
					'no_found_rows'          => false,
					'suppress_filters'       => true,
					'update_post_meta_cache' => false,
					'update_post_term_cache' => false,
					'tax_query'              => array(
						array(
							'taxonomy' => 'product_cat',
							'field'    => 'term_id',
							'terms'    => $categories,
							'operator' => 'IN',
						),
					),
				)
			);

			if ( ! empty( $query->posts ) && is_array( $query->posts ) ) {
				$product_ids = array_merge( $product_ids, array_map( 'absint', $query->posts ) );
			}

			$max_pages = absint( $query->max_num_pages );
			wp_reset_postdata();
			$page++;
		} while ( $page <= $max_pages && $max_pages > 0 );

		return array_values( array_unique( array_filter( $product_ids ) ) );
	}

	/**
	 * Query variation children for products.
	 */
	private static function query_child_variations( array $parent_ids ) {
		$parent_ids = array_values( array_unique( array_filter( array_map( 'absint', $parent_ids ) ) ) );
		if ( empty( $parent_ids ) ) {
			return array();
		}

		$variation_ids = array();
		$chunks        = array_chunk( $parent_ids, 300 );

		foreach ( $chunks as $chunk ) {
			$page = 1;
			do {
				$query = new WP_Query(
					array(
						'post_type'              => 'product_variation',
						'post_status'            => array( 'publish' ),
						'fields'                 => 'ids',
						'posts_per_page'         => 500,
						'paged'                  => $page,
						'orderby'                => 'ID',
						'order'                  => 'ASC',
						'ignore_sticky_posts'    => true,
						'no_found_rows'          => false,
						'suppress_filters'       => true,
						'update_post_meta_cache' => false,
						'update_post_term_cache' => false,
						'post_parent__in'        => $chunk,
					)
				);

				if ( ! empty( $query->posts ) && is_array( $query->posts ) ) {
					$variation_ids = array_merge( $variation_ids, array_map( 'absint', $query->posts ) );
				}

				$max_pages = absint( $query->max_num_pages );
				wp_reset_postdata();
				$page++;
			} while ( $page <= $max_pages && $max_pages > 0 );
		}

		return array_values( array_unique( array_filter( $variation_ids ) ) );
	}

	/**
	 * Check whether product should be excluded.
	 */
	private static function is_product_excluded( $product_id, array &$term_exclude_cache, array &$product_excluded_cache ) {
		$product_id = absint( $product_id );
		if ( ! $product_id ) {
			return true;
		}

		if ( isset( $product_excluded_cache[ $product_id ] ) ) {
			return (bool) $product_excluded_cache[ $product_id ];
		}

		$exclude = ( 'yes' === get_post_meta( $product_id, '_bpi_exclude', true ) );

		if ( ! $exclude ) {
			$term_ids = wp_get_object_terms(
				$product_id,
				'product_cat',
				array(
					'fields' => 'ids',
				)
			);

			if ( ! is_wp_error( $term_ids ) && ! empty( $term_ids ) ) {
				foreach ( $term_ids as $term_id ) {
					$term_id = absint( $term_id );
					if ( ! $term_id ) {
						continue;
					}

					if ( ! array_key_exists( $term_id, $term_exclude_cache ) ) {
						$term_exclude_cache[ $term_id ] = ( 'yes' === get_term_meta( $term_id, 'bpi_exclude', true ) );
					}

					if ( true === $term_exclude_cache[ $term_id ] ) {
						$exclude = true;
						break;
					}
				}
			}
		}

		$product_excluded_cache[ $product_id ] = $exclude ? 1 : 0;

		return $exclude;
	}

	/**
	 * Process batch.
	 */
	public static function ajax_process_batch() {
		check_ajax_referer( self::AJAX_NONCE_ACTION, 'nonce' );

		if ( ! current_user_can( self::required_capability() ) ) {
			wp_send_json_error( 'Недостаточно прав.', 403 );
		}

		if ( ! self::is_woocommerce_ready() ) {
			wp_send_json_error( 'WooCommerce недоступен.', 500 );
		}

		$task_id = isset( $_POST['task_id'] ) ? sanitize_text_field( wp_unslash( $_POST['task_id'] ) ) : '';
		if ( '' === $task_id ) {
			wp_send_json_error( 'Не указан идентификатор задачи.', 400 );
		}

		$task = self::get_task();
		if ( ! $task || ! is_array( $task ) || empty( $task['task_id'] ) ) {
			wp_send_json_error( 'Нет активной задачи.', 404 );
		}

		if ( empty( $task['status'] ) || 'in_progress' !== $task['status'] ) {
			wp_send_json_error( 'Задача не находится в состоянии обработки.', 409 );
		}

		if ( self::is_task_stale( $task ) ) {
			self::delete_task();
			wp_send_json_error( 'Задача устарела и была очищена. Запустите процесс заново.', 410 );
		}

		if ( $task['task_id'] !== $task_id ) {
			wp_send_json_error( 'Идентификатор задачи не совпадает. Обновите страницу и запустите процесс заново.', 409 );
		}

		$ids        = isset( $task['ids'] ) && is_array( $task['ids'] ) ? array_values( array_map( 'absint', $task['ids'] ) ) : array();
		$total      = count( $ids );
		$offset     = isset( $task['offset'] ) ? absint( $task['offset'] ) : 0;
		$percent    = isset( $task['percent'] ) ? (float) $task['percent'] : 0.0;
		$old_prices = isset( $task['old_prices'] ) && is_array( $task['old_prices'] ) ? $task['old_prices'] : array();

		if ( empty( $ids ) || $total <= 0 ) {
			self::delete_task();
			wp_send_json_error( 'Нет товаров для обработки.', 400 );
		}

		if ( $offset >= $total ) {
			self::finalize_task( $task, $old_prices );
			wp_send_json_success(
				array(
					'done'     => $total,
					'total'    => $total,
					'finished' => true,
					'warnings' => array(),
				)
			);
		}

		$batch_ids = array_slice( $ids, $offset, self::BATCH_SIZE );
		if ( empty( $batch_ids ) ) {
			wp_send_json_error( 'Не удалось получить партию для обработки.', 500 );
		}

		$warnings = array();

		foreach ( $batch_ids as $product_id ) {
			$product_id = absint( $product_id );
			if ( ! $product_id ) {
				$offset++;
				continue;
			}

			try {
				$result = self::update_single_price( $product_id, $percent, $old_prices );
				if ( is_wp_error( $result ) ) {
					$warnings[] = array(
						'product_id' => $product_id,
						'message'    => $result->get_error_message(),
					);
					self::log( 'warning', 'Price update skipped', array( 'product_id' => $product_id, 'error' => $result->get_error_message() ) );
				}
			} catch ( Exception $e ) {
				$warnings[] = array(
					'product_id' => $product_id,
					'message'    => $e->getMessage(),
				);
				self::log( 'error', 'Exception while updating price', array( 'product_id' => $product_id, 'exception' => $e->getMessage() ) );
			} catch ( Error $e ) {
				$warnings[] = array(
					'product_id' => $product_id,
					'message'    => $e->getMessage(),
				);
				self::log( 'error', 'Fatal error while updating price', array( 'product_id' => $product_id, 'error' => $e->getMessage() ) );
			}

			$offset++;
			$task['offset']     = $offset;
			$task['old_prices'] = $old_prices;
			$task['updated_at'] = time();
			self::save_task( $task );
		}

		$finished = ( $offset >= $total );

		if ( $finished ) {
			self::finalize_task( $task, $old_prices );
		} else {
			$task['offset']     = $offset;
			$task['old_prices'] = $old_prices;
			$task['updated_at'] = time();
			self::save_task( $task );
		}

		wp_send_json_success(
			array(
				'done'     => min( $offset, $total ),
				'total'    => $total,
				'finished' => $finished,
				'warnings' => $warnings,
			)
		);
	}

	/**
	 * Finalize task and store rollback state.
	 */
	private static function finalize_task( array $task, array $old_prices ) {
		$task['status']     = 'completed';
		$task['updated_at'] = time();
		self::save_task( $task );

		$last_op = array(
			'time'      => current_time( 'mysql' ),
			'time_gmt'  => current_time( 'mysql', true ),
			'task_id'   => isset( $task['task_id'] ) ? $task['task_id'] : '',
			'percent'   => isset( $task['percent'] ) ? (float) $task['percent'] : 0,
			'categories'=> isset( $task['categories'] ) ? array_values( array_map( 'absint', (array) $task['categories'] ) ) : array(),
			'prices'    => $old_prices,
		);

		self::save_last_operation( $last_op );

		self::delete_task();

		self::log(
			'info',
			'Task completed',
			array(
				'task_id' => isset( $task['task_id'] ) ? $task['task_id'] : '',
				'total'   => isset( $task['total'] ) ? absint( $task['total'] ) : 0,
			)
		);
	}

	/**
	 * Update single product or variation price.
	 */
	private static function update_single_price( $product_id, $percent, array &$old_prices ) {
		$product_id = absint( $product_id );
		if ( ! $product_id ) {
			return new WP_Error( 'bpi_invalid_product', 'Некорректный ID товара.' );
		}

		if ( isset( $old_prices[ $product_id ] ) ) {
			// Already processed in a previous retry; do not apply the increase again.
			return true;
		}

		$regular_raw = get_post_meta( $product_id, '_regular_price', true );
		$sale_raw    = get_post_meta( $product_id, '_sale_price', true );

		if ( '' === $regular_raw || ! is_numeric( $regular_raw ) ) {
			return new WP_Error( 'bpi_missing_regular_price', 'У товара отсутствует корректная regular price.' );
		}

		$regular = (float) $regular_raw;
		$sale    = ( '' !== $sale_raw && is_numeric( $sale_raw ) ) ? (float) $sale_raw : null;

		$old_prices[ $product_id ] = array(
			'regular' => (string) $regular_raw,
			'sale'    => ( '' !== $sale_raw ? (string) $sale_raw : '' ),
		);

		$new_regular = self::calc_new_price( $regular, $percent );
		$new_regular = self::format_price_for_storage( $new_regular );

		update_post_meta( $product_id, '_regular_price', $new_regular );

		if ( null !== $sale ) {
			$new_sale = self::calc_new_price( $sale, $percent );
			$new_sale = self::format_price_for_storage( $new_sale );

			// Keep sale price below regular price when rounding causes equality.
			if ( (float) $new_sale >= (float) $new_regular ) {
				$decimals   = function_exists( 'wc_get_price_decimals' ) ? (int) wc_get_price_decimals() : 2;
				$step       = 1 / pow( 10, max( 0, $decimals ) );
				$new_sale_f = max( 0, (float) $new_regular - $step );
				$new_sale   = self::format_price_for_storage( $new_sale_f );

				if ( (float) $new_sale >= (float) $new_regular ) {
					$new_sale = self::format_price_for_storage( max( 0, (float) $new_regular - $step ) );
				}
			}

			update_post_meta( $product_id, '_sale_price', $new_sale );
			update_post_meta( $product_id, '_price', $new_sale );
		} else {
			delete_post_meta( $product_id, '_sale_price' );
			update_post_meta( $product_id, '_price', $new_regular );
		}

		self::clear_product_caches( $product_id );

		// Variation updates can affect the parent product visibility/price caches.
		$parent_id = wp_get_post_parent_id( $product_id );
		if ( $parent_id ) {
			self::clear_product_caches( $parent_id );
		}

		return true;
	}

	/**
	 * Calculate new price.
	 */
	private static function calc_new_price( $price, $percent ) {
		$price   = (float) $price;
		$percent = (float) $percent;

		return $price * ( 1 + ( $percent / 100 ) );
	}

	/**
	 * Format price for WooCommerce storage.
	 */
	private static function format_price_for_storage( $price ) {
		$decimals = function_exists( 'wc_get_price_decimals' ) ? (int) wc_get_price_decimals() : 2;

		return wc_format_decimal( $price, $decimals );
	}

	/**
	 * Clear caches and lookup tables for a product.
	 */
	private static function clear_product_caches( $product_id ) {
		$product_id = absint( $product_id );
		if ( ! $product_id ) {
			return;
		}

		if ( function_exists( 'wc_delete_product_transients' ) ) {
			wc_delete_product_transients( $product_id );
		}

		clean_post_cache( $product_id );

		if ( function_exists( 'wc_update_product_lookup_tables' ) ) {
			try {
				wc_update_product_lookup_tables( $product_id );
			} catch ( Exception $e ) {
				self::log( 'warning', 'Lookup table update failed', array( 'product_id' => $product_id, 'error' => $e->getMessage() ) );
			} catch ( Error $e ) {
				self::log( 'warning', 'Lookup table update failed', array( 'product_id' => $product_id, 'error' => $e->getMessage() ) );
			}
		}
	}

	/**
	 * Ajax: rollback last operation.
	 */
	public static function ajax_rollback() {
		check_ajax_referer( self::AJAX_NONCE_ACTION, 'nonce' );

		if ( ! current_user_can( self::required_capability() ) ) {
			wp_send_json_error( 'Недостаточно прав.', 403 );
		}

		if ( ! self::is_woocommerce_ready() ) {
			wp_send_json_error( 'WooCommerce недоступен.', 500 );
		}

		$last_op = self::get_last_operation();
		if ( ! $last_op || empty( $last_op['prices'] ) || ! is_array( $last_op['prices'] ) ) {
			wp_send_json_error( 'Нет сохранённой операции для отката.', 404 );
		}

		$prices   = $last_op['prices'];
		$restored = 0;
		$errors   = array();

		foreach ( $prices as $product_id => $old ) {
			$product_id = absint( $product_id );
			if ( ! $product_id || ! is_array( $old ) ) {
				continue;
			}

			$regular = isset( $old['regular'] ) ? $old['regular'] : '';
			$sale    = isset( $old['sale'] ) ? $old['sale'] : '';

			try {
				if ( '' !== $regular ) {
					update_post_meta( $product_id, '_regular_price', $regular );
				}

				if ( '' !== $sale ) {
					update_post_meta( $product_id, '_sale_price', $sale );
					update_post_meta( $product_id, '_price', $sale );
				} else {
					delete_post_meta( $product_id, '_sale_price' );
					update_post_meta( $product_id, '_price', $regular );
				}

				self::clear_product_caches( $product_id );
				$parent_id = wp_get_post_parent_id( $product_id );
				if ( $parent_id ) {
					self::clear_product_caches( $parent_id );
				}

				$restored++;
			} catch ( Exception $e ) {
				$errors[] = array(
					'product_id' => $product_id,
					'message'    => $e->getMessage(),
				);
				self::log( 'error', 'Rollback failed', array( 'product_id' => $product_id, 'error' => $e->getMessage() ) );
			} catch ( Error $e ) {
				$errors[] = array(
					'product_id' => $product_id,
					'message'    => $e->getMessage(),
				);
				self::log( 'error', 'Rollback failed', array( 'product_id' => $product_id, 'error' => $e->getMessage() ) );
			}
		}

		self::delete_last_operation();

		wp_send_json_success(
			array(
				'restored' => $restored,
				'errors'   => $errors,
			)
		);
	}

	/**
	 * Product metabox.
	 */
	public static function add_product_metabox() {
		add_meta_box(
			'bpi_exclude_metabox',
			'Массовое изменение цен',
			array( __CLASS__, 'render_product_metabox' ),
			'product',
			'side',
			'low'
		);
	}

	/**
	 * Render product metabox.
	 */
	public static function render_product_metabox( $post ) {
		wp_nonce_field( self::PRODUCT_NONCE_ACTION, 'bpi_product_metabox_nonce' );
		$excluded = get_post_meta( $post->ID, '_bpi_exclude', true );
		?>
		<label>
			<input type="checkbox" name="bpi_exclude" value="yes" <?php checked( $excluded, 'yes' ); ?>>
			Не повышать автоматически
		</label>
		<p class="description">Если отмечено, этот товар и его вариации будут пропущены при массовом увеличении цен.</p>
		<?php
	}

	/**
	 * Save product metabox.
	 */
	public static function save_product_metabox( $post_id ) {
		if ( ! isset( $_POST['bpi_product_metabox_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bpi_product_metabox_nonce'] ) ), self::PRODUCT_NONCE_ACTION ) ) {
			return;
		}

		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$exclude = isset( $_POST['bpi_exclude'] ) ? 'yes' : 'no';
		update_post_meta( $post_id, '_bpi_exclude', $exclude );
	}

	/**
	 * Category add field.
	 */
	public static function render_term_add_field() {
		wp_nonce_field( self::TERM_NONCE_ACTION, 'bpi_term_nonce' );
		?>
		<div class="form-field">
			<label>
				<input type="checkbox" name="bpi_exclude_cat" value="yes">
				Не повышать автоматически
			</label>
			<p class="description">Все товары этой категории будут исключены из массового повышения цен.</p>
		</div>
		<?php
	}

	/**
	 * Category edit field.
	 */
	public static function render_term_edit_field( $term ) {
		wp_nonce_field( self::TERM_NONCE_ACTION, 'bpi_term_nonce' );
		$excluded = get_term_meta( $term->term_id, 'bpi_exclude', true );
		?>
		<tr class="form-field">
			<th scope="row"><label for="bpi_exclude_cat">Массовое изменение цен</label></th>
			<td>
				<label>
					<input type="checkbox" name="bpi_exclude_cat" id="bpi_exclude_cat" value="yes" <?php checked( $excluded, 'yes' ); ?>>
					Не повышать автоматически
				</label>
				<p class="description">Если отмечено, товары из этой категории не будут затронуты.</p>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save category field.
	 */
	public static function save_term_field( $term_id ) {
		if ( ! current_user_can( self::required_capability() ) ) {
			return;
		}

		if ( ! isset( $_POST['bpi_term_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bpi_term_nonce'] ) ), self::TERM_NONCE_ACTION ) ) {
			return;
		}

		$exclude = isset( $_POST['bpi_exclude_cat'] ) ? 'yes' : 'no';
		update_term_meta( $term_id, 'bpi_exclude', $exclude );
	}
}

BPI_Bulk_Price_Increase::init();

endif;
Install as a regular WordPress plugin, activate — a 'Bulk Price Update' item will appear in the WooCommerce menu.
The plugin works with all product types, including variable products.
In the 'Price change (%)' field, enter a number:
• Positive (e.g., 20) — increases the price by 20%.
• Negative (e.g., -15) — decreases the price by 15%.
• Allowed range: from -100% to +1000% (supports decimals).
Select product categories (leave empty for all) and click run.
Exclusions: set the 'Do not increase' checkbox on the product edit page or in category settings.
Safe one-click rollback of the last operation is available.
All changes apply instantly without reloading the page.
🎨

Custom header shortcode (v1)

Shortcode [custom_header] — responsive header with logo, menu, phone, city modal and request form.

<?php
/**
 * Custom Header Shortcode (v1) — обезличенная версия
 * 
 * Использование: [custom_header primary_color="#f58200" hover_color="#ff7a00" button_bg="#1A2B3C" button_hover_bg="#1A252F"]
 * 
 * Добавьте в functions.php.
 * 
 * Код полностью обезличен: все номера телефонов, email-адреса и ссылки
 * на изображения заменены на шаблонные значения.
 */

add_shortcode( 'custom_header', 'custom_header_shortcode' );

function custom_header_shortcode( $atts ) {
    // Параметры по умолчанию (кнопка теперь тёмно-синяя)
    $atts = shortcode_atts( array(
        'primary_color'     => '#f58200',
        'hover_color'       => '#ff7a00',
        'button_bg'         => '#512da8',
        'button_hover_bg'   => '#3e1f78',
    ), $atts );

    if ( empty( $atts['button_bg'] ) ) {
        $atts['button_bg'] = $atts['primary_color'];
    }

    // Подключаем стили
    if ( ! wp_style_is( 'custom-header-styles', 'enqueued' ) ) {
        wp_register_style( 'custom-header-styles', false );
        wp_enqueue_style( 'custom-header-styles' );

        $css = '
        /* Глобальное отключение горизонтального скролла */
        html, body {
            overflow-x: hidden !important;
        }

        .custom-header-block * {
            box-sizing: border-box;
        }
        .custom-header-block {
            font-family: "Roboto", sans-serif;
            font-weight: 400;
            color: #111;
            /* Принудительное растяжение на всю ширину экрана */
            width: 100vw !important;
            max-width: 100vw !important;
            margin-left: calc(-50vw + 50%) !important;
            position: relative;
            left: 0;
            right: 0;
            /* Делаем блок видимым после загрузки стилей */
            opacity: 1;
            transition: opacity 0.2s ease; /* Плавное появление (опционально) */
        }

        /* Скрываем пустые ссылки логотипа (если нет изображения) */
        .custom-header-block .desktop-logo:empty,
        .custom-header-block .mobile-header .logo:empty {
            display: none;
        }

        /* ========== ДЕСКТОПНАЯ ВЕРСИЯ (>1024px) ========== */
        .custom-header-block .desktop-header {
            max-width: 1400px;
            margin: 0 auto;
            display: flex;
            align-items: stretch;
            padding: 0 20px;
            min-height: 100px;
            position: relative;
        }
        .custom-header-block .desktop-logo {
            display: flex;
            align-items: center;
            text-decoration: none;
            color: #111;
            position: relative;
            overflow: hidden;
            cursor: pointer;
            height: auto;
            align-self: stretch;
            padding: 10px 0;
            border-radius: 8px;
        }
        .custom-header-block .desktop-logo img {
            display: block;
            max-height: 110px;
            width: auto;
            object-fit: contain;
            position: relative;
            z-index: 2;
        }
        .custom-header-block .desktop-logo.shine::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 50%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            animation: shine-animation 1.6s forwards;
            pointer-events: none;
            z-index: 3;
        }
        @keyframes shine-animation {
            0% { left: -150%; }
            100% { left: 100%; }
        }
        .custom-header-block .desktop-right {
            flex: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
            margin-left: 30px;
        }
        .custom-header-block .desktop-top-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 0;
            min-height: 70px;
        }
        .custom-header-block .desktop-location {
            display: flex;
            align-items: center;
            gap: 10px;
            color: var(--primary-color, #f58200);
            font-weight: 600;
            cursor: pointer;
            height: 100%;
            position: relative;
            overflow: hidden;
            margin-left: 20px;
            border-radius: 8px;
        }
        .custom-header-block .desktop-location .pin {
            width: 22px;
            height: 22px;
            opacity: 0.95;
            position: relative;
            top: -2px;
        }
        .custom-header-block .desktop-city {
            position: relative;
            font-size: 22px;
            line-height: 1;
            padding-bottom: 6px;
            display: flex;
            align-items: center;
            gap: 6px;
            font-weight: 400;
            color: currentColor;
        }
        .custom-header-block .desktop-city::after {
            content: "";
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            height: 2px;
            border-bottom: 2px dotted currentColor;
        }
        .custom-header-block .desktop-city .city-name {
            white-space: nowrap;
        }
        .custom-header-block .desktop-city .caret {
            width: 10px;
            height: 10px;
            display: inline-block;
            transform: translateY(2px);
        }
        .custom-header-block .desktop-location::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 45%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 3;
        }
        .custom-header-block .desktop-location:hover::after {
            left: 150%;
        }
        .custom-header-block .desktop-phone-block {
            text-align: right;
            display: flex;
            flex-direction: column;
            justify-content: center;
            height: 100%;
            position: relative;
            overflow: hidden;
            margin: 0 auto;
            border-radius: 8px;
        }
        .custom-header-block .desktop-phone-block .phone {
            display: block;
            color: var(--primary-color, #f58200);
            font-weight: 700;
            font-size: 30px;
            letter-spacing: 0.6px;
            text-decoration: none;
            white-space: nowrap;
            position: relative;
            z-index: 2;
            line-height: 1.2;
        }
        .custom-header-block .desktop-phone-block .hint {
            display: block;
            font-size: 14px;
            color: #333;
            margin-top: 0;
            line-height: 1.2;
            font-weight: 400;
            position: relative;
            z-index: 2;
        }
        .custom-header-block .desktop-phone-block::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 45%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
        }
        .custom-header-block .desktop-phone-block:hover::after {
            left: 120%;
        }
        .custom-header-block .desktop-phone-block .phone::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 45%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
            opacity: 0;
        }
        .custom-header-block .desktop-phone-block .phone:hover::after {
            left: 120%;
            opacity: 1;
        }
        .custom-header-block .desktop-btn {
            display: inline-flex;
            align-items: center;
            gap: 12px;
            background: var(--button-bg, #1A2B3C);
            color: #fff;
            padding: 12px 24px;
            border-radius: 8px;
            border: none;
            font-weight: 400;
            font-size: 22px;
            box-shadow: 0 4px 10px rgba(0,0,0,0.1);
            cursor: pointer;
            text-decoration: none;
            line-height: 1.2;
            height: fit-content;
            position: relative;
            overflow: hidden;
            transition: background-color 0.3s, box-shadow 0.3s;
        }
        .custom-header-block .desktop-btn:hover {
            background-color: var(--button-hover-bg, #1A252F);
            box-shadow: 0 6px 14px rgba(0,0,0,0.15);
        }
        .custom-header-block .desktop-btn::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 50%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
        }
        .custom-header-block .desktop-btn:hover::after {
            left: 120%;
        }
        .custom-header-block .desktop-btn .icon {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 36px;
            height: 36px;
        }
        .custom-header-block .desktop-btn svg {
            display: block;
            width: 32px;
            height: 32px;
        }
        .custom-header-block .desktop-hamburger {
            display: none;
        }
        .custom-header-block .desktop-nav-menu {
            display: flex;
            justify-content: space-between;
            list-style: none;
            margin: 0;
            padding: 0 0 0 20px;
            width: 100%;
        }
        .custom-header-block .desktop-nav-menu > li { position: relative; }
        .custom-header-block .desktop-nav-menu > li > a {
            display: block;
            padding: 16px 0;
            font-weight: 400;
            font-size: 22px;
            color: #f0f0f0;
            text-decoration: none;
            transition: color 0.2s;
            white-space: nowrap;
            position: relative;
            overflow: hidden;
            border-radius: 8px;
        }
        .custom-header-block .desktop-nav-menu > li:hover > a {
            color: #ffffff;
        }
        .custom-header-block .desktop-nav-menu > li > a::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 50%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
            opacity: 0;
        }
        .custom-header-block .desktop-nav-menu > li > a:hover::after {
            left: 120%;
            opacity: 1;
        }
        .custom-header-block .desktop-nav-menu .menu-separator,
        .custom-header-block .mobile-header .nav-menu .menu-separator {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 30px;
            color: #ccc;
            font-size: 22px;
        }
        .custom-header-block .desktop-nav-menu .menu-separator span,
        .custom-header-block .mobile-header .nav-menu .menu-separator span {
            display: block;
            line-height: 1;
        }
        @media (max-width: 720px) {
            .custom-header-block .mobile-header .nav-menu .menu-separator {
                display: none;
            }
        }

        .custom-header-block .desktop-dropdown {
            position: absolute;
            top: 100%;
            left: 0;
            background: #fff;
            border: 1px solid rgba(0,0,0,0.08);
            border-top: 2px solid var(--primary-color, #f58200);
            box-shadow: 0 10px 20px rgba(0,0,0,0.1);
            min-width: 240px;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s, visibility 0.2s;
            z-index: 100;
            padding: 10px 0;
        }
        .custom-header-block .desktop-nav-menu > li:hover > .desktop-dropdown {
            opacity: 1;
            visibility: visible;
        }
        .custom-header-block .desktop-dropdown li {
            list-style: none;
            position: relative;
        }
        .custom-header-block .desktop-dropdown li a {
            display: block;
            padding: 10px 20px;
            color: #111;
            text-decoration: none;
            font-size: 15px;
            transition: color 0.2s;
            white-space: nowrap;
        }
        .custom-header-block .desktop-dropdown li a:hover {
            color: var(--primary-color, #f58200);
            background: transparent;
        }
        .custom-header-block .desktop-grid-dropdown {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            width: 800px;
            padding: 20px;
        }
        .custom-header-block .desktop-grid-dropdown .col h4 {
            font-size: 16px;
            font-weight: 700;
            margin: 0 0 10px;
            color: var(--primary-color, #f58200);
            border-left: 3px solid var(--primary-color, #f58200);
            padding-left: 8px;
        }
        .custom-header-block .desktop-grid-dropdown .col ul {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .custom-header-block .desktop-grid-dropdown .col li { margin-bottom: 6px; }
        .custom-header-block .desktop-grid-dropdown .col a {
            padding: 4px 0;
            display: inline-block;
            color: #111;
            text-decoration: none;
            font-size: 14px;
            transition: color 0.2s;
        }
        .custom-header-block .desktop-grid-dropdown .col a:hover {
            color: var(--primary-color, #f58200);
            background: transparent;
        }
        .custom-header-block .desktop-bottom-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-top: 1px solid #e0e0e0;
            padding: 8px 0;
        }

        /* ========== ПЛАНШЕТНАЯ И МОБИЛЬНАЯ ВЕРСИЯ (≤1024px) ========== */
        .custom-header-block .mobile-header {
            display: none;
        }
        .custom-header-block .mobile-header .header-wrap {
            background: transparent;
        }
        .custom-header-block .mobile-header .header {
            max-width: 1400px;
            margin: 0 auto;
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 18px 20px;
            height: 100px;
        }
        .custom-header-block .mobile-header .logo {
            display: flex;
            align-items: center;
            text-decoration: none;
            color: #111;
            height: 100%;
            position: relative;
            overflow: hidden;
            cursor: pointer;
            border-radius: 8px;
        }
        .custom-header-block .mobile-header .logo img {
            display: block;
            max-height: 60px;
            width: auto;
            position: relative;
            z-index: 2;
        }
        .custom-header-block .mobile-header .logo.shine::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 50%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            animation: shine-animation 1.6s forwards;
            pointer-events: none;
            z-index: 3;
        }
        .custom-header-block .mobile-header .center {
            margin: 0;
            padding: 0;
            height: 100%;
        }
        .custom-header-block .mobile-header .location {
            display: flex;
            align-items: center;
            gap: 10px;
            color: var(--primary-color, #f58200);
            font-weight: 600;
            cursor: pointer;
            height: 100%;
            position: relative;
            overflow: hidden;
            border-radius: 8px;
        }
        .custom-header-block .mobile-header .location .pin {
            width: 22px;
            height: 22px;
            opacity: 0.95;
            position: relative;
            top: -2px;
        }
        .custom-header-block .mobile-header .city {
            position: relative;
            font-size: 22px;
            line-height: 1;
            padding-bottom: 6px;
            display: flex;
            align-items: center;
            gap: 6px;
            font-weight: 400;
            color: currentColor;
        }
        .custom-header-block .mobile-header .city::after {
            content: "";
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            height: 2px;
            border-bottom: 2px dotted currentColor;
        }
        .custom-header-block .mobile-header .city .city-name {
            white-space: nowrap;
        }
        .custom-header-block .mobile-header .city .caret {
            width: 10px;
            height: 10px;
            display: inline-block;
            transform: translateY(2px);
        }
        .custom-header-block .mobile-header .location::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 45%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 3;
        }
        .custom-header-block .mobile-header .location:hover::after {
            left: 150%;
        }
        .custom-header-block .mobile-header .phone-block {
            text-align: right;
            display: flex;
            flex-direction: column;
            justify-content: center;
            height: 100%;
            position: relative;
            overflow: hidden;
            border-radius: 8px;
        }
        .custom-header-block .mobile-header .phone-block .phone {
            display: block;
            color: var(--primary-color, #f58200);
            font-weight: 700;
            font-size: 30px;
            letter-spacing: 0.6px;
            text-decoration: none;
            white-space: nowrap;
            position: relative;
            z-index: 2;
        }
        .custom-header-block .mobile-header .phone-block .hint {
            display: block;
            font-size: 14px;
            color: #333;
            margin-top: 0;
            line-height: 1.2;
            font-weight: 400;
            position: relative;
            z-index: 2;
        }
        .custom-header-block .mobile-header .phone-block::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 45%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
        }
        .custom-header-block .mobile-header .phone-block:hover::after {
            left: 120%;
        }
        .custom-header-block .mobile-header .phone-block .phone::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 45%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
            opacity: 0;
        }
        .custom-header-block .mobile-header .phone-block .phone:hover::after {
            left: 120%;
            opacity: 1;
        }
        .custom-header-block .mobile-header .btn {
            display: inline-flex;
            align-items: center;
            gap: 12px;
            background: var(--button-bg, #1A2B3C);
            color: #fff;
            padding: 12px 24px;
            border-radius: 8px;
            border: none;
            font-weight: 400;
            font-size: 22px;
            box-shadow: 0 4px 10px rgba(0,0,0,0.1);
            cursor: pointer;
            text-decoration: none;
            line-height: 1.2;
            height: fit-content;
            position: relative;
            overflow: hidden;
            transition: background-color 0.3s, box-shadow 0.3s;
        }
        .custom-header-block .mobile-header .btn:hover {
            background-color: var(--button-hover-bg, #1A252F);
            box-shadow: 0 6px 14px rgba(0,0,0,0.15);
        }
        .custom-header-block .mobile-header .btn::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 50%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
        }
        .custom-header-block .mobile-header .btn:hover::after {
            left: 120%;
        }
        .custom-header-block .mobile-header .btn .icon {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 36px;
            height: 36px;
        }
        .custom-header-block .mobile-header .btn svg {
            display: block;
            width: 32px;
            height: 32px;
        }
        .custom-header-block .mobile-header .hamburger {
            display: none;
            background: none;
            border: none;
            width: 40px;
            height: 40px;
            position: relative;
            cursor: pointer;
            padding: 0;
            margin-left: 10px;
            outline: none;
            box-shadow: none;
            flex-shrink: 0;
        }
        .custom-header-block .mobile-header .hamburger span {
            display: block;
            position: absolute;
            height: 3px;
            width: 100%;
            background: #111;
            border-radius: 3px;
            left: 0;
            transform: rotate(0deg);
            transition: 0.25s ease-in-out;
        }
        .custom-header-block .mobile-header .hamburger span:nth-child(1) { top: 8px; }
        .custom-header-block .mobile-header .hamburger span:nth-child(2) { top: 18px; }
        .custom-header-block .mobile-header .hamburger span:nth-child(3) { top: 28px; }
        .custom-header-block .mobile-header .hamburger.active span:nth-child(1) {
            top: 18px;
            transform: rotate(135deg);
        }
        .custom-header-block .mobile-header .hamburger.active span:nth-child(2) {
            opacity: 0;
            left: -60px;
        }
        .custom-header-block .mobile-header .hamburger.active span:nth-child(3) {
            top: 18px;
            transform: rotate(-135deg);
        }
        .custom-header-block .mobile-header .nav-wrap {
            background: transparent;
            position: relative;
            display: flex;
            justify-content: center;
        }
        .custom-header-block .mobile-header .nav-wrap::before {
            content: "";
            position: absolute;
            top: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 1400px;
            max-width: 100%;
            height: 1px;
            background-color: rgba(0,0,0,0.08);
            pointer-events: none;
        }
        .custom-header-block .mobile-header .nav-container {
            width: 100%;
            max-width: 1400px;
            padding: 0 20px;
            background: transparent;
            position: relative;
        }
        .custom-header-block .mobile-header .nav-menu {
            display: flex;
            justify-content: space-between;
            list-style: none;
            margin: 0;
            padding: 0;
        }
        .custom-header-block .mobile-header .nav-menu > li { position: relative; }
        .custom-header-block .mobile-header .nav-menu > li > a {
            display: block;
            padding: 16px 0;
            font-weight: 400;
            font-size: 22px;
            color: #f0f0f0;
            text-decoration: none;
            transition: color 0.2s;
            white-space: nowrap;
            position: relative;
            overflow: hidden;
        }
        .custom-header-block .mobile-header .nav-menu > li:hover > a {
            color: #ffffff;
        }
        .custom-header-block .mobile-header .nav-menu > li > a::after {
            content: "";
            position: absolute;
            top: -50%;
            left: -60%;
            width: 50%;
            height: 200%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            transition: left 1s;
            pointer-events: none;
            z-index: 1;
            opacity: 0;
        }
        .custom-header-block .mobile-header .nav-menu > li > a:hover::after {
            left: 120%;
            opacity: 1;
        }
        .custom-header-block .mobile-header .dropdown {
            position: absolute;
            top: 100%;
            left: 0;
            background: #fff;
            border: 1px solid rgba(0,0,0,0.08);
            border-top: 2px solid var(--primary-color, #f58200);
            box-shadow: 0 10px 20px rgba(0,0,0,0.1);
            min-width: 240px;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s, visibility 0.2s;
            z-index: 100;
            padding: 10px 0;
        }
        .custom-header-block .mobile-header .nav-menu > li:hover > .dropdown {
            opacity: 1;
            visibility: visible;
        }
        .custom-header-block .mobile-header .dropdown li {
            list-style: none;
            position: relative;
        }
        .custom-header-block .mobile-header .dropdown li a {
            display: block;
            padding: 10px 20px;
            color: #111;
            text-decoration: none;
            font-size: 15px;
            transition: color 0.2s;
            white-space: nowrap;
        }
        .custom-header-block .mobile-header .dropdown li a:hover {
            color: var(--primary-color, #f58200);
            background: transparent;
        }
        .custom-header-block .mobile-header .grid-dropdown {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 20px;
            width: 1000px;
            padding: 20px;
        }
        .custom-header-block .mobile-header .grid-dropdown .col h4 {
            font-size: 16px;
            font-weight: 700;
            margin: 0 0 10px;
            color: var(--primary-color, #f58200);
            border-left: 3px solid var(--primary-color, #f58200);
            padding-left: 8px;
        }
        .custom-header-block .mobile-header .grid-dropdown .col ul {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .custom-header-block .mobile-header .grid-dropdown .col li { margin-bottom: 6px; }
        .custom-header-block .mobile-header .grid-dropdown .col a {
            padding: 4px 0;
            display: inline-block;
            color: #111;
            text-decoration: none;
            font-size: 14px;
            transition: color 0.2s;
        }
        .custom-header-block .mobile-header .grid-dropdown .col a:hover {
            color: var(--primary-color, #f58200);
            background: transparent;
        }
        .custom-header-block .mobile-header .mobile-city { display: none; }

        /* Планшеты (max-width: 1024px) */
        @media (max-width: 1180px) {
            .custom-header-block .desktop-header {
                display: none;
            }
            .custom-header-block .mobile-header {
                display: block;
            }
            .custom-header-block .mobile-header .header {
                padding: 15px 20px;
                height: 90px;
            }
            .custom-header-block .mobile-header .logo img { max-height: 50px; }
            .custom-header-block .mobile-header .location .pin { width: 18px; height: 18px; }
            .custom-header-block .mobile-header .city { font-size: 18px; }
            .custom-header-block .mobile-header .phone-block .phone { font-size: 24px; }
            .custom-header-block .mobile-header .phone-block .hint { font-size: 12px; }
            .custom-header-block .mobile-header .btn { font-size: 18px; padding: 8px 18px; }
            .custom-header-block .mobile-header .btn .icon { width: 30px; height: 30px; }
            .custom-header-block .mobile-header .btn svg { width: 26px; height: 26px; }
            .custom-header-block .mobile-header .nav-menu > li > a { font-size: 20px; padding: 14px 0; }
        }

        @media (max-width: 1024px) {
            .custom-header-block .desktop-header {
                display: none;
            }
            .custom-header-block .mobile-header {
                display: block;
            }
            .custom-header-block .mobile-header .header {
                padding: 15px 20px;
                height: 90px;
            }
            .custom-header-block .mobile-header .logo img { max-height: 50px; }
            .custom-header-block .mobile-header .location .pin { width: 18px; height: 18px; }
            .custom-header-block .mobile-header .city { font-size: 18px; }
            .custom-header-block .mobile-header .phone-block .phone { font-size: 24px; }
            .custom-header-block .mobile-header .phone-block .hint { font-size: 12px; }
            .custom-header-block .mobile-header .btn { font-size: 18px; padding: 8px 18px; }
            .custom-header-block .mobile-header .btn .icon { width: 30px; height: 30px; }
            .custom-header-block .mobile-header .btn svg { width: 26px; height: 26px; }
            .custom-header-block .mobile-header .nav-menu > li > a { font-size: 14px; padding: 14px 0; }
            .custom-header-block .mobile-header .grid-dropdown { width: 800px; gap: 15px; }
            .custom-header-block .mobile-header .grid-dropdown .col h4 { font-size: 14px; }
            .custom-header-block .mobile-header .grid-dropdown .col a { font-size: 12px; }
            .custom-header-block .mobile-header .dropdown li a { font-size: 13px; }
        }

        /* Плавное уменьшение размеров при сужении от 1024px до 720px */
        @media (max-width: 920px) {
            .custom-header-block .mobile-header .phone-block .phone { font-size: 22px; }
            .custom-header-block .mobile-header .phone-block .hint { font-size: 11px; }
            .custom-header-block .mobile-header .btn { font-size: 16px; padding: 7px 14px; }
            .custom-header-block .mobile-header .btn .icon { width: 26px; height: 26px; }
            .custom-header-block .mobile-header .btn svg { width: 22px; height: 22px; }
        }
        @media (max-width: 820px) {
            .custom-header-block .mobile-header .phone-block .phone { font-size: 18px; }
            .custom-header-block .mobile-header .phone-block .hint { font-size: 10px; }
            .custom-header-block .mobile-header .btn { font-size: 14px; padding: 7px 10px; }
            .custom-header-block .mobile-header .btn .icon { width: 22px; height: 22px; }
            .custom-header-block .mobile-header .btn svg { width: 18px; height: 18px; }
        }

        /* Мобильные (max-width: 720px) */
        @media (max-width: 720px) {
            .custom-header-block .mobile-header .center,
            .custom-header-block .mobile-header .btn,
            .custom-header-block .mobile-header .nav-wrap::before { display: none; }
            .custom-header-block .mobile-header .hamburger { display: block; }
            .custom-header-block .mobile-header .header {
                padding: 12px 15px;
                height: 80px;
            }
            .custom-header-block .mobile-header .logo { flex-shrink: 0; }
            .custom-header-block .mobile-header .logo img { max-height: 40px; }
            .custom-header-block .mobile-header .phone-block {
                flex: 0 1 auto;
                display: flex;
                flex-direction: column;
                justify-content: center;
                align-items: flex-end;
                margin: 0 5px;
                min-width: 0;
                height: 100%;
            }
            .custom-header-block .mobile-header .phone-wrapper {
                display: flex;
                flex-direction: column;
                align-items: flex-end;
                width: 100%;
            }
            .custom-header-block .mobile-header .phone-block .phone {
                font-size: 18px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .custom-header-block .mobile-header .phone-block .hint {
                font-size: 10px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .custom-header-block .mobile-header .hamburger {
                margin-left: 5px;
                flex-shrink: 0;
            }
            .custom-header-block .mobile-header .nav-menu {
                display: none;
                flex-direction: column;
                align-items: stretch;
                background: #fff;
                position: absolute;
                left: 0;
                width: 100%;
                padding: 10px 20px 20px;
                z-index: 999;
            }
            .custom-header-block .mobile-header .nav-menu.active { display: flex; }
            .custom-header-block .mobile-header .nav-menu > li > a {
                padding: 12px 0;
                font-size: 18px;
                border-bottom: 1px solid #f0f0f0;
                position: relative;
                color: #111;
            }
            .custom-header-block .mobile-header .nav-menu > li > a:hover {
                color: var(--primary-color, #f58200);
            }
            .custom-header-block .mobile-header .nav-menu .dropdown {
                display: none;
                position: static;
                opacity: 1;
                visibility: visible;
                box-shadow: none;
                border: none;
                padding-left: 20px;
                background: transparent;
                border-top: none;
                margin: 0;
            }
            .custom-header-block .mobile-header .nav-menu li.open > .dropdown { display: block; }
            .custom-header-block .mobile-header .grid-dropdown {
                grid-template-columns: 1fr;
                width: auto;
                padding: 10px 0 10px 20px;
                background: transparent;
            }
            .custom-header-block .mobile-header .mobile-city { display: block; }
            .custom-header-block .mobile-header .mobile-city a { color: var(--primary-color, #f58200); font-weight: 500; }
            .custom-header-block .mobile-header .nav-menu > li.menu-item-has-children > a::after {
                content: "▼";
                float: right;
                font-size: 14px;
                transition: transform 0.2s;
            }
            .custom-header-block .mobile-header .nav-menu > li.menu-item-has-children.open > a::after {
                transform: rotate(180deg);
            }
        }

        /* Очень маленькие экраны (≤360px) */
        @media (max-width: 360px) {
            .custom-header-block .mobile-header .phone-block .phone { font-size: 16px; }
            .custom-header-block .mobile-header .phone-block .hint { font-size: 8px; }
        }

        /* Модальное окно выбора города */
        .custom-header-block .modal {
            display: none;
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(0,0,0,0.5);
            align-items: center;
            justify-content: center;
            z-index: 1000;
        }
        .custom-header-block .modal.active { display: flex; }
        .custom-header-block .modal__content {
            background: white;
            border-radius: 12px;
            max-width: 800px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            padding: 24px 30px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.2);
            position: relative;
        }
        .custom-header-block .modal__close {
            position: absolute;
            top: 20px;
            right: 24px;
            font-size: 28px;
            line-height: 1;
            cursor: pointer;
            color: #aaa;
            background: none;
            border: none;
            padding: 0;
        }
        .custom-header-block .modal__close:hover { color: var(--primary-color, #f58200); }
        .custom-header-block .modal h2 {
            font-size: 26px;
            font-weight: 700;
            margin: 0 0 16px;
            color: #111;
            border-bottom: 2px solid var(--primary-color, #f58200);
            padding-bottom: 8px;
        }
        .custom-header-block .city-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 16px 24px;
        }
        .custom-header-block .city-group {
            break-inside: avoid;
            margin-bottom: 16px;
        }
        .custom-header-block .city-group h3 {
            font-size: 18px;
            font-weight: 500;
            margin: 0 0 8px;
            color: var(--primary-color, #f58200);
            border-left: 3px solid var(--primary-color, #f58200);
            padding-left: 8px;
        }
        .custom-header-block .city-list {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .custom-header-block .city-list li { margin-bottom: 6px; }
        .custom-header-block .city-list button {
            background: none;
            border: none;
            color: #111;
            font-size: 16px;
            font-weight: 400;
            text-align: left;
            cursor: pointer;
            padding: 4px 8px;
            width: 100%;
            border-radius: 4px;
            transition: color 0.2s;
            font-family: "Roboto", sans-serif;
        }
        .custom-header-block .city-list button:hover {
            color: var(--primary-color, #f58200);
            background: transparent;
            font-weight: 400;
        }
        .custom-header-block .city-list .note {
            font-size: 13px;
            color: #888;
            margin-left: 8px;
        }
        @media (max-width: 720px) {
            .custom-header-block .modal .city-grid {
                grid-template-columns: repeat(2, 1fr);
                gap: 12px 16px;
            }
        }
        @media (max-width: 480px) {
            .custom-header-block .modal .city-grid {
                grid-template-columns: 1fr;
            }
        }

        /* Модальное окно для бесплатного замера */
        .custom-free-measure-modal {
            display: none;
            position: fixed;
            z-index: 1001;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            align-items: center;
            justify-content: center;
        }
        .custom-free-measure-modal.active { display: flex; }
        .custom-free-measure-content {
            background: white;
            padding: 32px;
            border-radius: 8px;
            max-width: 500px;
            width: 90%;
            max-height: 90vh;
            overflow-y: auto;
            box-shadow: 0 20px 40px rgba(0,0,0,0.2);
            position: relative;
            font-size: 16px;
        }
        .custom-free-measure-content h3 {
            margin-top: 0;
            margin-bottom: 20px;
            font-size: 24px;
            padding-right: 20px;
        }
        .custom-free-measure-close {
            position: absolute;
            top: 20px;
            right: 20px;
            font-size: 24px;
            cursor: pointer;
            color: #94a3b8;
            transition: 0.2s;
            background: none;
            border: none;
            line-height: 1;
        }
        .custom-free-measure-close:hover { color: #0f172a; }
        .custom-free-measure-form-group { margin-bottom: 20px; }
        .custom-free-measure-form-group label {
            display: block;
            font-weight: 500;
            margin-bottom: 6px;
            color: #334155;
        }
        .custom-free-measure-form-group input,
        .custom-free-measure-form-group textarea {
            width: 100%;
            padding: 8px 16px;
            border: 1px solid #cbd5e1;
            border-radius: 8px;
            font-size: 16px;
            line-height: 1.5;
            outline: none;
            transition: 0.2s;
        }
        .custom-free-measure-form-group input:focus,
        .custom-free-measure-form-group textarea:focus {
            border-color: #f59e0b;
            box-shadow: 0 0 0 3px rgba(245,158,11,0.2);
        }
        .custom-free-measure-form-group textarea {
            resize: vertical;
            min-height: 80px;
        }
        .required::after { content: " *"; color: #ef4444; }
        .custom-free-measure-checkbox-group {
            display: flex;
            align-items: center;
            gap: 10px;
            margin: 20px 0;
        }
        .custom-free-measure-checkbox-group input[type="checkbox"] {
            width: 18px;
            height: 18px;
            cursor: pointer;
        }
        .custom-free-measure-checkbox-group label {
            font-size: 14px;
            color: #1e293b;
            cursor: pointer;
        }
        .custom-free-measure-error-message {
            color: #ef4444;
            font-size: 14px;
            margin-top: 4px;
        }
        .custom-free-measure-modal-actions {
            display: flex;
            gap: 16px;
            margin-top: 24px;
        }
        .custom-free-measure-btn-primary {
            background: #f59e0b;
            color: white;
            border: none;
            padding: 14px 28px;
            border-radius: 8px;
            font-weight: 600;
            font-size: 16px;
            cursor: pointer;
            transition: 0.2s;
            flex: 1;
        }
        .custom-free-measure-btn-primary:hover { background: #d97706; }
        .custom-free-measure-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
        .custom-free-measure-btn-secondary {
            background: #e2e8f0;
            color: #1e293b;
            border: none;
            padding: 14px 28px;
            border-radius: 8px;
            font-weight: 600;
            font-size: 16px;
            cursor: pointer;
            transition: 0.2s;
            flex: 1;
        }
        .custom-free-measure-btn-secondary:hover { background: #cbd5e1; }

        /* Весь текст шапки белый (кроме выпадающих меню и модалок) */
        .custom-header-block .desktop-logo,
        .custom-header-block .desktop-location,
        .custom-header-block .desktop-city,
        .custom-header-block .desktop-phone-block .phone,
        .custom-header-block .desktop-phone-block .hint,
        .custom-header-block .mobile-header .logo,
        .custom-header-block .mobile-header .location,
        .custom-header-block .mobile-header .city,
        .custom-header-block .mobile-header .phone-block .phone,
        .custom-header-block .mobile-header .phone-block .hint {
            color: #ffffff !important;
        }

        .custom-header-block .desktop-location:hover .desktop-city,
        .custom-header-block .mobile-header .location:hover .city {
            color: #ffffff !important;
        }

        .custom-header-block .desktop-phone-block .hint,
        .custom-header-block .mobile-header .phone-block .hint {
            color: #e0e0e0 !important;
        }

        .custom-header-block .mobile-header .hamburger span {
            background: #ffffff !important;
        }

        .custom-header-block .desktop-header svg,
        .custom-header-block .mobile-header svg {
            filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3));
        }

        .custom-header-block .desktop-dropdown,
        .custom-header-block .mobile-header .dropdown {
            box-shadow: 0 15px 30px rgba(0,0,0,0.2);
        }
        ';

        wp_add_inline_style( 'custom-header-styles', $css );
    }

    if ( ! wp_script_is( 'custom-header-script', 'enqueued' ) ) {
        wp_register_script( 'custom-header-script', false, array(), false, true );
        wp_enqueue_script( 'custom-header-script' );

        $js = '
        document.addEventListener("DOMContentLoaded", function() {
            const mobileHeader = document.querySelector(".mobile-header");
            const hamburger = mobileHeader ? mobileHeader.querySelector(".hamburger") : null;
            const navMenu = mobileHeader ? mobileHeader.querySelector(".nav-menu") : null;

            const desktopLocationBtn = document.getElementById("desktopLocationBtn") || document.getElementById("locationButton");
            const mobileLocationBtn = document.getElementById("mobileLocationBtn");
            const mobileCityTrigger = document.getElementById("mobileCityTrigger");
            const cityModal = document.getElementById("cityModal");
            const cityCloseBtn = document.getElementById("modalClose");
            const cityNameElements = document.querySelectorAll(".city-name");
            const cityButtons = document.querySelectorAll(".custom-header-block .city-list button[data-city]");

            function openCityModal() {
                if (cityModal) {
                    cityModal.classList.add("active");
                    document.body.style.overflow = "hidden";
                }
            }
            function closeCityModal() {
                if (cityModal) {
                    cityModal.classList.remove("active");
                    document.body.style.overflow = "";
                }
            }
            function selectCity(city) {
                cityNameElements.forEach(el => el.textContent = city);
                localStorage.setItem("selectedCity", city);
                window.selectedCity = city;
                closeCityModal();
                document.dispatchEvent(new CustomEvent("cityChanged", { detail: { city: city } }));
            }

            if (desktopLocationBtn) desktopLocationBtn.addEventListener("click", openCityModal);
            if (mobileLocationBtn) mobileLocationBtn.addEventListener("click", openCityModal);
            if (mobileCityTrigger) {
                mobileCityTrigger.addEventListener("click", function(e) {
                    e.preventDefault();
                    openCityModal();
                    if (navMenu) navMenu.classList.remove("active");
                    if (hamburger) hamburger.classList.remove("active");
                });
            }
            if (cityCloseBtn) cityCloseBtn.addEventListener("click", closeCityModal);
            if (cityModal) {
                cityModal.addEventListener("click", function(e) {
                    if (e.target === cityModal) closeCityModal();
                });
            }
            cityButtons.forEach(btn => {
                btn.addEventListener("click", function() { selectCity(this.dataset.city); });
            });

            const measureModal = document.getElementById("customFreeMeasureModal");
            const measureModalClose = document.getElementById("customFreeMeasureModalClose");
            const measureCloseBtn = document.getElementById("customFreeMeasureClose");
            const measureBtns = document.querySelectorAll(".desktop-btn, .btn");

            function openMeasureModal() {
                if (measureModal) {
                    measureModal.classList.add("active");
                    document.body.style.overflow = "hidden";
                    const nameInput = document.getElementById("customFreeMeasureName");
                    const phoneInput = document.getElementById("customFreeMeasurePhone");
                    const commentInput = document.getElementById("customFreeMeasureComment");
                    const consentCheck = document.getElementById("customFreeMeasureConsent");
                    const phoneError = document.getElementById("customFreeMeasurePhoneError");
                    const consentError = document.getElementById("customFreeMeasureConsentError");
                    if (nameInput) nameInput.value = "";
                    if (phoneInput) phoneInput.value = "";
                    if (commentInput) commentInput.value = "";
                    if (consentCheck) consentCheck.checked = false;
                    if (phoneError) phoneError.textContent = "";
                    if (consentError) consentError.textContent = "";
                }
            }
            function closeMeasureModal() {
                if (measureModal) {
                    measureModal.classList.remove("active");
                    document.body.style.overflow = "";
                }
            }

            measureBtns.forEach(btn => {
                btn.addEventListener("click", function(e) {
                    e.preventDefault();
                    openMeasureModal();
                });
            });

            if (measureModalClose) measureModalClose.addEventListener("click", closeMeasureModal);
            if (measureCloseBtn) measureCloseBtn.addEventListener("click", closeMeasureModal);
            if (measureModal) {
                measureModal.addEventListener("click", function(e) {
                    if (e.target === measureModal) closeMeasureModal();
                });
            }

            document.addEventListener("keydown", function(e) {
                if (e.key === "Escape") {
                    if (cityModal && cityModal.classList.contains("active")) closeCityModal();
                    if (measureModal && measureModal.classList.contains("active")) closeMeasureModal();
                }
            });

            (function initCity() {
                const savedCity = localStorage.getItem("selectedCity");
                const defaultCity = "Минская область";
                window.selectedCity = savedCity || defaultCity;
                if (savedCity) {
                    cityNameElements.forEach(el => el.textContent = savedCity);
                } else {
                    cityNameElements.forEach(el => el.textContent = defaultCity);
                }
            })();

            if (hamburger && navMenu) {
                hamburger.addEventListener("click", function() {
                    navMenu.classList.toggle("active");
                    hamburger.classList.toggle("active");
                });
            }

            document.querySelectorAll(".mobile-header .nav-menu > li.menu-item-has-children > a").forEach(link => {
                link.addEventListener("click", function(e) {
                    if (window.innerWidth <= 720) {
                        e.preventDefault();
                        const parentLi = link.parentElement;
                        parentLi.classList.toggle("open");
                    }
                });
            });

            if (navMenu) {
                navMenu.querySelectorAll("a").forEach(link => {
                    link.addEventListener("click", function() {
                        if (window.innerWidth <= 720 && !this.parentElement.classList.contains("menu-item-has-children")) {
                            navMenu.classList.remove("active");
                            if (hamburger) hamburger.classList.remove("active");
                        }
                    });
                });
            }

            const logos = document.querySelectorAll(".desktop-logo, .mobile-header .logo");
            logos.forEach(logo => {
                if (logo) {
                    setTimeout(() => {
                        logo.classList.add("shine");
                        setTimeout(() => logo.classList.remove("shine"), 1600);
                    }, 500);
                    logo.addEventListener("mouseenter", () => {
                        logo.classList.add("shine");
                        setTimeout(() => logo.classList.remove("shine"), 1600);
                    });
                }
            });

            const measureForm = document.getElementById("customFreeMeasureForm");
            const measureName = document.getElementById("customFreeMeasureName");
            const measurePhone = document.getElementById("customFreeMeasurePhone");
            const measureComment = document.getElementById("customFreeMeasureComment");
            const measureConsent = document.getElementById("customFreeMeasureConsent");
            const measurePhoneError = document.getElementById("customFreeMeasurePhoneError");
            const measureConsentError = document.getElementById("customFreeMeasureConsentError");

            function validatePhone(phone) { return phone.replace(/\D/g, "").length >= 10; }

            if (measureForm) {
                measureForm.addEventListener("submit", async function(e) {
                    e.preventDefault();

                    if (measurePhoneError) measurePhoneError.textContent = "";
                    if (measureConsentError) measureConsentError.textContent = "";

                    const name = measureName ? measureName.value.trim() : "";
                    const phone = measurePhone ? measurePhone.value.trim() : "";
                    const comment = measureComment ? measureComment.value.trim() : "";
                    const consent = measureConsent ? measureConsent.checked : false;

                    if (!name) { alert("Введите имя"); return; }
                    if (!phone) { alert("Введите телефон"); return; }
                    if (!validatePhone(phone)) {
                        if (measurePhoneError) measurePhoneError.textContent = "Введите корректный номер (не менее 10 цифр)";
                        return;
                    }
                    if (!consent) {
                        if (measureConsentError) measureConsentError.textContent = "Необходимо дать согласие на обработку персональных данных";
                        return;
                    }

                    const formData = new FormData();
                    formData.append("name", name);
                    formData.append("phone", phone);
                    if (comment) formData.append("comment", comment);
                    formData.append("_subject", "Заявка на бесплатный замер");
                    formData.append("_captcha", "false");

                    const submitBtn = document.getElementById("customFreeMeasureSubmit");
                    if (submitBtn) {
                        submitBtn.disabled = true;
                        submitBtn.textContent = "Отправка...";
                    }

                    try {
                        const response = await fetch("https://formsubmit.co/ajax/email@example.com", {
                            method: "POST",
                            body: formData
                        });

                        if (response.ok) {
                            alert("Заявка отправлена! Скоро мы свяжемся с вами.");
                            closeMeasureModal();
                        } else {
                            alert("Ошибка при отправке. Попробуйте позже.");
                        }
                    } catch (error) {
                        alert("Ошибка сети. Не удалось отправить.");
                        console.error(error);
                    } finally {
                        if (submitBtn) {
                            submitBtn.disabled = false;
                            submitBtn.textContent = "Отправить";
                        }
                    }
                });
            }
        });
        ';

        wp_add_inline_script( 'custom-header-script', $js );
    }

    ob_start();

    // Логотип
    $custom_logo_id = get_theme_mod( 'custom_logo' );
    $logo_html = '';
    if ( $custom_logo_id ) {
        $logo_url = wp_get_attachment_image_url( $custom_logo_id, 'full' );
        if ( $logo_url ) {
            $logo_html = '<img src="' . esc_url( $logo_url ) . '" alt="' . get_bloginfo( 'name' ) . '">';
        }
    }

    $inline_style = sprintf(
        'style="--primary-color: %s; --hover-color: %s; --button-bg: %s; --button-hover-bg: %s;"',
        esc_attr( $atts['primary_color'] ),
        esc_attr( $atts['hover_color'] ),
        esc_attr( $atts['button_bg'] ),
        esc_attr( $atts['button_hover_bg'] )
    );
    ?>

    <style>
        .custom-header-block { opacity: 0; }
    </style>

    <div class="custom-header-block" <?php echo $inline_style; ?>>
        <!-- Десктопная версия -->
        <div class="desktop-header">
            <a class="desktop-logo" href="<?php echo esc_url( home_url( '/' ) ); ?>">
                <?php echo $logo_html; ?>
            </a>
            <div class="desktop-right">
                <div class="desktop-top-row">
                    <div class="desktop-location" id="desktopLocationBtn">
                        <svg class="pin" viewBox="0 0 24 24">
                            <path d="M12 2C8.686 2 6 4.686 6 8c0 4.5 6 12 6 12s6-7.5 6-12c0-3.314-2.686-6-6-6z" fill="none" stroke="currentColor" stroke-width="1.4"/>
                            <circle cx="12" cy="8" r="2" fill="currentColor"/>
                        </svg>
                        <div class="desktop-city">
                            <span class="city-name">Минская область</span>
                            <svg class="caret" viewBox="0 0 10 10">
                                <path d="M1 3.2 L5 7 L9 3.2" fill="none" stroke="currentColor" stroke-width="1.2"/>
                            </svg>
                        </div>
                    </div>
                    <div class="desktop-phone-block">
                        <a class="phone" href="tel:+375XXXXXXXXX">+375 (XX) XXX-XX-XX</a>
                        <span class="hint">ежедневно с 09.00 до 20.00</span>
                    </div>
                    <a class="desktop-btn" href="#">
                        <span class="icon">
                            <img src="/wp-content/uploads/placeholder.avif" alt="" width="32" height="32" style="display:block; width:100%; height:auto;">
                        </span>
                        <span>Бесплатный замер</span>
                    </a>
                    <button class="desktop-hamburger">
                        <span></span><span></span><span></span>
                    </button>
                </div>
                <div class="desktop-bottom-row">
                    <ul class="desktop-nav-menu">
                        <li class="menu-item-has-children">
                            <a href="/natyazhnye-potolki/">Натяжные потолки</a>
                            <div class="desktop-dropdown desktop-grid-dropdown">
                                <div class="col">
                                    <h4>По фактуре</h4>
                                    <ul>
                                        <li><a href="/matovye/">Матовые</a></li>
                                        <li><a href="/glyanczevye/">Глянцевые</a></li>
                                        <li><a href="/satinovye/">Сатиновые</a></li>
                                        <li><a href="/fakturnye/">Фактурные</a></li>
                                        <li><a href="/tkanevye/">Тканевые</a></li>
                                    </ul>
                                </div>
                                <div class="col">
                                    <h4>По технологии</h4>
                                    <ul>
                                        <li><a href="/belye-potolki/">Белые потолки</a></li>
                                        <li><a href="/czvetnye/">Цветные</a></li>
                                        <li><a href="/paryashhie/">Парящие</a></li>
                                        <li><a href="/s-fotopechatyu/">С фотопечатью</a></li>
                                        <li><a href="/konturnye/">Контурные</a></li>
                                        <li><a href="/krivolinejnaya-spajka/">Криволинейная спайка</a></li>
                                        <li><a href="/s-podsvetkoj/">С подсветкой</a></li>
                                        <li><a href="/mnogourovnevye/">Многоуровневые</a></li>
                                        <li><a href="/tenevye/">Теневые</a></li>
                                        <li><a href="/potolok-kosmos/">Потолок космос</a></li>
                                        <li><a href="/reznoj/">Резной</a></li>
                                        <li><a href="/svetovye-linii/">Световые линии</a></li>
                                        <li><a href="/trekovye-svetilniki/">Трековые светильники</a></li>
                                    </ul>
                                </div>
                                <div class="col">
                                    <h4>По комнатам</h4>
                                    <ul>
                                        <li><a href="/dlya-gostinoj/">Для гостиной</a></li>
                                        <li><a href="/dlya-zala/">Для зала</a></li>
                                        <li><a href="/dlya-spalni/">Для спальни</a></li>
                                        <li><a href="/dlya-detskoj/">Для детской</a></li>
                                        <li><a href="/dlya-koridora/">Для коридора</a></li>
                                        <li><a href="/dlya-kuhni/">Для кухни</a></li>
                                        <li><a href="/dlya-vannoj/">Для ванной</a></li>
                                        <li><a href="/v-bassejn/">В бассейн</a></li>
                                    </ul>
                                </div>
                            </div>
                        </li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/sovety/">Советы</a></li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/nashi-raboty/">Наши работы</a></li>
                        <li class="menu-separator"><span>|</span></li>
                        <li class="menu-item-has-children">
                            <a href="/pokupatelyam/">Покупателям</a>
                            <ul class="desktop-dropdown">
                                <li><a href="/garantii-i-sertifikaty/">Гарантии и сертификаты</a></li>
                                <li><a href="/otzyvy/">Отзывы</a></li>
                            </ul>
                        </li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/kontakty/">Контакты</a></li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- Мобильная/планшетная версия -->
        <div class="mobile-header">
            <header class="header-wrap">
                <div class="header">
                    <a class="logo" href="<?php echo esc_url( home_url( '/' ) ); ?>">
                        <?php echo $logo_html; ?>
                    </a>
                    <div class="center">
                        <div class="location" id="mobileLocationBtn">
                            <svg class="pin" viewBox="0 0 24 24">
                                <path d="M12 2C8.686 2 6 4.686 6 8c0 4.5 6 12 6 12s6-7.5 6-12c0-3.314-2.686-6-6-6z" fill="none" stroke="currentColor" stroke-width="1.4"/>
                                <circle cx="12" cy="8" r="2" fill="currentColor"/>
                            </svg>
                            <div class="city">
                                <span class="city-name">Минская область</span>
                                <svg class="caret" viewBox="0 0 10 10">
                                    <path d="M1 3.2 L5 7 L9 3.2" fill="none" stroke="currentColor" stroke-width="1.2"/>
                                </svg>
                            </div>
                        </div>
                    </div>
                    <div class="phone-block">
                        <div class="phone-wrapper">
                            <a class="phone" href="tel:+375XXXXXXXXX">+375 (XX) XXX-XX-XX</a>
                            <span class="hint">ежедневно с 09.00 до 20.00</span>
                        </div>
                    </div>
                    <a class="btn" href="#">
                        <span class="icon">
                            <img src="/wp-content/uploads/placeholder.avif" alt="" width="32" height="32" style="display:block; width:100%; height:auto;">
                        </span>
                        <span>Бесплатный замер</span>
                    </a>
                    <button class="hamburger" id="hamburger">
                        <span></span><span></span><span></span>
                    </button>
                </div>
            </header>
            <div class="nav-wrap">
                <div class="nav-container">
                    <ul class="nav-menu" id="navMenu">
                        <li class="menu-item-has-children">
                            <a href="/natyazhnye-potolki/">Натяжные потолки</a>
                            <div class="dropdown grid-dropdown">
                                <div class="col">
                                    <h4>По фактуре</h4>
                                    <ul>
                                        <li><a href="/matovye/">Матовые</a></li>
                                        <li><a href="/glyanczevye/">Глянцевые</a></li>
                                        <li><a href="/satinovye/">Сатиновые</a></li>
                                        <li><a href="/fakturnye/">Фактурные</a></li>
                                        <li><a href="/tkanevye/">Тканевые</a></li>
                                    </ul>
                                </div>
                                <div class="col">
                                    <h4>По технологии</h4>
                                    <ul>
                                        <li><a href="/belye-potolki/">Белые потолки</a></li>
                                        <li><a href="/czvetnye/">Цветные</a></li>
                                        <li><a href="/paryashhie/">Парящие</a></li>
                                        <li><a href="/s-fotopechatyu/">С фотопечатью</a></li>
                                        <li><a href="/konturnye/">Контурные</a></li>
                                        <li><a href="/krivolinejnaya-spajka/">Криволинейная спайка</a></li>
                                        <li><a href="/s-podsvetkoj/">С подсветкой</a></li>
                                        <li><a href="/mnogourovnevye/">Многоуровневые</a></li>
                                        <li><a href="/tenevye/">Теневые</a></li>
                                        <li><a href="/potolok-kosmos/">Потолок космос</a></li>
                                        <li><a href="/reznoj/">Резной</a></li>
                                        <li><a href="/svetovye-linii/">Световые линии</a></li>
                                        <li><a href="/trekovye-svetilniki/">Трековые светильники</a></li>
                                    </ul>
                                </div>
                                <div class="col">
                                    <h4>По комнатам</h4>
                                    <ul>
                                        <li><a href="/dlya-gostinoj/">Для гостиной</a></li>
                                        <li><a href="/dlya-zala/">Для зала</a></li>
                                        <li><a href="/dlya-spalni/">Для спальни</a></li>
                                        <li><a href="/dlya-detskoj/">Для детской</a></li>
                                        <li><a href="/dlya-koridora/">Для коридора</a></li>
                                        <li><a href="/dlya-kuhni/">Для кухни</a></li>
                                        <li><a href="/dlya-vannoj/">Для ванной</a></li>
                                        <li><a href="/v-bassejn/">В бассейн</a></li>
                                    </ul>
                                </div>
                            </div>
                        </li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/sovety/">Советы</a></li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/nashi-raboty/">Наши работы</a></li>
                        <li class="menu-separator"><span>|</span></li>
                        <li class="menu-item-has-children">
                            <a href="/pokupatelyam/">Покупателям</a>
                            <ul class="dropdown">
                                <li><a href="/garantii-i-sertifikaty/">Гарантии и сертификаты</a></li>
                                <li><a href="/otzyvy/">Отзывы</a></li>
                            </ul>
                        </li>
                        <li class="menu-separator"><span>|</span></li>
                        <li><a href="/kontakty/">Контакты</a></li>
                        <li class="mobile-city"><a href="#" id="mobileCityTrigger">Выбрать город</a></li>
                    </ul>
                </div>
            </div>
        </div>

        <!-- Модальное окно выбора города -->
        <div class="modal" id="cityModal">
            <div class="modal__content">
                <button class="modal__close" id="modalClose">&times;</button>
                <h2>Выберите ваш город</h2>
                <div class="city-grid">
                    <div class="city-group">
                        <h3>Северный регион</h3>
                        <ul class="city-list">
                            <li><button data-city="Борисов">Борисов</button></li>
                            <li><button data-city="Молодечно">Молодечно</button></li>
                            <li><button data-city="Вилейка">Вилейка</button></li>
                            <li><button data-city="Логойск">Логойск</button></li>
                            <li><button data-city="Крупки">Крупки</button></li>
                            <li><button data-city="Мядель">Мядель</button></li>
                        </ul>
                    </div>
                    <div class="city-group">
                        <h3>Восточный регион</h3>
                        <ul class="city-list">
                            <li><button data-city="Березино">Березино</button></li>
                            <li><button data-city="Червень">Червень</button></li>
                            <li><button data-city="Смолевичи">Смолевичи</button></li>
                            <li><button data-city="Марьина Горка">Марьина Горка</button></li>
                        </ul>
                    </div>
                    <div class="city-group">
                        <h3>Южный регион</h3>
                        <ul class="city-list">
                            <li><button data-city="Солигорск">Солигорск</button></li>
                            <li><button data-city="Слуцк">Слуцк</button></li>
                            <li><button data-city="Любань">Любань</button></li>
                            <li><button data-city="Старые Дороги">Старые Дороги</button></li>
                            <li><button data-city="Копыль">Копыль</button></li>
                            <li><button data-city="Клецк">Клецк</button></li>
                            <li><button data-city="Несвиж">Несвиж</button></li>
                        </ul>
                    </div>
                    <div class="city-group">
                        <h3>Западный регион</h3>
                        <ul class="city-list">
                            <li><button data-city="Дзержинск">Дзержинск</button></li>
                            <li><button data-city="Столбцы">Столбцы</button></li>
                            <li><button data-city="Узда">Узда</button></li>
                            <li><button data-city="Воложин">Воложин</button></li>
                        </ul>
                    </div>
                    <div class="city-group">
                        <h3>Крупные центры</h3>
                        <ul class="city-list">
                            <li><button data-city="Жодино">Жодино</button></li>
                            <li><button data-city="Фаниполь">Фаниполь</button></li>
                            <li><button data-city="Заславль">Заславль</button></li>
                            <li><button data-city="Минск">Минск <span class="note">(столица)</span></button></li>
                        </ul>
                    </div>
                    <div class="city-group">
                        <h3>Вся область</h3>
                        <ul class="city-list">
                            <li><button data-city="Минская область">Минская область <br> <span class="note">(весь регион)</span></button></li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>

        <!-- Модальное окно для бесплатного замера -->
        <div class="custom-free-measure-modal" id="customFreeMeasureModal">
            <div class="custom-free-measure-content">
                <span class="custom-free-measure-close" id="customFreeMeasureModalClose">&times;</span>
                <h3>Бесплатный замер</h3>
                <form id="customFreeMeasureForm">
                    <input type="hidden" name="_subject" value="Заявка на бесплатный замер">
                    <input type="hidden" name="_captcha" value="false">
                    <div class="custom-free-measure-form-group">
                        <label class="required">Имя</label>
                        <input type="text" name="name" id="customFreeMeasureName" required>
                    </div>
                    <div class="custom-free-measure-form-group">
                        <label class="required">Телефон</label>
                        <input type="tel" name="phone" id="customFreeMeasurePhone" required placeholder="+375 XX XXX-XX-XX">
                        <div class="custom-free-measure-error-message" id="customFreeMeasurePhoneError"></div>
                    </div>
                    <div class="custom-free-measure-form-group">
                        <label>Комментарий</label>
                        <textarea name="comment" id="customFreeMeasureComment" rows="4"></textarea>
                    </div>
                    <div class="custom-free-measure-checkbox-group">
                        <input type="checkbox" id="customFreeMeasureConsent" required>
                        <label for="customFreeMeasureConsent">Даю согласие на обработку моих персональных данных</label>
                    </div>
                    <div class="custom-free-measure-error-message" id="customFreeMeasureConsentError"></div>
                    <div class="custom-free-measure-modal-actions">
                        <button type="button" class="custom-free-measure-btn-secondary" id="customFreeMeasureClose">Отмена</button>
                        <button type="submit" class="custom-free-measure-btn-primary" id="customFreeMeasureSubmit">Отправить</button>
                    </div>
                </form>
            </div>
        </div>
    </div>

    <?php
    return ob_get_clean();
}
Add code to functions.php or an MU-plugin.
Use shortcode [custom_header primary_color="#f58200" hover_color="#ff7a00" button_bg="#1A2B3C" button_hover_bg="#1A252F"].
Set logo via Customizer (custom_logo).
Menu and cities can be edited directly in the code.
The request form uses formsubmit.co — replace the email in JS.
🖼️

Gallery tabs with lightbox

Two shortcodes: [gallery_tabs] (grid + text) and [gallery_all_tabs] (all images in tabs). Admin panel to manage categories and images.

<?php
// === Галереи: регистрация таксономии, админ-страница, медиазагрузка и два шорткода ===

// 1) Регистрация таксономии gallery_cat
add_action('init', function() {
    $labels = array(
        'name'              => 'Галереи',
        'singular_name'     => 'Галерея',
        'search_items'      => 'Поиск галерей',
        'all_items'         => 'Все галереи',
        'edit_item'         => 'Редактировать галерею',
        'view_item'         => 'Просмотреть галерею',
        'update_item'       => 'Обновить галерею',
        'add_new_item'      => 'Добавить галерею',
        'new_item_name'     => 'Новая галерея',
        'menu_name'         => 'Галереи',
    );
    register_taxonomy('gallery_cat', array('post'), array(
        'labels'            => $labels,
        'public'            => false,
        'show_ui'           => false,
        'hierarchical'      => true,
        'rewrite'           => false,
    ));
});

// 2) Пункт меню в админке
add_action('admin_menu', function() {
    add_menu_page(
        'Галереи',
        'Галереи',
        'manage_options',
        'custom-galleries',
        'cg_render_admin_page',
        'dashicons-format-gallery',
        58
    );
});

// 3) Скрипты для медиазагрузчика
add_action('admin_enqueue_scripts', function($hook) {
    if ($hook !== 'toplevel_page_custom-galleries') return;
    wp_enqueue_media();
    wp_enqueue_script('cg-admin', get_stylesheet_directory_uri() . '/cg-admin.js', array('jquery'), false, true);
    wp_add_inline_script('cg-admin', cg_admin_js());
    wp_enqueue_style('cg-admin-css', false);
    wp_add_inline_style('cg-admin-css', cg_admin_css());
});

// 4) Обработка сохранения термина (admin-post)
add_action('admin_post_save_gallery_term', function() {
    if (!current_user_can('manage_options')) wp_die('No permission');
    check_admin_referer('cg_save_term');

    $action = isset($_POST['cg_action']) ? sanitize_text_field($_POST['cg_action']) : '';
    $name = isset($_POST['cg_name']) ? sanitize_text_field($_POST['cg_name']) : '';
    $slug = isset($_POST['cg_slug']) ? sanitize_text_field($_POST['cg_slug']) : '';
    $term_id = isset($_POST['cg_term_id']) ? intval($_POST['cg_term_id']) : 0;
    $description = isset($_POST['cg_description']) ? wp_kses_post($_POST['cg_description']) : '';
    $button_enabled = isset($_POST['cg_button_enabled']) ? 1 : 0;
    $button_label = isset($_POST['cg_button_label']) ? sanitize_text_field($_POST['cg_button_label']) : 'Заказать';
    $order = isset($_POST['cg_order']) ? sanitize_text_field($_POST['cg_order']) : 'date_desc';
    $images = isset($_POST['cg_images']) ? array_map('intval', $_POST['cg_images']) : array();
    $title = isset($_POST['cg_title']) ? sanitize_text_field($_POST['cg_title']) : '';
    $tab_order = isset($_POST['cg_tab_order']) ? intval($_POST['cg_tab_order']) : 0; // НОВОЕ ПОЛЕ

    if ($action === 'add') {
        if (empty($name)) {
            wp_redirect(add_query_arg('cg_err', 'empty_name', wp_get_referer()));
            exit;
        }
        $r = wp_insert_term($name, 'gallery_cat', array('slug' => $slug));
        if (is_wp_error($r)) {
            wp_redirect(add_query_arg('cg_err', 'insert_failed', wp_get_referer()));
            exit;
        }
        $term_id = $r['term_id'];
    } elseif ($action === 'delete' && $term_id) {
        wp_delete_term($term_id, 'gallery_cat');
        wp_redirect(admin_url('admin.php?page=custom-galleries'));
        exit;
    } elseif ($action !== 'edit') {
        wp_redirect(add_query_arg('cg_err', 'bad_action', wp_get_referer()));
        exit;
    }

    if ($term_id) {
        wp_update_term($term_id, 'gallery_cat', array(
            'description' => $description,
        ));
        update_term_meta($term_id, 'gallery_button_enabled', $button_enabled ? 1 : 0);
        update_term_meta($term_id, 'gallery_button_label', $button_label);
        update_term_meta($term_id, 'gallery_images', $images);
        update_term_meta($term_id, 'gallery_order', in_array($order, array('date_desc','date_asc','random')) ? $order : 'date_desc');
        update_term_meta($term_id, 'gallery_title', $title);
        update_term_meta($term_id, 'gallery_tab_order', $tab_order); // СОХРАНЕНИЕ ПОРЯДКА ТАБОВ
    }

    wp_redirect(admin_url('admin.php?page=custom-galleries&cg_saved=1&term='.$term_id));
    exit;
});

// 4.1) Обработка сохранения глобальных настроек лайтбокса
add_action('admin_post_save_lightbox_settings', function() {
    if (!current_user_can('manage_options')) wp_die('No permission');
    check_admin_referer('cg_lightbox_settings');

    $enabled = isset($_POST['cg_lightbox_4_3']) ? 1 : 0;
    update_option('cg_lightbox_4_3_enabled', $enabled);

    wp_redirect(admin_url('admin.php?page=custom-galleries&lightbox_saved=1'));
    exit;
});

// 5) Рендер админ-страницы
function cg_render_admin_page() {
    if (!current_user_can('manage_options')) return;
    $terms = get_terms(array('taxonomy' => 'gallery_cat', 'hide_empty' => false));
    $editing_term = null;
    $edit_id = isset($_GET['term']) ? intval($_GET['term']) : 0;
    if ($edit_id) $editing_term = get_term($edit_id, 'gallery_cat');

    $lightbox_enabled = get_option('cg_lightbox_4_3_enabled', 1);
    ?>
    <div class="wrap">
        <h1>Галереи</h1>

        <!-- Блок глобальных настроек лайтбокса -->
        <div style="background: #fff; padding: 15px; margin-bottom: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
            <h2 style="margin-top: 0;">Настройки лайтбокса</h2>
            <form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
                <?php wp_nonce_field('cg_lightbox_settings'); ?>
                <input type="hidden" name="action" value="save_lightbox_settings">
                <label>
                    <input type="checkbox" name="cg_lightbox_4_3" value="1" <?php checked($lightbox_enabled, 1); ?>>
                    Режим 4:3 для лайтбокса (изображение заполняет контейнер с обрезкой по краям)
                </label>
                <p class="description">При включении все изображения в лайтбоксе будут отображаться в пропорциях 4:3 с object-fit: cover. Контейнер лайтбокса имеет фиксированные размеры, что исключает скачки при переключении.</p>
                <p><input type="submit" class="button button-secondary" value="Сохранить настройки"></p>
            </form>
        </div>

        <div style="display:flex; gap:30px; align-items:flex-start;">
            <div style="flex:1; min-width:320px;">
                <h2>Существующие категории</h2>
                <table class="widefat fixed striped">
                    <thead><tr><th>Название</th><th>Слаг</th><th>Картинок</th><th>Действие</th></tr></thead>
                    <tbody>
                    <?php if ($terms): foreach($terms as $t):
                        $imgs = get_term_meta($t->term_id, 'gallery_images', true);
                        $count = is_array($imgs) ? count($imgs) : 0;
                        ?>
                        <tr>
                            <td><?php echo esc_html($t->name); ?></td>
                            <td><?php echo esc_html($t->slug); ?></td>
                            <td><?php echo intval($count); ?></td>
                            <td>
                                <a class="button" href="<?php echo admin_url('admin.php?page=custom-galleries&term='.$t->term_id); ?>">Редактировать</a>
                                <form style="display:inline" method="post" action="<?php echo admin_url('admin-post.php'); ?>" onsubmit="return confirm('Удалить категорию и её данные?');">
                                    <?php wp_nonce_field('cg_save_term'); ?>
                                    <input type="hidden" name="action" value="save_gallery_term">
                                    <input type="hidden" name="cg_action" value="delete">
                                    <input type="hidden" name="cg_term_id" value="<?php echo intval($t->term_id); ?>">
                                    <input type="submit" class="button button-danger" value="Удалить">
                                </form>
                            </td>
                        </tr>
                    <?php endforeach; else: ?>
                        <tr><td colspan="4">Категорий нет</td></tr>
                    <?php endif; ?>
                    </tbody>
                </table>

                <h3 style="margin-top:20px;">Добавить новую категорию</h3>
                <form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
                    <?php wp_nonce_field('cg_save_term'); ?>
                    <input type="hidden" name="action" value="save_gallery_term">
                    <input type="hidden" name="cg_action" value="add">
                    <table class="form-table">
                        <tr><th>Название</th><td><input name="cg_name" class="regular-text"></td></tr>
                        <tr><th>Слаг (необяз.)</th><td><input name="cg_slug" class="regular-text"></td></tr>
                    </table>
                    <p><input type="submit" class="button button-primary" value="Добавить"></p>
                </form>
            </div>

            <div style="flex:1; min-width:420px;">
                <?php if ($editing_term): 
                    $term_id = $editing_term->term_id;
                    $images = get_term_meta($term_id, 'gallery_images', true) ?: array();
                    $button_enabled = get_term_meta($term_id, 'gallery_button_enabled', true) ? 1 : 0;
                    $button_label = get_term_meta($term_id, 'gallery_button_label', true) ?: 'Заказать';
                    $order = get_term_meta($term_id, 'gallery_order', true) ?: 'date_desc';
                    $title = get_term_meta($term_id, 'gallery_title', true);
                    $tab_order = get_term_meta($term_id, 'gallery_tab_order', true); // ПОЛУЧАЕМ ПОРЯДОК ТАБОВ
                    ?>
                    <h2>Редактирование: <?php echo esc_html($editing_term->name); ?></h2>
                    <form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
                        <?php wp_nonce_field('cg_save_term'); ?>
                        <input type="hidden" name="action" value="save_gallery_term">
                        <input type="hidden" name="cg_action" value="edit">
                        <input type="hidden" name="cg_term_id" value="<?php echo intval($term_id); ?>">

                        <table class="form-table">
                            <tr>
                                <th>Заголовок (H3)</th>
                                <td><input name="cg_title" class="regular-text" value="<?php echo esc_attr($title); ?>"></td>
                            </tr>
                            <tr>
                                <th>Описание</th>
                                <td><textarea name="cg_description" rows="6" class="large-text"><?php echo esc_textarea($editing_term->description); ?></textarea></td>
                            </tr>
                            <tr>
                                <th>Кнопка</th>
                                <td>
                                    <label><input type="checkbox" name="cg_button_enabled" <?php checked(1,$button_enabled); ?>> Показывать кнопку</label><br>
                                    <input name="cg_button_label" class="regular-text" value="<?php echo esc_attr($button_label); ?>">
                                </td>
                            </tr>
                            <tr>
                                <th>Порядок картинок</th>
                                <td>
                                    <select name="cg_order">
                                        <option value="date_desc" <?php selected($order,'date_desc'); ?>>По дате (новые сверху)</option>
                                        <option value="date_asc" <?php selected($order,'date_asc'); ?>>По дате (старые сверху)</option>
                                        <option value="random" <?php selected($order,'random'); ?>>Случайный</option>
                                    </select>
                                </td>
                            </tr>
                            <!-- НОВОЕ ПОЛЕ: ПОРЯДОК ТАБОВ -->
                            <tr>
                                <th>Порядок табов</th>
                                <td>
                                    <input type="number" name="cg_tab_order" value="<?php echo esc_attr($tab_order); ?>" step="1" style="width: 100px;">
                                    <span class="description">Чем меньше число, тем выше будет вкладка в списке.</span>
                                </td>
                            </tr>
                            <tr>
                                <th>Картинки галереи</th>
                                <td>
                                    <div id="cg-images-list" style="display:flex;gap:10px;flex-wrap:wrap;">
                                        <?php foreach($images as $img_id): $src = wp_get_attachment_image_url($img_id, 'thumbnail'); if (!$src) continue; ?>
                                            <div class="cg-image-item" data-id="<?php echo intval($img_id); ?>">
                                                <img src="<?php echo esc_url($src); ?>" style="width:120px;height:80px;object-fit:cover;border:1px solid #ddd;padding:2px;">
                                                <div style="text-align:center;margin-top:4px;">
                                                    <button type="button" class="button cg-remove-image">Удалить</button>
                                                </div>
                                            </div>
                                        <?php endforeach; ?>
                                    </div>
                                    <p>
                                        <button id="cg-add-images" type="button" class="button">Добавить/выбрать изображения</button>
                                    </p>
                                    <input type="hidden" name="cg_images[]" value="" id="cg-images-hidden-1">
                                 </div>
                             </td>
                         </tr>
                        </table>

                        <p><input type="submit" class="button button-primary" value="Сохранить"></p>
                    </form>

                <?php else: ?>
                    <h2>Редактор категории</h2>
                    <p>Выберите категорию слева для редактирования.</p>
                <?php endif; ?>
            </div>
        </div>
    </div>
    <?php
}

// 6) Встроенные JS и CSS для админки
function cg_admin_js() {
    return <<<JS
jQuery(function($){
    var frame;
    $('#cg-add-images').on('click', function(e){
        e.preventDefault();
        if (frame) frame.open();
        else {
            frame = wp.media({
                title: 'Выберите картинки для галереи',
                library: { type: 'image' },
                button: { text: 'Добавить' },
                multiple: true
            });
            frame.on('select', function(){
                var attachments = frame.state().get('selection').toJSON();
                var list = $('#cg-images-list');
                attachments.forEach(function(att){
                    if ($('#cg-images-list .cg-image-item[data-id="'+att.id+'"]').length) return;
                    var img = $('<div class="cg-image-item" data-id="'+att.id+'"></div>');
                    var thumb = $('<img>').attr('src', att.sizes && att.sizes.thumbnail ? att.sizes.thumbnail.url : att.url).css({width:'120px',height:'80px','object-fit':'cover',border:'1px solid #ddd',padding:'2px'});
                    var btn = $('<div style="text-align:center;margin-top:4px;"><button type="button" class="button cg-remove-image">Удалить</button></div>');
                    img.append(thumb).append(btn);
                    list.append(img);
                });
                cg_store_ids();
            });
        }
        frame.open();
    });

    $('#cg-images-list').on('click', '.cg-remove-image', function(){
        $(this).closest('.cg-image-item').remove();
        cg_store_ids();
    });

    function cg_store_ids(){
        var ids = [];
        $('#cg-images-list .cg-image-item').each(function(){
            ids.push($(this).data('id'));
        });
        $('input[name="cg_images[]"]').remove();
        ids.forEach(function(id){
            var inp = $('<input>').attr('type','hidden').attr('name','cg_images[]').val(id);
            $('#cg-images-list').after(inp);
        });
    }
    cg_store_ids();
});
JS;
}

function cg_admin_css() {
    return <<<CSS
.cg-image-item { position:relative; display:inline-block; }
CGADMIN {}
CSS;
}

// ========== ШОРТКОД №1 (исходный) ==========
add_shortcode('gallery_tabs', function($atts){
    $atts = shortcode_atts(array(
        'order'   => '',
        'default' => '',
    ), $atts, 'gallery_tabs');

    $terms = get_terms(array('taxonomy' => 'gallery_cat', 'hide_empty' => false));
    if (is_wp_error($terms) || empty($terms)) return '<!-- gallery_tabs: no categories -->';

    // === НОВАЯ СОРТИРОВКА ПО ПОЛЮ "ПОРЯДОК ТАБОВ" ===
    usort($terms, function($a, $b) {
        $order_a = (int) get_term_meta($a->term_id, 'gallery_tab_order', true);
        $order_b = (int) get_term_meta($b->term_id, 'gallery_tab_order', true);
        if ($order_a === $order_b) {
            return strcmp($a->name, $b->name);
        }
        return ($order_a < $order_b) ? -1 : 1;
    });
    // =============================================

    $uid = 'cg_tabs_' . wp_unique_id();

    // Глобальная настройка лайтбокса
    $lightbox_4_3 = get_option('cg_lightbox_4_3_enabled', 1);
    $lightbox_class = 'cg-lightbox';
    if ($lightbox_4_3) {
        $lightbox_class .= ' cg-lightbox-4-3';
    }

    ob_start();
    ?>

    <div id="<?php echo esc_attr($uid); ?>" class="cg-gallery-tabs-wrap">
        <h2 class="cg-section-title">Примеры наших работ</h2>

        <!-- Навигация по вкладкам -->
        <div class="cg-tabs-nav" role="tablist">
            <?php foreach ($terms as $i => $t):
                $slug = esc_attr($t->slug);
                $active = ($atts['default'] && $atts['default'] === $t->slug) || (!$atts['default'] && $i === 0);
            ?>
                <button type="button" role="tab"
                        class="cg-tab <?php echo $active ? 'active' : ''; ?>"
                        data-slug="<?php echo $slug; ?>"
                        aria-selected="<?php echo $active ? 'true' : 'false'; ?>">
                    <?php echo esc_html($t->name); ?>
                </button>
            <?php endforeach; ?>
        </div>

        <!-- Панели вкладок -->
        <div class="cg-tabs-panels">
            <?php foreach ($terms as $i => $t):
                $slug = $t->slug;
                $term_id = $t->term_id;
                $is_active = ($atts['default'] && $atts['default'] === $slug) || (!$atts['default'] && $i === 0);

                $images = get_term_meta($term_id, 'gallery_images', true) ?: array();
                if (!is_array($images)) $images = array();

                // Сортировка
                $order = $atts['order'] ?: (get_term_meta($term_id, 'gallery_order', true) ?: 'date_desc');
                if ($order === 'random') {
                    shuffle($images);
                } elseif ($order === 'date_asc') {
                    usort($images, function($a, $b) {
                        return strtotime(get_post_field('post_date', $a)) - strtotime(get_post_field('post_date', $b));
                    });
                } else {
                    usort($images, function($a, $b) {
                        return strtotime(get_post_field('post_date', $b)) - strtotime(get_post_field('post_date', $a));
                    });
                }

                // Берём не более 6 изображений
                $images = array_slice($images, 0, 6);

                // Данные для текстового блока (десктоп)
                $main_id = !empty($images) ? $images[0] : 0;
                $thumbs = array_slice($images, 1, 4);
                $main_src = $main_id ? (wp_get_attachment_image_url($main_id, 'large') ?: wp_get_attachment_image_url($main_id, 'full')) : '';
                $description = term_description($term_id, 'gallery_cat');
                $button_enabled = get_term_meta($term_id, 'gallery_button_enabled', true) ? 1 : 0;
                $button_label = get_term_meta($term_id, 'gallery_button_label', true) ?: 'Заказать';
                $title = get_term_meta($term_id, 'gallery_title', true) ?: $t->name;

                // Собираем все полноразмерные URL для лайтбокса
                $lightbox_images = array();
                foreach ($images as $img_id) {
                    $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                    if ($full) $lightbox_images[] = $full;
                }
                $lightbox_json = htmlspecialchars(json_encode($lightbox_images), ENT_QUOTES, 'UTF-8');
            ?>
                <section class="cg-tab-panel <?php echo $is_active ? 'active' : ''; ?>" data-slug="<?php echo esc_attr($slug); ?>" role="tabpanel" data-lightbox="<?php echo $lightbox_json; ?>">

                    <!-- Десктопная версия (исходная вёрстка) -->
                    <div class="cg-desktop-layout">
                        <div class="cg-gallery-block-original">
                            <div class="cg-main-wrap" data-full="<?php echo esc_url($main_src); ?>">
                                <?php if ($main_src): ?>
                                    <img class="cg-main-img" data-src="<?php echo esc_url($main_src); ?>" alt="<?php echo esc_attr($t->name); ?>">
                                <?php else: ?>
                                    <div class="cg-no-img">Нет изображения</div>
                                <?php endif; ?>
                            </div>
                            <div class="cg-text-block">
                                <h3 class="cg-title"><?php echo esc_html($title); ?></h3>
                                <div class="cg-desc"><?php echo $description; ?></div>
                                <?php if ($button_enabled): ?>
                                    <a class="cg-cta btn" href="#"><?php echo esc_html($button_label); ?></a>
                                <?php endif; ?>
                            </div>
                            <div class="cg-thumbs-row">
                                <?php for ($k = 0; $k < 4; $k++):
                                    $tid = isset($thumbs[$k]) ? $thumbs[$k] : 0;
                                    if ($tid):
                                        $src = wp_get_attachment_image_url($tid, 'medium') ?: wp_get_attachment_image_url($tid, 'thumbnail');
                                        $full = wp_get_attachment_image_url($tid, 'large') ?: wp_get_attachment_image_url($tid, 'full');
                                        echo '<div class="cg-thumb-wrap" data-full="'.esc_url($full).'"><img class="cg-thumb" data-src="'.esc_url($src).'" alt=""></div>';
                                    else:
                                        echo '<div class="cg-thumb-wrap cg-thumb-empty"></div>';
                                    endif;
                                endfor; ?>
                            </div>
                        </div>
                    </div>

                    <!-- Мобильная версия (сетка 3×2 / 2×3, без текста) -->
                    <div class="cg-mobile-layout">
                        <div class="cg-images-grid">
                            <?php foreach ($images as $img_id):
                                $thumb = wp_get_attachment_image_url($img_id, 'medium') ?: wp_get_attachment_image_url($img_id, 'thumbnail');
                                $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                                if (!$thumb) continue;
                            ?>
                                <div class="cg-image-item" data-full="<?php echo esc_url($full); ?>">
                                    <img data-src="<?php echo esc_url($thumb); ?>" alt="">
                                </div>
                            <?php endforeach; ?>
                        </div>
                    </div>

                </section>
            <?php endforeach; ?>
        </div><!-- .cg-tabs-panels -->

        <!-- Лайтбокс -->
        <div class="<?php echo esc_attr($lightbox_class); ?>" id="<?php echo esc_attr($uid); ?>-lightbox">
            <div class="cg-lightbox-overlay"></div>
            <div class="cg-lightbox-content">
                <img src="" alt="" class="cg-lightbox-img">
                <button class="cg-lightbox-close">&times;</button>
                <button class="cg-lightbox-prev">&#10094;</button>
                <button class="cg-lightbox-next">&#10095;</button>
            </div>
        </div>
    </div>

    <style>
        /* Стили для шорткода gallery_tabs */
        #<?php echo esc_attr($uid); ?>.cg-gallery-tabs-wrap {
            max-width: 1400px;
            margin: 0 auto;
            font-family: 'Roboto', sans-serif;
            color: #222;
            position: relative;
        }
        #<?php echo esc_attr($uid); ?> .cg-section-title {
            text-align: center;
            margin: 0 0 24px;
            font-size: 36px;
            font-weight: 600;
            text-transform: uppercase;
            color: #262626;
            letter-spacing: 0.02em;
        }
        #<?php echo esc_attr($uid); ?> .cg-tabs-nav {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            background: #f2f2f2;
            border-radius: 6px;
            margin-bottom: 20px;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab {
            flex: 1 1 auto;
            border: none;
            background: transparent;
            padding: 18px 10px;
            cursor: pointer;
            font-family: 'Roboto', sans-serif;
            font-weight: 300;
            font-size: 22px;
            color: #444;
            border-radius: 4px;
            transition: background .12s, color .12s;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab.active {
            background: #f07f00;
            color: #fff;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab:focus {
            outline: 3px solid rgba(240,127,0,0.18);
        }
        #<?php echo esc_attr($uid); ?> .cg-tab-panel {
            padding: 16px 0;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab-panel:not(.active) {
            display: none;
        }
        #<?php echo esc_attr($uid); ?> .cg-desktop-layout {
            display: none;
        }
        #<?php echo esc_attr($uid); ?> .cg-mobile-layout {
            display: none;
        }
        @media (min-width: 1023px) {
            #<?php echo esc_attr($uid); ?> .cg-desktop-layout {
                display: block;
            }
            #<?php echo esc_attr($uid); ?> .cg-mobile-layout {
                display: none;
            }
        }
        @media (max-width: 1022px) {
            #<?php echo esc_attr($uid); ?> .cg-desktop-layout {
                display: none;
            }
            #<?php echo esc_attr($uid); ?> .cg-mobile-layout {
                display: block;
            }
        }
        /* Стили для десктопной вёрстки */
        #<?php echo esc_attr($uid); ?> .cg-gallery-block-original {
            display: grid;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: auto auto;
            gap: 16px;
            align-items: start;
        }
        #<?php echo esc_attr($uid); ?> .cg-main-wrap {
            grid-column: 1 / 2;
            grid-row: 1 / 2;
            background: #fff;
            border-radius: 6px;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 6px 20px rgba(0,0,0,0.04);
            aspect-ratio: 4 / 3;
            cursor: pointer;
        }
        #<?php echo esc_attr($uid); ?> .cg-main-img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        }
        #<?php echo esc_attr($uid); ?> .cg-text-block {
            grid-column: 2 / 3;
            grid-row: 1 / 2;
            padding: 0 0 0 20px;
            box-sizing: border-box;
        }
        #<?php echo esc_attr($uid); ?> .cg-title {
            font-family: 'Roboto', sans-serif;
            font-weight: 600;
            font-size: 30px;
            color: #434b53;
            margin: 0 0 10px;
        }
        #<?php echo esc_attr($uid); ?> .cg-desc {
            font-family: 'Roboto', sans-serif;
            font-weight: 400;
            font-size: 22px;
            color: #434b53;
            line-height: 1.5;
            margin-bottom: 16px;
        }
        #<?php echo esc_attr($uid); ?> .cg-cta {
            display: inline-block;
            padding: 10px 18px;
            background: #f07f00;
            color: #fff;
            text-decoration: none;
            border-radius: 6px;
            font-family: 'Roboto', sans-serif;
            font-weight: 400;
            font-size: 22px;
            cursor: pointer;
            border: none;
            transition: background 0.2s;
        }
        #<?php echo esc_attr($uid); ?> .cg-cta:hover {
            background: #e67e00;
        }
        #<?php echo esc_attr($uid); ?> .cg-thumbs-row {
            grid-column: 1 / 3;
            grid-row: 2 / 3;
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 16px;
        }
        #<?php echo esc_attr($uid); ?> .cg-thumb-wrap {
            background: #fff;
            border-radius: 6px;
            overflow: hidden;
            aspect-ratio: 4 / 3;
            cursor: pointer;
        }
        #<?php echo esc_attr($uid); ?> .cg-thumb {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        }
        #<?php echo esc_attr($uid); ?> .cg-thumb-empty {
            background: #f6f6f6;
            border-radius: 6px;
            aspect-ratio: 4 / 3;
        }
        /* Мобильная сетка */
        #<?php echo esc_attr($uid); ?> .cg-images-grid {
            display: grid;
            gap: 12px;
        }
        #<?php echo esc_attr($uid); ?> .cg-image-item {
            aspect-ratio: 1 / 1;
            background: #f6f6f6;
            border-radius: 6px;
            overflow: hidden;
            cursor: pointer;
        }
        #<?php echo esc_attr($uid); ?> .cg-image-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        }
        @media (min-width: 601px) and (max-width: 1022px) {
            #<?php echo esc_attr($uid); ?> .cg-images-grid {
                grid-template-columns: repeat(3, 1fr);
            }
        }
        @media (max-width: 600px) {
            #<?php echo esc_attr($uid); ?> .cg-images-grid {
                grid-template-columns: repeat(2, 1fr);
                gap: 8px;
            }
        }
        /* Эффект блеска */
        .cg-main-wrap.shine::after,
        .cg-thumb-wrap.shine::after,
        .cg-image-item.shine::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 50%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            animation: shine-animation 1s forwards;
            pointer-events: none;
            z-index: 3;
        }
        .cg-main-wrap, .cg-thumb-wrap, .cg-image-item {
            position: relative;
            overflow: hidden;
        }
        @keyframes shine-animation {
            0% { left: -150%; }
            100% { left: 100%; }
        }
        /* Лайтбокс */
        #<?php echo esc_attr($uid); ?> .cg-lightbox {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 9999;
            align-items: center;
            justify-content: center;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox.active {
            display: flex;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.9);
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-content {
            position: relative;
            max-width: 90vw;
            max-height: 90vh;
            z-index: 10000;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-img {
            max-width: 100%;
            max-height: 90vh;
            display: block;
            margin: 0 auto;
            border-radius: 4px;
        }
        /* Стили для режима 4:3 в лайтбоксе — фиксированный размер контейнера */
        #<?php echo esc_attr($uid); ?> .cg-lightbox.cg-lightbox-4-3 .cg-lightbox-content {
            width: 90vw;
            height: calc(90vw * 3 / 4);
            max-width: calc(90vh * 4 / 3);
            max-height: 90vh;
            aspect-ratio: 4 / 3;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox.cg-lightbox-4-3 .cg-lightbox-img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-close {
            position: absolute;
            top: -40px;
            right: 0;
            background: none;
            border: none;
            color: white;
            font-size: 40px;
            cursor: pointer;
            padding: 0 10px;
            line-height: 1;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev,
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            background: rgba(255,255,255,0.2);
            border: none;
            color: white;
            font-size: 40px;
            cursor: pointer;
            padding: 10px 15px;
            border-radius: 4px;
            transition: background 0.2s;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev:hover,
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next:hover {
            background: rgba(255,255,255,0.4);
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev {
            left: 10px;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next {
            right: 10px;
        }
    </style>

    <script>
    (function(){
        var root = document.getElementById('<?php echo esc_js($uid); ?>');
        if (!root) return;

        var tabs = Array.prototype.slice.call(root.querySelectorAll('.cg-tab'));
        var panels = Array.prototype.slice.call(root.querySelectorAll('.cg-tab-panel'));

        var currentLoading = {
            panel: null,
            timeoutIds: [],
            stop: false
        };

        function stopLoading() {
            currentLoading.stop = true;
            currentLoading.timeoutIds.forEach(id => clearTimeout(id));
            currentLoading.timeoutIds = [];
        }

        function loadImagesSequentially(panel) {
            stopLoading();
            currentLoading.panel = panel;
            currentLoading.stop = false;

            var images = panel.querySelectorAll('.cg-main-img, .cg-thumb, .cg-image-item img');
            var index = 0;
            var total = images.length;

            function loadNext() {
                if (currentLoading.stop || index >= total) return;
                var img = images[index];
                var container = img.closest('.cg-main-wrap, .cg-thumb-wrap, .cg-image-item');
                if (!container) {
                    index++;
                    loadNext();
                    return;
                }
                var dataSrc = img.getAttribute('data-src');
                if (!dataSrc) {
                    index++;
                    loadNext();
                    return;
                }

                function afterLoad() {
                    if (currentLoading.stop) return;
                    container.classList.add('shine');
                    var timeoutId = setTimeout(function() {
                        container.classList.remove('shine');
                        index++;
                        loadNext();
                    }, 1000);
                    currentLoading.timeoutIds.push(timeoutId);
                }

                if (img.complete && img.naturalHeight > 0) {
                    afterLoad();
                } else {
                    img.onload = function() {
                        img.onload = null;
                        img.onerror = null;
                        afterLoad();
                    };
                    img.onerror = function() {
                        img.onerror = null;
                        index++;
                        loadNext();
                    };
                    img.src = dataSrc;
                }
            }

            loadNext();
        }

        function activate(slug) {
            tabs.forEach(function(tb) {
                var is = tb.dataset.slug === slug;
                tb.classList.toggle('active', is);
                tb.setAttribute('aria-selected', is ? 'true' : 'false');
            });
            panels.forEach(function(p) {
                var active = (p.dataset.slug === slug);
                p.style.display = active ? 'block' : 'none';
                if (active) {
                    loadImagesSequentially(p);
                }
            });
            sessionStorage.setItem('cg_active_tab_<?php echo esc_js($uid); ?>', slug);
        }

        var stored = sessionStorage.getItem('cg_active_tab_<?php echo esc_js($uid); ?>');
        if (stored && tabs.some(tb => tb.dataset.slug === stored)) {
            activate(stored);
        } else {
            var start = '<?php echo esc_js($atts['default'] ?: ''); ?>';
            if (!start && tabs.length) start = tabs[0].dataset.slug;
            if (start) activate(start);
        }

        tabs.forEach(function(tb) {
            tb.addEventListener('click', function() { activate(tb.dataset.slug); });
            tb.addEventListener('keydown', function(e) {
                if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                    e.preventDefault();
                    var idx = tabs.indexOf(tb);
                    var next = (e.key === 'ArrowRight') ? (idx + 1) : (idx - 1);
                    if (next < 0) next = tabs.length - 1;
                    if (next >= tabs.length) next = 0;
                    tabs[next].focus();
                    tabs[next].click();
                }
            });
        });

        // Лайтбокс
        var lightbox = root.querySelector('.cg-lightbox');
        var lightboxImg = lightbox.querySelector('.cg-lightbox-img');
        var lightboxClose = lightbox.querySelector('.cg-lightbox-close');
        var lightboxPrev = lightbox.querySelector('.cg-lightbox-prev');
        var lightboxNext = lightbox.querySelector('.cg-lightbox-next');
        var currentImages = [];
        var currentIndex = 0;

        function openLightbox(index) {
            if (!currentImages.length) return;
            lightbox.classList.add('active');
            document.body.style.overflow = 'hidden';
            showImage(index);
        }

        function showImage(index) {
            if (index < 0) index = currentImages.length - 1;
            if (index >= currentImages.length) index = 0;
            currentIndex = index;
            lightboxImg.src = currentImages[currentIndex];
        }

        function closeLightbox() {
            lightbox.classList.remove('active');
            document.body.style.overflow = '';
        }

        lightboxClose.addEventListener('click', closeLightbox);
        lightboxPrev.addEventListener('click', function() {
            showImage(currentIndex - 1);
        });
        lightboxNext.addEventListener('click', function() {
            showImage(currentIndex + 1);
        });
        lightbox.addEventListener('click', function(e) {
            if (e.target === lightbox || e.target.classList.contains('cg-lightbox-overlay')) {
                closeLightbox();
            }
        });

        document.addEventListener('keydown', function(e) {
            if (!lightbox.classList.contains('active')) return;
            if (e.key === 'Escape') {
                closeLightbox();
            } else if (e.key === 'ArrowLeft') {
                showImage(currentIndex - 1);
            } else if (e.key === 'ArrowRight') {
                showImage(currentIndex + 1);
            }
        });

        root.addEventListener('click', function(e) {
            var target = e.target.closest('.cg-main-wrap, .cg-thumb-wrap, .cg-image-item');
            if (!target) return;
            var panel = target.closest('.cg-tab-panel');
            if (!panel) return;
            var imagesData = panel.getAttribute('data-lightbox');
            if (!imagesData) return;
            try {
                currentImages = JSON.parse(imagesData);
            } catch (err) {
                return;
            }

            var fullUrl = target.getAttribute('data-full');
            if (!fullUrl) return;
            var idx = currentImages.indexOf(fullUrl);
            if (idx >= 0) {
                currentIndex = idx;
                openLightbox(currentIndex);
            }
        });
    })();
    </script>

    <?php
    return ob_get_clean();
});

// ========== ШОРТКОД №2 (новый: все изображения с табом "Все") ==========
add_shortcode('gallery_all_tabs', function($atts){
    $atts = shortcode_atts(array(
        'title'   => 'Наши выполненные объекты',
        'order'   => 'date_desc',
        'default' => 'all',
    ), $atts, 'gallery_all_tabs');

    $terms = get_terms(array('taxonomy' => 'gallery_cat', 'hide_empty' => false));
    if (is_wp_error($terms) || empty($terms)) return '<!-- gallery_all_tabs: no categories -->';

    $uid = 'cg_all_tabs_' . wp_unique_id();

    // Глобальная настройка лайтбокса
    $lightbox_4_3 = get_option('cg_lightbox_4_3_enabled', 1);
    $lightbox_class = 'cg-lightbox';
    if ($lightbox_4_3) {
        $lightbox_class .= ' cg-lightbox-4-3';
    }

    // Собираем все изображения из всех категорий (без повторов) для таба "Все"
    $all_images_ids = array();
    foreach ($terms as $t) {
        $imgs = get_term_meta($t->term_id, 'gallery_images', true);
        if (is_array($imgs)) {
            $all_images_ids = array_merge($all_images_ids, $imgs);
        }
    }
    $all_images_ids = array_unique($all_images_ids);

    // Сортировка для всех изображений
    $order = $atts['order'];
    if ($order === 'random') {
        shuffle($all_images_ids);
    } elseif ($order === 'date_asc') {
        usort($all_images_ids, function($a, $b) {
            return strtotime(get_post_field('post_date', $a)) - strtotime(get_post_field('post_date', $b));
        });
    } else {
        usort($all_images_ids, function($a, $b) {
            return strtotime(get_post_field('post_date', $b)) - strtotime(get_post_field('post_date', $a));
        });
    }

    ob_start();
    ?>

    <div id="<?php echo esc_attr($uid); ?>" class="cg-all-tabs-wrap">
        <h2 class="cg-section-title"><?php echo esc_html($atts['title']); ?></h2>

        <!-- Табы: Все + категории -->
        <div class="cg-tabs-nav" role="tablist">
            <button type="button" role="tab"
                    class="cg-tab <?php echo ($atts['default'] === 'all') ? 'active' : ''; ?>"
                    data-slug="all"
                    aria-selected="<?php echo ($atts['default'] === 'all') ? 'true' : 'false'; ?>">
                Все
            </button>
            <?php foreach ($terms as $i => $t):
                $slug = esc_attr($t->slug);
                $active = ($atts['default'] === $t->slug);
            ?>
                <button type="button" role="tab"
                        class="cg-tab <?php echo $active ? 'active' : ''; ?>"
                        data-slug="<?php echo $slug; ?>"
                        aria-selected="<?php echo $active ? 'true' : 'false'; ?>">
                    <?php echo esc_html($t->name); ?>
                </button>
            <?php endforeach; ?>
        </div>

        <!-- Панели вкладок -->
        <div class="cg-tabs-panels">
            <!-- Панель "Все" -->
            <?php
            $lightbox_all = array();
            foreach ($all_images_ids as $img_id) {
                $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                if ($full) $lightbox_all[] = $full;
            }
            $lightbox_all_json = htmlspecialchars(json_encode($lightbox_all), ENT_QUOTES, 'UTF-8');
            ?>
            <section class="cg-tab-panel <?php echo ($atts['default'] === 'all') ? 'active' : ''; ?>" data-slug="all" role="tabpanel" data-lightbox="<?php echo $lightbox_all_json; ?>">
                <div class="cg-all-images-grid">
                    <?php foreach ($all_images_ids as $img_id):
                        $thumb = wp_get_attachment_image_url($img_id, 'medium') ?: wp_get_attachment_image_url($img_id, 'thumbnail');
                        $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                        if (!$thumb) continue;
                    ?>
                        <div class="cg-image-item" data-full="<?php echo esc_url($full); ?>">
                            <img data-src="<?php echo esc_url($thumb); ?>" alt="">
                        </div>
                    <?php endforeach; ?>
                </div>
            </section>

            <!-- Панели категорий -->
            <?php foreach ($terms as $t):
                $slug = $t->slug;
                $term_id = $t->term_id;
                $images = get_term_meta($term_id, 'gallery_images', true) ?: array();
                if (!is_array($images)) $images = array();

                $cat_order = $atts['order'];
                if ($cat_order === 'random') {
                    shuffle($images);
                } elseif ($cat_order === 'date_asc') {
                    usort($images, function($a, $b) {
                        return strtotime(get_post_field('post_date', $a)) - strtotime(get_post_field('post_date', $b));
                    });
                } else {
                    usort($images, function($a, $b) {
                        return strtotime(get_post_field('post_date', $b)) - strtotime(get_post_field('post_date', $a));
                    });
                }

                $lightbox_cat = array();
                foreach ($images as $img_id) {
                    $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                    if ($full) $lightbox_cat[] = $full;
                }
                $lightbox_cat_json = htmlspecialchars(json_encode($lightbox_cat), ENT_QUOTES, 'UTF-8');
            ?>
                <section class="cg-tab-panel" data-slug="<?php echo esc_attr($slug); ?>" role="tabpanel" data-lightbox="<?php echo $lightbox_cat_json; ?>">
                    <div class="cg-all-images-grid">
                        <?php foreach ($images as $img_id):
                            $thumb = wp_get_attachment_image_url($img_id, 'medium') ?: wp_get_attachment_image_url($img_id, 'thumbnail');
                            $full = wp_get_attachment_image_url($img_id, 'large') ?: wp_get_attachment_image_url($img_id, 'full');
                            if (!$thumb) continue;
                        ?>
                            <div class="cg-image-item" data-full="<?php echo esc_url($full); ?>">
                                <img data-src="<?php echo esc_url($thumb); ?>" alt="">
                            </div>
                        <?php endforeach; ?>
                    </div>
                </section>
            <?php endforeach; ?>
        </div><!-- .cg-tabs-panels -->

        <!-- Лайтбокс -->
        <div class="<?php echo esc_attr($lightbox_class); ?>" id="<?php echo esc_attr($uid); ?>-lightbox">
            <div class="cg-lightbox-overlay"></div>
            <div class="cg-lightbox-content">
                <img src="" alt="" class="cg-lightbox-img">
                <button class="cg-lightbox-close">&times;</button>
                <button class="cg-lightbox-prev">&#10094;</button>
                <button class="cg-lightbox-next">&#10095;</button>
            </div>
        </div>
    </div>

    <style>
        /* Стили для нового шорткода */
        #<?php echo esc_attr($uid); ?>.cg-all-tabs-wrap {
            max-width: 1400px;
            margin: 0 auto;
            font-family: 'Roboto', sans-serif;
            color: #222;
            position: relative;
        }
        #<?php echo esc_attr($uid); ?> .cg-section-title {
            text-align: center;
            margin: 0 0 24px;
            font-size: 36px;
            font-weight: 600;
            text-transform: uppercase;
            color: #262626;
            letter-spacing: 0.02em;
        }
        /* Табы */
        #<?php echo esc_attr($uid); ?> .cg-tabs-nav {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            background: #f2f2f2;
            border-radius: 6px;
            margin-bottom: 20px;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab {
            flex: 1 1 auto;
            border: none;
            background: transparent;
            padding: 18px 10px;
            cursor: pointer;
            font-family: 'Roboto', sans-serif;
            font-weight: 300;
            font-size: 22px;
            color: #444;
            border-radius: 4px;
            transition: background .12s, color .12s;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab.active {
            background: #f07f00;
            color: #fff;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab:focus {
            outline: 3px solid rgba(240,127,0,0.18);
        }
        #<?php echo esc_attr($uid); ?> .cg-tab-panel {
            padding: 16px 0;
        }
        #<?php echo esc_attr($uid); ?> .cg-tab-panel:not(.active) {
            display: none;
        }

        /* Сетка изображений — по умолчанию квадратная (мобильная) */
        #<?php echo esc_attr($uid); ?> .cg-all-images-grid {
            display: grid;
            gap: 16px;
        }
        #<?php echo esc_attr($uid); ?> .cg-image-item {
            aspect-ratio: 1 / 1;
            background: #f6f6f6;
            border-radius: 6px;
            overflow: hidden;
            cursor: pointer;
        }
        #<?php echo esc_attr($uid); ?> .cg-image-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        }

        /* Адаптивность колонок и пропорции */
        @media (min-width: 1023px) {
            #<?php echo esc_attr($uid); ?> .cg-all-images-grid {
                grid-template-columns: repeat(4, 1fr);
            }
            #<?php echo esc_attr($uid); ?> .cg-image-item {
                aspect-ratio: 4 / 3; /* на десктопе 4:3 */
            }
        }
        @media (min-width: 601px) and (max-width: 1022px) {
            #<?php echo esc_attr($uid); ?> .cg-all-images-grid {
                grid-template-columns: repeat(3, 1fr);
            }
            #<?php echo esc_attr($uid); ?> .cg-image-item {
                aspect-ratio: 1 / 1; /* на планшетах квадрат */
            }
        }
        @media (max-width: 600px) {
            #<?php echo esc_attr($uid); ?> .cg-all-images-grid {
                grid-template-columns: repeat(2, 1fr);
                gap: 8px;
            }
            #<?php echo esc_attr($uid); ?> .cg-image-item {
                aspect-ratio: 1 / 1; /* на телефонах квадрат */
            }
        }

        /* Эффект блеска (уменьшенная задержка) */
        #<?php echo esc_attr($uid); ?> .cg-image-item.shine::after {
            content: "";
            position: absolute;
            top: 0;
            left: -150%;
            width: 50%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
            transform: skewX(-20deg);
            animation: shine-animation 0.33s forwards;
            pointer-events: none;
            z-index: 3;
        }
        #<?php echo esc_attr($uid); ?> .cg-image-item {
            position: relative;
            overflow: hidden;
        }
        @keyframes shine-animation {
            0% { left: -150%; }
            100% { left: 100%; }
        }

        /* Лайтбокс (адаптирован из первого шорткода) */
        #<?php echo esc_attr($uid); ?> .cg-lightbox {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 9999;
            align-items: center;
            justify-content: center;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox.active {
            display: flex;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.9);
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-content {
            position: relative;
            max-width: 90vw;
            max-height: 90vh;
            z-index: 10000;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-img {
            max-width: 100%;
            max-height: 90vh;
            display: block;
            margin: 0 auto;
            border-radius: 4px;
        }
        /* Стили для режима 4:3 в лайтбоксе — фиксированный размер контейнера */
        #<?php echo esc_attr($uid); ?> .cg-lightbox.cg-lightbox-4-3 .cg-lightbox-content {
            width: 90vw;
            height: calc(90vw * 3 / 4);
            max-width: calc(90vh * 4 / 3);
            max-height: 90vh;
            aspect-ratio: 4 / 3;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox.cg-lightbox-4-3 .cg-lightbox-img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-close {
            position: absolute;
            top: -40px;
            right: 0;
            background: none;
            border: none;
            color: white;
            font-size: 40px;
            cursor: pointer;
            padding: 0 10px;
            line-height: 1;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev,
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            background: rgba(255,255,255,0.2);
            border: none;
            color: white;
            font-size: 40px;
            cursor: pointer;
            padding: 10px 15px;
            border-radius: 4px;
            transition: background 0.2s;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev:hover,
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next:hover {
            background: rgba(255,255,255,0.4);
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-prev {
            left: 10px;
        }
        #<?php echo esc_attr($uid); ?> .cg-lightbox-next {
            right: 10px;
        }
    </style>

    <script>
    (function(){
        var root = document.getElementById('<?php echo esc_js($uid); ?>');
        if (!root) return;

        var tabs = Array.prototype.slice.call(root.querySelectorAll('.cg-tab'));
        var panels = Array.prototype.slice.call(root.querySelectorAll('.cg-tab-panel'));

        var currentLoading = {
            panel: null,
            timeoutIds: [],
            stop: false
        };

        function stopLoading() {
            currentLoading.stop = true;
            currentLoading.timeoutIds.forEach(id => clearTimeout(id));
            currentLoading.timeoutIds = [];
        }

        function loadImagesSequentially(panel) {
            stopLoading();
            currentLoading.panel = panel;
            currentLoading.stop = false;

            var images = panel.querySelectorAll('.cg-image-item img');
            var index = 0;
            var total = images.length;

            function loadNext() {
                if (currentLoading.stop || index >= total) return;
                var img = images[index];
                var container = img.closest('.cg-image-item');
                if (!container) {
                    index++;
                    loadNext();
                    return;
                }
                var dataSrc = img.getAttribute('data-src');
                if (!dataSrc) {
                    index++;
                    loadNext();
                    return;
                }

                function afterLoad() {
                    if (currentLoading.stop) return;
                    container.classList.add('shine');
                    var timeoutId = setTimeout(function() {
                        container.classList.remove('shine');
                        index++;
                        loadNext();
                    }, 333);
                    currentLoading.timeoutIds.push(timeoutId);
                }

                if (img.complete && img.naturalHeight > 0) {
                    afterLoad();
                } else {
                    img.onload = function() {
                        img.onload = null;
                        img.onerror = null;
                        afterLoad();
                    };
                    img.onerror = function() {
                        img.onerror = null;
                        index++;
                        loadNext();
                    };
                    img.src = dataSrc;
                }
            }

            loadNext();
        }

        function activate(slug) {
            tabs.forEach(function(tb) {
                var is = tb.dataset.slug === slug;
                tb.classList.toggle('active', is);
                tb.setAttribute('aria-selected', is ? 'true' : 'false');
            });
            panels.forEach(function(p) {
                var active = (p.dataset.slug === slug);
                p.style.display = active ? 'block' : 'none';
                if (active) {
                    loadImagesSequentially(p);
                }
            });
            sessionStorage.setItem('cg_active_tab_<?php echo esc_js($uid); ?>', slug);
        }

        var stored = sessionStorage.getItem('cg_active_tab_<?php echo esc_js($uid); ?>');
        if (stored && tabs.some(tb => tb.dataset.slug === stored)) {
            activate(stored);
        } else {
            var start = '<?php echo esc_js($atts['default']); ?>';
            if (!start && tabs.length) start = tabs[0].dataset.slug;
            if (start) activate(start);
        }

        tabs.forEach(function(tb) {
            tb.addEventListener('click', function() { activate(tb.dataset.slug); });
            tb.addEventListener('keydown', function(e) {
                if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                    e.preventDefault();
                    var idx = tabs.indexOf(tb);
                    var next = (e.key === 'ArrowRight') ? (idx + 1) : (idx - 1);
                    if (next < 0) next = tabs.length - 1;
                    if (next >= tabs.length) next = 0;
                    tabs[next].focus();
                    tabs[next].click();
                }
            });
        });

        // Лайтбокс
        var lightbox = root.querySelector('.cg-lightbox');
        var lightboxImg = lightbox.querySelector('.cg-lightbox-img');
        var lightboxClose = lightbox.querySelector('.cg-lightbox-close');
        var lightboxPrev = lightbox.querySelector('.cg-lightbox-prev');
        var lightboxNext = lightbox.querySelector('.cg-lightbox-next');
        var currentImages = [];
        var currentIndex = 0;

        function openLightbox(index) {
            if (!currentImages.length) return;
            lightbox.classList.add('active');
            document.body.style.overflow = 'hidden';
            showImage(index);
        }

        function showImage(index) {
            if (index < 0) index = currentImages.length - 1;
            if (index >= currentImages.length) index = 0;
            currentIndex = index;
            lightboxImg.src = currentImages[currentIndex];
        }

        function closeLightbox() {
            lightbox.classList.remove('active');
            document.body.style.overflow = '';
        }

        lightboxClose.addEventListener('click', closeLightbox);
        lightboxPrev.addEventListener('click', function() {
            showImage(currentIndex - 1);
        });
        lightboxNext.addEventListener('click', function() {
            showImage(currentIndex + 1);
        });
        lightbox.addEventListener('click', function(e) {
            if (e.target === lightbox || e.target.classList.contains('cg-lightbox-overlay')) {
                closeLightbox();
            }
        });

        document.addEventListener('keydown', function(e) {
            if (!lightbox.classList.contains('active')) return;
            if (e.key === 'Escape') {
                closeLightbox();
            } else if (e.key === 'ArrowLeft') {
                showImage(currentIndex - 1);
            } else if (e.key === 'ArrowRight') {
                showImage(currentIndex + 1);
            }
        });

        root.addEventListener('click', function(e) {
            var target = e.target.closest('.cg-image-item');
            if (!target) return;
            var panel = target.closest('.cg-tab-panel');
            if (!panel) return;
            var imagesData = panel.getAttribute('data-lightbox');
            if (!imagesData) return;
            try {
                currentImages = JSON.parse(imagesData);
            } catch (err) {
                return;
            }

            var fullUrl = target.getAttribute('data-full');
            if (!fullUrl) return;
            var idx = currentImages.indexOf(fullUrl);
            if (idx >= 0) {
                currentIndex = idx;
                openLightbox(currentIndex);
            }
        });
    })();
    </script>

    <?php
    return ob_get_clean();
});

// 8) Очистка мета при удалении вложения
add_action('delete_attachment', function($post_id){
    $terms = get_terms(array('taxonomy'=>'gallery_cat','hide_empty'=>false));
    foreach($terms as $t){
        $imgs = get_term_meta($t->term_id, 'gallery_images', true);
        if (empty($imgs) || !is_array($imgs)) continue;
        $new = array_diff($imgs, array($post_id));
        if (count($new) !== count($imgs)) update_term_meta($t->term_id, 'gallery_images', array_values($new));
    }
});
Install as an MU-plugin.
A 'Galleries' menu appears in admin: create categories, upload images, set descriptions and buttons.
Shortcode [gallery_tabs] – desktop version with text block, mobile – grid.
Shortcode [gallery_all_tabs] – 4×3 grid on desktop, 3×2 on tablet, 2×X on phone, with 'All' tab.
Lightbox supports 4:3 mode (enable in settings).
🧮

Stretch ceiling calculator

Interactive stretch ceiling cost calculator with texture selection, area, corners, lighting. Uses Shadow DOM, fully encapsulated.

<div id="stretch-calc-shadow"></div>

<script>
(function() {
  const host = document.getElementById('stretch-calc-shadow');
  const shadow = host.attachShadow({ mode: 'open' });

  shadow.innerHTML = `
    <style>
      * { box-sizing: border-box; margin: 0; padding: 0; }

      :host {
        --accent: #ff8a00;
        --accent-light: #ffb55a;
        --text: #222;
        --text-muted: #6b7280;
        --bg: #f5f8f9;
        --panel-bg: #ffffff;
        --border: #e5e9f0;
        --radius: 16px;
        --radius-sm: 4px;
        --font-family: 'Roboto', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        display: block;
        font-family: var(--font-family);
        color: var(--text);
        max-width: 1100px;
        margin: 0 auto;
        padding: 24px;
      }

      h2 {
        text-align: center;
        margin: 0 0 24px;
        font-size: 36px;
        font-weight: 600;
        text-transform: uppercase;
        color: #262626;
        letter-spacing: 0.02em;
      }

      .grid {
        display: grid;
        grid-template-columns: 280px 1fr 300px;
        grid-template-rows: auto auto;
        gap: 10px;
        overflow: hidden;
      }

      .panel { padding: 20px; background: transparent; border: none; box-shadow: none; }
      .panel:not(:last-child) { border-right: 1px solid var(--border); }
      .panel-left { grid-row: span 2; }

      .panel__title {
        font-size: 26px;
        font-weight: 600;
        margin-bottom: 18px;
        color: #1e293b;
        display: flex;
        align-items: center;
        gap: 8px;
      }
      .panel__title::before {
        content: '';
        width: 4px;
        height: 20px;
        background: var(--accent);
        border-radius: 4px;
      }

      .texture-list { display: flex; flex-direction: column; gap: 8px; }

      .texture-btn {
        width: 100%;
        padding: 12px 16px;
        border: 1px solid var(--border);
        background: white;
        border-radius: var(--radius-sm);
        text-align: left;
        cursor: pointer;
        transition: all 0.2s;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-weight: 300;
        font-size: 22px;
        line-height: 22px;
        color: #222;
      }
      .texture-btn:hover {
        border-color: var(--accent-light);
        background: #fffaf2;
      }
      .texture-btn.active {
        background: var(--accent);
        border-color: var(--accent);
        color: #fff;
      }
      .texture-price {
        font-size: 14px;
        opacity: 0.8;
        background: rgba(0,0,0,0.05);
        padding: 4px 8px;
        border-radius: 4px;
        font-weight: 300;
      }
      .texture-btn.active .texture-price { color: white; }

      .ceiling-price-block { margin-top: 32px; }
      .ceiling-price-block .panel__title { margin-bottom: 8px; }
      .ceiling-price-content { padding-left: 16px; }

      .ceiling-price-value {
        font-size: 34px;
        font-weight: 700;
        color: var(--accent);
        line-height: 1.2;
        position: relative;
        display: inline-block;
        overflow: hidden;
      }
      .ceiling-price-install {
        font-size: 22px;
        font-weight: 300;
        color: #434b53;
        margin-top: 4px;
      }

      .ceiling-price-value::before {
        content: '';
        position: absolute;
        top: 0;
        left: -75%;
        width: 50%;
        height: 100%;
        background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%);
        transform: skewX(-20deg);
        pointer-events: none;
      }
      .ceiling-price-value:hover::before,
      .ceiling-price-value.shine::before {
        animation: shine 2s ease-in-out;
      }

      @keyframes shine {
        0% { left: -75%; }
        50% { left: 125%; }
        100% { left: 125%; }
      }

      .param-group { margin-bottom: 20px; }
      .param-label {
        display: flex;
        justify-content: space-between;
        margin-bottom: 8px;
        font-weight: 300;
        font-size: 22px;
        color: #434b53;
      }

      .slider-row {
        display: flex;
        gap: 12px;
        align-items: center;
      }

      input[type="number"] {
        width: 71px;
        padding: 10px 12px;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        font-size: 18px;
        font-weight: 600;
        color: #262626;
        background: white;
        font-family: var(--font-family);
        -moz-appearance: textfield;
      }
      input[type="number"]::-webkit-inner-spin-button,
      input[type="number"]::-webkit-outer-spin-button {
        -webkit-appearance: none;
        margin: 0;
      }

      .range-wrapper {
        position: relative;
        flex: 1;
        height: 16px;
      }
      .range-track {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 16px;
        background: #dcdcdc;
        border-radius: 12px;
        border: 1px solid #BAB7B9;
        z-index: 1;
      }
      .range-progress {
        position: absolute;
        top: 0;
        left: 0;
        height: 16px;
        background: #FF8902;
        border-radius: 12px 0 0 12px;
        pointer-events: none;
        width: 0px;
        z-index: 2;
      }

      input[type="range"] {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 16px;
        background: transparent;
        -webkit-appearance: none;
        appearance: none;
        pointer-events: auto;
        z-index: 3;
        margin: 0;
      }
      input[type="range"]::-webkit-slider-runnable-track {
        background: transparent;
        border: none;
      }
      input[type="range"]::-moz-range-track {
        background: transparent;
        border: none;
      }
      input[type="range"]::-webkit-slider-thumb {
        -webkit-appearance: none;
        appearance: none;
        width: 18px;
        height: 18px;
        background-color: #dcdcdc;
        border: 1px solid #BAB7B9;
        border-radius: 50%;
        box-shadow: 0px 2px 2px rgba(0,0,0,0.25);
        margin-top: -2px;
        cursor: pointer;
      }
      input[type="range"]::-moz-range-thumb {
        width: 18px;
        height: 18px;
        background-color: #dcdcdc;
        border: 1px solid #BAB7B9;
        border-radius: 50%;
        box-shadow: 0px 2px 2px rgba(0,0,0,0.25);
        cursor: pointer;
      }

      .counter {
        display: flex;
        align-items: center;
        gap: 12px;
      }
      .counter-value {
        min-width: 60px;
        text-align: center;
        font-weight: 600;
        font-size: 18px;
        color: #262626;
      }
      .counter-value .unit { font-weight: 300; }

      .calc__plus, .calc__minus {
        background: unset;
        outline: unset;
        border: unset;
        padding: 0;
        border-radius: 3px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        transition: 0.3s;
        width: 37px;
        height: 37px;
      }
      .calc__plus {
        background-color: #ff8902;
        border: 1px solid #ff8902;
      }
      .calc__minus {
        background-color: #fff;
        border: 1px solid #BAB7B9;
      }
      .calc__plus svg path { fill: white; }
      .calc__minus svg path { fill: #D9D9D9; }
      .calc__minus:hover {
        background-color: #D9D9D9;
        border-color: #D9D9D9;
      }
      .calc__minus:hover svg path { fill: white; }
      .calc__plus:hover {
        background-color: #e67e00;
        border-color: #e67e00;
      }

      .callback-bottom {
        grid-column: 2 / span 2;
        grid-row: 2;
        padding: 20px;
        background: transparent;
        border-top: 1px solid var(--border);
        display: flex;
        flex-direction: column;
        align-items: flex-start;
      }
      .callback-container {
        max-width: 620px;
        width: 100%;
        margin: 0;
      }
      .callback-text {
        font-weight: 300;
        font-size: 22px;
        line-height: 1.4;
        color: var(--text);
        margin-bottom: 16px;
        text-align: left;
      }
      .callback-form {
        display: flex;
        flex-direction: column;
        gap: 16px;
      }
      .phone-row {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
      }
      .phone-input {
        flex: 1 1 50px;
        padding: 12px 16px;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        font-size: 16px;
        font-family: var(--font-family);
        background: white;
        min-width: 150px;
        font-weight: 500;
        letter-spacing: 0.3px;
      }
      .phone-input:focus {
        border-color: var(--accent);
        outline: none;
      }
      .btn-primary {
        background: var(--accent);
        color: white;
        border: none;
        padding: 12px 24px;
        border-radius: var(--radius-sm);
        font-weight: 600;
        font-size: 16px;
        cursor: pointer;
        transition: background 0.2s;
        white-space: nowrap;
      }
      .btn-primary:hover { background: #e67e00; }
      .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }

      .consent-text {
        font-size: 16px;
        color: var(--text-muted);
        font-weight: 300;
        line-height: 1.4;
      }

      .params-row {
        display: flex;
        gap: 20px;
        margin-bottom: 20px;
      }
      .params-row .param-group { flex: 1; margin-bottom: 0; }

      /* Мобильный блок цены */
      .mobile-price-panel {
        display: none;
        padding: 20px;
        background: transparent;
        border-top: 1px solid var(--border);
        border-bottom: 1px solid var(--border);
        margin: 10px 0;
      }
      .mobile-price-panel .ceiling-price-content {
        padding-left: 0;
      }
      .mobile-price-panel .panel__title {
        margin-bottom: 12px;
      }

      .sticky-price-bar {
        display: none;
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        background: white;
        box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
        padding: 12px 24px;
        z-index: 1000;
        justify-content: space-between;
        align-items: center;
        border-top: 1px solid var(--border);
        font-size: 22px;
        font-weight: 600;
        color: var(--text);
      }
      .sticky-price-bar .price-value {
        color: var(--accent);
        font-size: 28px;
        font-weight: 700;
      }
      .sticky-price-bar .price-label {
        font-weight: 300;
        color: var(--text-muted);
      }

      @media(max-width:900px){
        :host { padding: 16px; }
        .grid { 
          grid-template-columns: 1fr; 
          grid-template-rows: auto; 
        }
        .panel-left { grid-row: auto; }
        .panel-left .ceiling-price-block {
          display: none;
        }
        .callback-bottom { 
          grid-column: 1; 
          grid-row: auto;
          flex-direction: column; 
        }
        .panel:not(:last-child) {
          border-right: none;
          border-bottom: 1px solid var(--border);
        }
        .params-row { flex-direction: column; }
        .phone-row { flex-direction: column; }
        .btn-primary { width: 100%; }
        .mobile-price-panel { display: block; }
        .sticky-price-bar {
          display: flex;
          transform: translateY(100%);
          transition: transform 0.3s ease;
        }
        .sticky-price-bar.visible {
          transform: translateY(0);
        }
      }
    </style>

    <h2>Калькулятор натяжных потолков</h2>
    <div class="grid" id="app">
      <div class="panel panel-left">
        <div class="panel__title">Фактура полотна</div>
        <div class="texture-list" id="textureList"></div>
        <div class="ceiling-price-block" id="desktopPriceBlock">
          <div class="panel__title">Цена потолка</div>
          <div class="ceiling-price-content">
            <div class="ceiling-price-value" id="totalPriceLeft">0 руб.</div>
            <div class="ceiling-price-install">с установкой</div>
          </div>
        </div>
      </div>

      <div class="panel">
        <div class="panel__title">Параметры комнаты</div>
        <div class="param-group">
          <div class="param-label"><span>Площадь, м²</span></div>
          <div class="slider-row">
            <input type="number" id="areaInput" min="1" step="0.1" value="16">
            <div class="range-wrapper" id="rangeWrapper">
              <div class="range-track"></div>
              <div class="range-progress" id="areaProgress"></div>
              <input type="range" id="areaRange" min="1" max="200" step="0.1" value="16">
            </div>
          </div>
        </div>

        <div class="params-row">
          <div class="param-group">
            <div class="param-label"><span>Количество углов</span></div>
            <div class="counter">
              <button class="calc__minus" id="cornerMinus" type="button"> <svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.975h19.651v5.36H8.598v-5.36Z"></path></svg></button>
              <span class="counter-value" id="cornersValue">4</span>
              <button class="calc__plus" id="cornerPlus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.752h19.651v5.359H8.598v-5.36Z"></path><path d="M15.743 28.257V8.606h5.36v19.65h-5.36Z"></path></svg></button>
            </div>
          </div>

          <div class="param-group">
            <div class="param-label"><span>Переход уровня</span></div>
            <div class="counter">
              <button class="calc__minus" id="levelMinus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.975h19.651v5.36H8.598v-5.36Z"></path></svg></button>
              <span class="counter-value" id="levelMetersValue">0 <span class="unit">м.п.</span></span>
              <button class="calc__plus" id="levelPlus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.752h19.651v5.359H8.598v-5.36Z"></path><path d="M15.743 28.257V8.606h5.36v19.65h-5.36Z"></path></svg></button>
            </div>
          </div>
        </div>
      </div>

      <div class="panel">
        <div class="panel__title">Освещение</div>
        <div class="param-group">
          <div class="param-label">Люстры</div>
          <div class="counter">
            <button class="calc__minus" id="chMinus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.975h19.651v5.36H8.598v-5.36Z"></path></svg></button>
            <span class="counter-value" id="chandeliersValue">1</span>
            <button class="calc__plus" id="chPlus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.752h19.651v5.359H8.598v-5.36Z"></path><path d="M15.743 28.257V8.606h5.36v19.65h-5.36Z"></path></svg></button>
          </div>
        </div>

        <div class="param-group">
          <div class="param-label">Светильники</div>
          <div class="counter">
            <button class="calc__minus" id="lampMinus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.975h19.651v5.36H8.598v-5.36Z"></path></svg></button>
            <span class="counter-value" id="lampsValue">0</span>
            <button class="calc__plus" id="lampPlus" type="button"><svg viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><path d="M8.597 15.752h19.651v5.359H8.598v-5.36Z"></path><path d="M15.743 28.257V8.606h5.36v19.65h-5.36Z"></path></svg></button>
          </div>
        </div>
      </div>

      <div class="mobile-price-panel" id="mobilePricePanel">
        <div class="panel__title">Цена потолка</div>
        <div class="ceiling-price-content">
          <div class="ceiling-price-value" id="mobileTotalPrice">0 руб.</div>
          <div class="ceiling-price-install">с установкой</div>
        </div>
      </div>

      <div class="callback-bottom" id="callbackBottom">
        <div class="callback-container">
          <div class="callback-text">
            Чтобы узнать точную стоимость, запишитесь на бесплатный замер вашего потолка
          </div>
          <div class="callback-form">
            <div class="phone-row">
              <input type="tel" inputmode="tel" autocomplete="tel" class="phone-input" placeholder="+375 XX XXX-XX-XX" id="phoneInput">
              <button class="btn-primary" id="submitPhone">Записаться</button>
            </div>
            <div class="consent-text">
              Отправляя форму, я даю «Согласие на обработку персональных данных» и соглашаюсь с «Политикой обработки персональных данных»
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="sticky-price-bar" id="stickyPriceBar">
      <span class="price-label">Итого:</span>
      <span class="price-value" id="stickyPriceValue">от 0 руб.</span>
    </div>

    <!-- Скрытая форма для отправки -->
    <form id="callbackForm" style="display:none;" method="POST" action="https://get.belinfo.by/handler.php">
      <input type="hidden" name="site_key" value="fortuna-potolki">
      <input type="hidden" name="name" value="Заявка с калькулятора">
      <input type="hidden" name="page_url" id="pageUrl">
      <input type="hidden" name="referer" id="referer">
      <input type="hidden" name="utm_source" id="utmSource">
      <input type="hidden" name="utm_medium" id="utmMedium">
      <input type="hidden" name="utm_campaign" id="utmCampaign">
      <input type="hidden" name="utm_term" id="utmTerm">
      <input type="hidden" name="utm_content" id="utmContent">
      <input type="hidden" name="local_time" id="localTime">
      <input type="hidden" name="consent" value="on">
      <input type="hidden" name="phone" id="hiddenPhone">
    </form>
  `;

  // --- Цены и состояние ---
  const PRICES = {
    textures: { "Матовая": { per_m2: 20 }, "Сатин": { per_m2: 20 }, "Глянец": { per_m2: 18 } },
    light: { chandelier: 20, lamp: 15 },
    extras: { corner: 6, bypassPipe: 15, hiddenCornice: 35, levelTransition: 25 },
    currency: "руб."
  };

  const defaultState = { texture: "Матовая", area: 16, corners: 4, chandeliers: 1, lamps: 0, extras: { levelMeters: 0 } };
  let state = { ...defaultState };

  // Элементы DOM
  const textureList = shadow.getElementById('textureList');
  const totalPriceLeft = shadow.getElementById('totalPriceLeft');
  const mobileTotalPrice = shadow.getElementById('mobileTotalPrice');
  const stickyPriceValue = shadow.getElementById('stickyPriceValue');
  const areaInput = shadow.getElementById('areaInput');
  const areaRange = shadow.getElementById('areaRange');
  const areaProgress = shadow.getElementById('areaProgress');
  const rangeWrapper = shadow.getElementById('rangeWrapper');
  const cornersValue = shadow.getElementById('cornersValue');
  const chandeliersValue = shadow.getElementById('chandeliersValue');
  const lampsValue = shadow.getElementById('lampsValue');
  const levelMetersValue = shadow.getElementById('levelMetersValue');
  const levelMinus = shadow.getElementById('levelMinus');
  const levelPlus = shadow.getElementById('levelPlus');
  const cornerMinus = shadow.getElementById('cornerMinus');
  const cornerPlus = shadow.getElementById('cornerPlus');
  const chMinus = shadow.getElementById('chMinus');
  const chPlus = shadow.getElementById('chPlus');
  const lampMinus = shadow.getElementById('lampMinus');
  const lampPlus = shadow.getElementById('lampPlus');
  const phoneInput = shadow.getElementById('phoneInput');
  const submitBtn = shadow.getElementById('submitPhone');
  const stickyBar = shadow.getElementById('stickyPriceBar');
  const mobilePricePanel = shadow.getElementById('mobilePricePanel');
  const callbackBottom = shadow.getElementById('callbackBottom');
  const callbackForm = shadow.getElementById('callbackForm');
  const hiddenPhone = shadow.getElementById('hiddenPhone');

  const THUMB_WIDTH = 18;
  let prevTotal = null;

  function formatMoney(value) {
    return 'от ' + Math.round(value).toLocaleString('ru-RU') + ' ' + PRICES.currency;
  }

  function updateProgressWidth() {
    if (!areaRange || !areaProgress || !rangeWrapper) return;
    const wrapperWidth = rangeWrapper.offsetWidth;
    if (wrapperWidth === 0) return;
    const min = parseFloat(areaRange.min) || 1;
    const max = parseFloat(areaRange.max) || 200;
    const val = parseFloat(areaRange.value) || min;
    const maxThumbLeft = wrapperWidth - THUMB_WIDTH;
    const thumbLeft = ((val - min) / (max - min)) * maxThumbLeft;
    areaProgress.style.width = (thumbLeft + THUMB_WIDTH / 2) + 'px';
  }

  function updateRangeProgress() { updateProgressWidth(); }

  function updateUI() {
    shadow.querySelectorAll('.texture-btn').forEach(btn => {
      const textureName = btn.dataset.texture;
      btn.classList.toggle('active', textureName === state.texture);
    });

    areaInput.value = (Math.round(state.area * 10) % 10 === 0) ? Math.round(state.area) : state.area;
    areaRange.value = state.area;
    updateRangeProgress();

    cornersValue.textContent = state.corners;
    chandeliersValue.textContent = state.chandeliers;
    lampsValue.textContent = state.lamps;
    levelMetersValue.innerHTML = state.extras.levelMeters + ' <span class="unit">м.п.</span>';
  }

  function recalc() {
    const area = Number(state.area) || 0;
    const texture = PRICES.textures[state.texture] || { per_m2: 0 };
    const material = Math.round(area * (texture.per_m2 || 0));
    const light = Math.round(state.chandeliers * PRICES.light.chandelier + state.lamps * PRICES.light.lamp);
    const cornersExtra = state.corners > 4 ? (state.corners - 4) * PRICES.extras.corner : 0;
    const levelMetersVal = Number(state.extras.levelMeters) || 0;
    const extraSum = levelMetersVal * PRICES.extras.levelTransition;
    const total = material + light + cornersExtra + extraSum;

    const formatted = formatMoney(total);
    totalPriceLeft.textContent = formatted;
    mobileTotalPrice.textContent = formatted;
    stickyPriceValue.textContent = formatted;

    if (prevTotal === null || prevTotal !== total) {
      totalPriceLeft.classList.add('shine');
      mobileTotalPrice.classList.add('shine');
      setTimeout(() => {
        totalPriceLeft.classList.remove('shine');
        mobileTotalPrice.classList.remove('shine');
      }, 2100);
    }
    prevTotal = total;
  }

  function setStateAndUpdate(newState) {
    if (newState.extras) {
      state.extras = { ...state.extras, ...newState.extras };
      delete newState.extras;
    }
    Object.assign(state, newState);
    updateUI();
    recalc();
  }

  function renderTextures() {
    textureList.innerHTML = '';
    Object.keys(PRICES.textures).forEach(name => {
      const btn = document.createElement('button');
      btn.className = 'texture-btn';
      btn.dataset.texture = name;
      btn.type = 'button';
      btn.innerHTML = `<span>${name}</span><span class="texture-price">${PRICES.textures[name].per_m2} руб./м²</span>`;
      btn.addEventListener('click', () => setStateAndUpdate({ texture: name }));
      textureList.appendChild(btn);
    });
    updateUI();
  }

  // Привязка событий
  areaInput.addEventListener('input', (e) => {
    let val = parseFloat(e.target.value);
    if (isNaN(val) || val < 1) val = 1;
    if (val > 200) val = 200;
    val = Math.round(val * 10) / 10;
    setStateAndUpdate({ area: val });
  });

  areaRange.addEventListener('input', (e) => {
    let val = parseFloat(e.target.value);
    setStateAndUpdate({ area: val });
  });

  cornerMinus.addEventListener('click', () => setStateAndUpdate({ corners: Math.max(0, state.corners - 1) }));
  cornerPlus.addEventListener('click', () => setStateAndUpdate({ corners: state.corners + 1 }));
  chMinus.addEventListener('click', () => setStateAndUpdate({ chandeliers: Math.max(0, state.chandeliers - 1) }));
  chPlus.addEventListener('click', () => setStateAndUpdate({ chandeliers: state.chandeliers + 1 }));
  lampMinus.addEventListener('click', () => setStateAndUpdate({ lamps: Math.max(0, state.lamps - 1) }));
  lampPlus.addEventListener('click', () => setStateAndUpdate({ lamps: state.lamps + 1 }));
  levelMinus.addEventListener('click', () => {
    const newVal = Math.max(0, state.extras.levelMeters - 1);
    setStateAndUpdate({ extras: { levelMeters: newVal } });
  });
  levelPlus.addEventListener('click', () => {
    const newVal = state.extras.levelMeters + 1;
    setStateAndUpdate({ extras: { levelMeters: newVal } });
  });

  window.addEventListener('resize', updateRangeProgress);

  renderTextures();
  updateUI();
  recalc();

  // --- Логика для sticky-панели ---
  let isMobile = window.matchMedia('(max-width: 900px)').matches;
  let observers = [];

  function initStickyBehavior() {
    observers.forEach(obs => obs.disconnect());
    observers = [];
    if (!isMobile) {
      stickyBar.classList.remove('visible');
      return;
    }
    const pricePanel = mobilePricePanel;
    const formPanel = callbackBottom;
    const calcElement = host;

    function isAnyTargetVisible() {
      const priceVisible = pricePanel.getBoundingClientRect().top < window.innerHeight && pricePanel.getBoundingClientRect().bottom > 0;
      const formVisible = formPanel.getBoundingClientRect().top < window.innerHeight && formPanel.getBoundingClientRect().bottom > 0;
      return priceVisible || formVisible;
    }

    const calcObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          if (!isAnyTargetVisible()) {
            stickyBar.classList.add('visible');
          } else {
            stickyBar.classList.remove('visible');
          }
        } else {
          stickyBar.classList.remove('visible');
        }
      });
    }, { threshold: 0 });

    const priceObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          stickyBar.classList.remove('visible');
        } else {
          const formVisible = formPanel.getBoundingClientRect().top < window.innerHeight && formPanel.getBoundingClientRect().bottom > 0;
          const calcVisible = calcElement.getBoundingClientRect().top < window.innerHeight && calcElement.getBoundingClientRect().bottom > 0;
          if (calcVisible && !formVisible) {
            stickyBar.classList.add('visible');
          }
        }
      });
    }, { threshold: 0 });

    const formObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          stickyBar.classList.remove('visible');
        } else {
          const priceVisible = pricePanel.getBoundingClientRect().top < window.innerHeight && pricePanel.getBoundingClientRect().bottom > 0;
          const calcVisible = calcElement.getBoundingClientRect().top < window.innerHeight && calcElement.getBoundingClientRect().bottom > 0;
          if (calcVisible && !priceVisible) {
            stickyBar.classList.add('visible');
          }
        }
      });
    }, { threshold: 0 });

    calcObserver.observe(calcElement);
    priceObserver.observe(pricePanel);
    formObserver.observe(formPanel);
    observers = [calcObserver, priceObserver, formObserver];
  }

  const mediaQuery = window.matchMedia('(max-width: 900px)');
  function handleMediaChange(e) {
    isMobile = e.matches;
    initStickyBehavior();
  }
  mediaQuery.addListener(handleMediaChange);
  initStickyBehavior();

  // --- Телефон и валидация ---
  function formatPhoneNumber(raw) {
    if (!raw) return '';
    let cleaned = raw.replace(/[^\d+]/g, '');
    if (cleaned.indexOf('+') > 0) cleaned = '+' + cleaned.replace(/\+/g, '');
    const hasPlus = cleaned.startsWith('+');
    let digits = cleaned.replace(/\D/g, '');

    if (digits.length === 11 && digits.startsWith('8')) {
      if (digits.startsWith('80')) {
        digits = '375' + digits.slice(2);
      } else {
        digits = '375' + digits.slice(1);
      }
    }

    if (digits.length === 0) return hasPlus ? '+' : '';

    if (digits.length === 12 && digits.startsWith('375')) {
      const part0 = digits.slice(0, 3);
      const part1 = digits.slice(3, 5);
      const part2 = digits.slice(5, 8);
      const part3 = digits.slice(8, 10);
      const part4 = digits.slice(10, 12);
      return (hasPlus ? '+' : '+') + part0 + ' ' + part1 + ' ' + part2 + '-' + part3 + '-' + part4;
    }

    if (digits.length <= 3) return (hasPlus ? '+' : '') + digits;
    if (digits.length <= 5) return (hasPlus ? '+' : '') + digits.slice(0, 3) + ' ' + digits.slice(3);
    if (digits.length <= 8) return (hasPlus ? '+' : '') + digits.slice(0, 3) + ' ' + digits.slice(3, 5) + ' ' + digits.slice(5);
    if (digits.length <= 10) return (hasPlus ? '+' : '') + digits.slice(0, 3) + ' ' + digits.slice(3, 5) + ' ' + digits.slice(5, 8) + '-' + digits.slice(8);
    return (hasPlus ? '+' : '') + digits.slice(0, 3) + ' ' + digits.slice(3, 5) + ' ' + digits.slice(5, 8) + '-' + digits.slice(8, 10) + '-' + digits.slice(10, 12);
  }

  phoneInput.addEventListener('input', function() {
    const cursorPos = this.selectionStart;
    const prevLen = this.value.length;
    this.value = formatPhoneNumber(this.value);
    const newLen = this.value.length;
    const diff = newLen - prevLen;
    try {
      this.setSelectionRange(cursorPos + Math.max(0, diff), cursorPos + Math.max(0, diff));
    } catch (e) {}
  });

  function isValidPhone(phone) {
    const digits = phone.replace(/\D/g, '');
    return digits.length >= 6;
  }

  function fillUtmFields() {
    shadow.getElementById('pageUrl').value = window.location.href;
    shadow.getElementById('referer').value = document.referrer || '';
    const params = new URLSearchParams(window.location.search);
    shadow.getElementById('utmSource').value = params.get('utm_source') || '';
    shadow.getElementById('utmMedium').value = params.get('utm_medium') || '';
    shadow.getElementById('utmCampaign').value = params.get('utm_campaign') || '';
    shadow.getElementById('utmTerm').value = params.get('utm_term') || '';
    shadow.getElementById('utmContent').value = params.get('utm_content') || '';
    shadow.getElementById('localTime').value = new Date().toLocaleString('ru-RU', { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, hour12: false });
  }

  let isSubmitting = false;

  submitBtn.addEventListener('click', async function() {
    if (isSubmitting) return;

    const phone = phoneInput.value.trim();
    if (!isValidPhone(phone)) {
      alert('Введите корректный номер телефона (минимум 6 цифр)');
      phoneInput.focus();
      return;
    }

    isSubmitting = true;
    submitBtn.disabled = true;
    const originalText = submitBtn.textContent;
    submitBtn.textContent = 'Отправка...';

    fillUtmFields();
    hiddenPhone.value = phone;

    const formData = new FormData(callbackForm);
    console.log('Отправляемые данные:');
    for (let [key, value] of formData.entries()) {
      console.log(key, value);
    }

    try {
      const response = await fetch(callbackForm.action, {
        method: 'POST',
        body: formData
      });

      if (!response.ok) {
        if (response.status === 429) {
          throw new Error('Слишком много запросов. Подождите минуту.');
        } else {
          const text = await response.text();
          console.error('Ответ сервера:', text);
          throw new Error(`Ошибка ${response.status}: ${text}`);
        }
      }

      const result = await response.json();
      if (result.success) {
        alert('Спасибо! Ваша заявка отправлена. Мы свяжемся с вами в ближайшее время.');
        phoneInput.value = '';
      } else {
        throw new Error(result.message || 'Неизвестная ошибка сервера');
      }
    } catch (error) {
      alert(`Не удалось отправить заявку. ${error.message}`);
      console.error(error);
    } finally {
      isSubmitting = false;
      submitBtn.disabled = false;
      submitBtn.textContent = originalText;
    }
  });

})();
</script>
Insert the code anywhere on a page (HTML block or template). The calculator mounts itself via Shadow DOM into a container with id="stretch-calc-shadow".
Dependencies: Roboto font (load separately).
The form submits to https://get.belinfo.by/handler.php with site key 'fortuna-potolki'. Change if needed.
📋

News shortcode (Shadow DOM)

Shortcode [custom_news] displays an 'Interesting articles' block with heading, date, animated underline and 'ALL NEWS' link. Uses Shadow DOM.

<?php
/**
 * Шорткод [custom_news] – блок новостей в стиле калькулятора с заголовком
 * 
 * Атрибуты:
 * @param int    $posts_per_page   количество постов (по умолчанию 4)
 * @param string $category         ярлык категории (по умолчанию 'poleznoe')
 * 
 * @return string HTML с Shadow DOM
 */
function custom_news_shortcode($atts) {
    $atts = shortcode_atts([
        'posts_per_page' => 4,
        'category'       => 'poleznoe'
    ], $atts);

    // Запрос постов
    $query = new WP_Query([
        'post_type'      => 'post',
        'posts_per_page' => intval($atts['posts_per_page']),
        'category_name'  => sanitize_text_field($atts['category'])
    ]);

    if (!$query->have_posts()) {
        return '<p>' . esc_html__('Нет новостей', 'your-text-domain') . '</p>';
    }

    // Ссылка "ПОСМОТРЕТЬ ВСЕ СОВЕТЫ" ведёт на /sovety/
    $all_news_url = '/sovety/';

    $uniqid = 'news-block-' . uniqid();

    // Формируем внутренний HTML (будет вставлен в Shadow DOM)
    ob_start();
    ?>
    <style>
        /* ===== Переменные из калькулятора ===== */
        :host {
            --accent: #ff8a00;
            --accent-light: #ffb55a;
            --text: #222;
            --text-muted: #6b7280;
            --bg: #f5f8f9;
            --panel-bg: #ffffff;
            --border: #e5e9f0;
            --radius: 8px;
            --radius-sm: 4px;
            --font-family: 'Roboto', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            
            display: block;
            font-family: var(--font-family);
            color: var(--text);
            max-width: 1400px;
            margin: 0 auto;
            padding: 24px;
        }

        /* ===== Стили заголовка ===== */
        .news-heading {
            text-align: center;
            margin: 0 0 24px;
            font-size: 36px;
            font-weight: 600;
            text-transform: uppercase;
            color: #262626;
            letter-spacing: 0.02em;
        }

        /* ===== Сетка новостей ===== */
        .news-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 20px;
        }

        /* Карточка */
        .news-item {
            position: relative;
            overflow: hidden;
            background: var(--panel-bg);
            border: 1px solid var(--border);
            border-radius: var(--radius);
            transition: box-shadow 0.2s ease;
            display: flex;
            flex-direction: column;
            height: 100%;
        }
        .news-item:hover {
            box-shadow: 0 8px 20px rgba(0,0,0,0.08);
        }

        /* Эффект блеска при наведении (как в калькуляторе) */
        .news-item::before {
            content: '';
            position: absolute;
            top: 0;
            left: -150%;
            width: 200%;
            height: 100%;
            background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.4) 50%, rgba(255,255,255,0) 100%);
            transform: skewX(-20deg);
            pointer-events: none;
            z-index: 2;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .news-item:hover::before {
            opacity: 1;
            animation: shine 1.5s ease-in-out;
        }

        @keyframes shine {
            0% { left: -150%; }
            100% { left: 100%; }
        }

        /* Миниатюра */
        .news-thumb {
            position: relative;
            aspect-ratio: 400/240;
            overflow: hidden;
        }
        .news-thumb img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        .news-date {
            position: absolute;
            bottom: 15px;
            right: 15px;
            background: var(--accent);
            color: #fff;
            text-align: center;
            padding: 8px 12px;
            font-weight: 700;
            line-height: 1.2;
            border-radius: var(--radius-sm);
            z-index: 3;
        }
        .news-day {
            font-size: 18px;
            display: block;
        }
        .news-month {
            font-size: 12px;
            text-transform: uppercase;
            display: block;
        }

        /* Контент */
        .news-content {
            padding: 20px;
            flex: 1;
            display: flex;
            flex-direction: column;
            position: relative;
            z-index: 3;
        }

        .news-title {
            font-size: 20px;
            font-weight: 600;
            line-height: 1.4;
            margin-bottom: 15px;
        }
        .title-inner {
            position: relative;
            display: inline-block;
        }
        .title-inner::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 0;
            height: 2px;
            background-color: var(--accent);
            transition: width 1.4s ease;
        }
        .news-title-link:hover .title-inner::after {
            width: 100%;
        }
        .news-title-link {
            text-decoration: none;
            color: inherit;
        }

        /* Ссылка «Читать далее» */
        .news-read-more {
            position: relative;
            display: inline-flex;
            align-items: center;
            font-weight: 600;
            font-size: 14px;
            text-decoration: none;
            color: inherit;
            background: transparent;
            border: none;
            padding: 0;
            cursor: pointer;
            transition: color 0.2s;
            margin-top: auto;
            z-index: 3;
        }
        .news-read-text-wrapper {
            position: relative;
            display: inline-block;
        }
        .news-read-text {
            position: relative;
            display: inline-block;
            transition: color 0.3s ease;
        }
        .news-read-text::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 0;
            height: 1px;
            background-color: var(--accent);
            transition: width 0.7s ease;
        }
        .news-read-more:hover .news-read-text::after {
            width: 100%;
        }
        .news-read-text.hide-underline::after {
            width: 0 !important;
            transition: none !important;
        }
        .news-read-arrow {
            margin-left: 8px;
            transition: transform 0.3s ease, color 0.3s ease;
        }
        .news-read-more:hover .news-read-arrow {
            transform: translateX(3px);
            color: var(--accent);
        }
        .news-read-more:hover .news-read-text {
            color: var(--accent);
        }

        /* Блок «ПОСМОТРЕТЬ ВСЕ СОВЕТЫ» */
        .news-all-container {
            text-align: center;
            margin-top: 40px;
        }
        .news-all-link {
            display: inline-flex;
            align-items: center;
            font-size: 22px;
            font-weight: 500;
            text-transform: uppercase;
            color: var(--text);
            text-decoration: none;
            position: relative;
            padding-bottom: 6px;
            transition: color 0.5s ease 0.7s;
        }
        .news-all-text {
            position: relative;
            transition: color 0.3s ease;
        }
        .news-all-text::after {
            content: '';
            position: absolute;
            bottom: -6px;
            left: 0;
            width: 100%;
            height: 1px;
            background-color: var(--accent);
            transform-origin: left;
            transition: transform 0.7s ease;
            transform: scaleX(1);
        }
        .news-all-link:hover .news-all-text::after {
            transform-origin: right;
            transform: scaleX(0);
        }
        .news-all-arrow {
            margin-left: 10px;
            opacity: 0;
            width: 0;
            overflow: hidden;
            transition: opacity 0.5s ease, width 0.5s ease;
            color: var(--accent);
            font-size: 22px;
            font-weight: 500;
            line-height: 1;
        }
        .news-all-arrow::before {
            content: '>';
        }
        .news-all-link:hover .news-all-arrow {
            opacity: 1;
            width: 20px;
            transition-delay: 0.7s;
        }
        .news-all-link:hover {
            color: var(--accent);
            transition-delay: 0s;
        }

        /* Адаптивность */
        @media (max-width: 1024px) {
            .news-grid {
                grid-template-columns: repeat(2, 1fr);
            }
        }
        @media (max-width: 600px) {
            .news-heading {
                font-size: 28px;
                margin-bottom: 20px;
            }
            .news-grid {
                grid-template-columns: 1fr;
            }
            .news-thumb {
                aspect-ratio: 400/240;
            }
            .news-all-link {
                font-size: 18px;
            }
            .news-all-arrow {
                font-size: 18px;
            }
            .news-title {
                font-size: 18px;
            }
        }
    </style>

    <!-- Добавленный заголовок -->
    <h2 class="news-heading">Интересные статьи о натяжных потолках</h2>

    <div class="news-block-final">
        <div class="news-grid">
            <?php while ($query->have_posts()) : $query->the_post(); 
                $title     = esc_html(get_the_title());
                $permalink = esc_url(get_permalink());
                $day       = esc_html(get_the_date('d'));
                $month     = esc_html(get_the_date('M'));
                $thumb_url = get_the_post_thumbnail_url(get_the_ID(), 'medium');
            ?>
            <article class="news-item">
                <div class="news-thumb">
                    <a href="<?php echo $permalink; ?>">
                        <?php if ($thumb_url) : ?>
                            <img src="<?php echo esc_url($thumb_url); ?>" alt="<?php echo $title; ?>" width="400" height="240">
                        <?php else : ?>
                            <img src="/wp-content/uploads/2025/09/400x240.avif" alt="<?php echo $title; ?>" width="400" height="240">
                        <?php endif; ?>
                        <div class="news-date">
                            <span class="news-day"><?php echo $day; ?></span>
                            <span class="news-month"><?php echo $month; ?></span>
                        </div>
                    </a>
                </div>
                <div class="news-content">
                    <a href="<?php echo $permalink; ?>" class="news-title-link">
                        <h3 class="news-title"><span class="title-inner"><?php echo $title; ?></span></h3>
                    </a>
                    <a href="<?php echo $permalink; ?>" class="news-read-more">
                        <span class="news-read-text-wrapper">
                            <span class="news-read-text">Читать далее</span>
                        </span>
                        <span class="news-read-arrow">→</span>
                    </a>
                </div>
            </article>
            <?php endwhile; wp_reset_postdata(); ?>
        </div>

        <div class="news-all-container">
            <a href="<?php echo esc_url($all_news_url); ?>" class="news-all-link">
                <span class="news-all-text">ПОСМОТРЕТЬ ВСЕ СОВЕТЫ</span>
                <span class="news-all-arrow"></span>
            </a>
        </div>
    </div>
    <?php
    $inner_html = ob_get_clean();

    // Внешний контейнер + скрипт для Shadow DOM и анимаций
    $output = '<div id="' . $uniqid . '"></div>';
    $output .= '<script>
        (function() {
            var host = document.getElementById("' . $uniqid . '");
            if (!host) return;
            var shadow = host.attachShadow({ mode: "open" });
            shadow.innerHTML = ' . json_encode($inner_html) . ';

            // Анимация для ссылок "Читать далее"
            var links = shadow.querySelectorAll(".news-read-more");
            var duration = 700;

            links.forEach(function(link) {
                var text = link.querySelector(".news-read-text");
                var arrow = link.querySelector(".news-read-arrow");
                var underlineTimer = null;
                var arrowChangeTimer = null;

                link.addEventListener("mouseenter", function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove("hide-underline");
                    arrow.textContent = "→";
                    arrow.style.color = "";
                    text.style.color = "";

                    underlineTimer = setTimeout(function() {
                        text.classList.add("hide-underline");
                    }, duration - 50);

                    arrowChangeTimer = setTimeout(function() {
                        arrow.textContent = ">";
                        arrow.style.color = "var(--accent, #ff8a00)";
                        text.style.color = "var(--accent, #ff8a00)";
                    }, duration);
                });

                link.addEventListener("mouseleave", function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove("hide-underline");
                    arrow.textContent = "→";
                    arrow.style.color = "";
                    text.style.color = "";
                });

                link.addEventListener("blur", function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove("hide-underline");
                    arrow.textContent = "→";
                    arrow.style.color = "";
                    text.style.color = "";
                });
            });
        })();
    </script>';

    return $output;
}
add_shortcode('custom_news', 'custom_news_shortcode');
Add the code to functions.php.
Use shortcode [custom_news posts_per_page="4" category="poleznoe"].
Styles and scripts are inline, Shadow DOM isolates styles.
Thumbnails: 400×240, a placeholder /wp-content/uploads/2025/09/400x240.avif is used for posts without image.
📋

News shortcode (classic)

Shortcode [custom_news] displays a news block in green tones with date, title, excerpt, animated 'Read more' link and 'ALL NEWS' button. No Shadow DOM.

<?php
/**
 * Шорткод [custom_news] – классический блок новостей (без Shadow DOM)
 * Использование: [custom_news posts_per_page="4" category="poleznoe"]
 * Стили и скрипты встроены прямо в шорткод.
 */

function custom_news_classic_shortcode($atts) {
    $atts = shortcode_atts([
        'posts_per_page' => 4,
        'category'       => 'poleznoe'
    ], $atts);

    $query_args = [
        'post_type'      => 'post',
        'posts_per_page' => intval($atts['posts_per_page']),
        'category_name'  => sanitize_text_field($atts['category'])
    ];
    $query = new WP_Query($query_args);

    if (!$query->have_posts()) {
        return '<p>' . esc_html__('Нет новостей', 'your-text-domain') . '</p>';
    }

    // Ссылка на все новости (замените при необходимости)
    $all_news_url = '/sovety/';

    // Начинаем буферизацию
    ob_start();
    ?>
    <style>
        .news-block-final {
            --news-primary-color: #2c7f2e;
            --news-text-color: #2c3e50;
            --news-background: #fff;
            --news-shadow: 0 3px 10px rgba(0,0,0,0.1);
            font-family: 'Manrope', sans-serif;
            max-width: 100%;
            margin: 40px 0;
            padding: 0 20px;
            color: var(--news-text-color);
        }

        .news-block-final .news-grid {
            display: grid;
            grid-template-columns: repeat(4,1fr);
            gap: 20px;
        }

        .news-block-final .news-item {
            background: var(--news-background);
            overflow: hidden;
            box-shadow: var(--news-shadow);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
            display: flex;
            flex-direction: column;
            height: 100%;
        }
        .news-block-final .news-item:hover {
            transform: translateY(-5px);
            box-shadow: 0 8px 20px rgba(0,0,0,0.15);
        }

        .news-block-final .news-thumb {
            position: relative;
            aspect-ratio: 400/240;
            overflow: hidden;
        }
        .news-block-final .news-thumb img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        .news-block-final .news-date {
            position: absolute;
            bottom: 15px;
            right: 15px;
            background: var(--news-primary-color);
            color: #fff;
            text-align: center;
            padding: 8px 10px;
            font-weight: 700;
            line-height: 1.2;
            z-index: 2;
        }
        .news-block-final .news-day { font-size: 18px; display:block; }
        .news-block-final .news-month { font-size: 12px; text-transform: uppercase; display:block; }

        .news-block-final .news-content {
            padding: 20px;
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        .news-block-final .news-title { font-size: 18px; font-weight: 600; line-height: 1.4; margin-bottom: 15px; }
        .news-block-final .title-inner {
            position: relative;
            display: inline-block;
        }
        .news-block-final .title-inner::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 0;
            height: 2px;
            background-color: var(--news-primary-color);
            transition: width 1.4s ease;
        }
        .news-block-final .news-title-link:hover .title-inner::after {
            width: 100%;
        }
        .news-block-final .news-title-link { text-decoration: none; }

        .news-block-final .news-excerpt { font-size: 14px; color: #555; line-height: 1.5; margin-bottom: 20px; flex: 1; }

        .news-block-final .news-read-more {
            position: relative;
            display: inline-flex;
            align-items: center;
            font-weight: 600;
            text-decoration: none;
            font-size: 14px;
            margin-top: auto;
            color: inherit;
        }

        .news-block-final .news-read-text-wrapper {
            position: relative;
            display: inline-block;
        }

        .news-block-final .news-read-text {
            position: relative;
            display: inline-block;
            transition: color 0.3s ease;
        }

        .news-block-final .news-read-text::after {
            content: '';
            position: absolute;
            left: 0;
            bottom: 0;
            width: 0;
            height: 1px;
            background-color: var(--news-primary-color);
            transition: width 0.7s ease;
        }

        .news-block-final .news-read-more:hover .news-read-text::after {
            width: 100%;
        }

        .news-block-final .news-read-text.hide-underline::after {
            width: 0 !important;
            transition: none !important;
        }

        .news-block-final .news-read-arrow { 
            margin-left: 8px; 
            transition: transform 0.3s ease, color 0.3s ease; 
        }

        .news-block-final .news-read-more:hover .news-read-arrow {
            transform: translateX(3px);
            color: var(--news-primary-color);
        }

        .news-block-final .news-read-more:hover .news-read-text {
            color: var(--news-primary-color);
        }

        /* Стили для ссылки "КО ВСЕМ НОВОСТЯМ" */
        .news-block-final .news-all-container { 
            text-align: center; 
            margin-top: 40px;
        }

        .news-block-final .news-all-link {
            display: inline-flex;
            align-items: center;
            font-size: 22px;
            font-weight: 500;
            text-transform: uppercase;
            color: var(--news-text-color);
            text-decoration: none;
            position: relative;
            padding-bottom: 6px;
            transition: color 0.5s ease 0.7s;
        }

        .news-block-final .news-all-text {
            position: relative;
            transition: color 0.3s ease;
        }

        .news-block-final .news-all-text::after {
            content: '';
            position: absolute;
            bottom: -6px;
            left: 0;
            width: 100%;
            height: 1px;
            background-color: var(--news-primary-color);
            transform-origin: left;
            transition: transform 0.7s ease;
            transform: scaleX(1);
        }

        .news-block-final .news-all-link:hover .news-all-text::after {
            transform-origin: right;
            transform: scaleX(0);
        }

        .news-block-final .news-all-arrow {
            margin-left: 10px;
            opacity: 0;
            width: 0;
            overflow: hidden;
            transition: opacity 0.5s ease, width 0.5s ease;
            color: var(--news-primary-color);
            font-size: 22px;
            font-weight: 500;
            line-height: 1;
        }

        .news-block-final .news-all-link:hover .news-all-arrow {
            opacity: 1;
            width: 20px;
            transition-delay: 0.7s;
        }

        .news-block-final .news-all-arrow::before {
            content: '>';
        }

        .news-block-final .news-all-link:hover {
            color: var(--news-primary-color);
            transition-delay: 0s;
        }

        /* Адаптивность */
        @media (max-width:1024px) { .news-block-final .news-grid { grid-template-columns: repeat(2,1fr); } }
        @media (max-width:600px) {
            .news-block-final .news-grid { grid-template-columns: 1fr; }
            .news-block-final .news-thumb { aspect-ratio: 400/240; }
            .news-block-final .news-all-link { font-size: 18px; }
            .news-block-final .news-all-arrow { font-size: 18px; }
            .news-block-final .news-title { font-size: 16px; }
            .news-block-final .title-inner { word-break: break-word; white-space: normal; }
            .news-block-final .title-inner::after { height: 1.5px; }
        }
    </style>

    <div class="news-block-final">
        <div class="news-grid">
            <?php while ($query->have_posts()) : $query->the_post(); ?>
            <article class="news-item">
                <div class="news-thumb">
                    <a href="<?php echo esc_url(get_permalink()); ?>">
                        <?php
                        if (has_post_thumbnail()) {
                            the_post_thumbnail('medium', ['alt' => esc_attr(get_the_title()), 'width'=>400,'height'=>240]);
                        } else {
                            echo '<img src="/wp-content/uploads/2025/09/400x240.avif" alt="'.esc_attr(get_the_title()).'" width="400" height="240">';
                        }
                        ?>
                        <div class="news-date">
                            <span class="news-day"><?php echo esc_html(get_the_date('d')); ?></span>
                            <span class="news-month"><?php echo esc_html(get_the_date('M')); ?></span>
                        </div>
                    </a>
                </div>
                <div class="news-content">
                    <a href="<?php echo esc_url(get_permalink()); ?>" class="news-title-link">
                        <h3 class="news-title"><span class="title-inner"><?php the_title(); ?></span></h3>
                    </a>
                    <p class="news-excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 20)); ?></p>
                    <a href="<?php echo esc_url(get_permalink()); ?>" class="news-read-more">
                        <span class="news-read-text-wrapper">
                            <span class="news-read-text"><?php esc_html_e('Читать далее', 'your-text-domain'); ?></span>
                        </span>
                        <span class="news-read-arrow">→</span>
                    </a>
                </div>
            </article>
            <?php endwhile; ?>
        </div>

        <div class="news-all-container">
            <a href="<?php echo esc_url($all_news_url); ?>" class="news-all-link">
                <span class="news-all-text"><?php esc_html_e('КО ВСЕМ НОВОСТЯМ', 'your-text-domain'); ?></span>
                <span class="news-all-arrow"></span>
            </a>
        </div>
    </div>

    <script>
        (function() {
            var links = document.querySelectorAll('.news-read-more');
            var duration = 700;

            links.forEach(function(link) {
                var text = link.querySelector('.news-read-text');
                var arrow = link.querySelector('.news-read-arrow');
                var underlineTimer = null;
                var arrowChangeTimer = null;

                link.addEventListener('mouseenter', function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove('hide-underline');
                    arrow.textContent = '→';
                    arrow.style.color = '';
                    text.style.color = '';

                    underlineTimer = setTimeout(function() {
                        text.classList.add('hide-underline');
                    }, duration - 50);

                    arrowChangeTimer = setTimeout(function() {
                        arrow.textContent = '>';
                        arrow.style.color = '#2c7f2e';
                        text.style.color = '#2c7f2e';
                    }, duration);
                });

                link.addEventListener('mouseleave', function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove('hide-underline');
                    arrow.textContent = '→';
                    arrow.style.color = '';
                    text.style.color = '';
                });

                link.addEventListener('blur', function() {
                    clearTimeout(underlineTimer);
                    clearTimeout(arrowChangeTimer);
                    text.classList.remove('hide-underline');
                    arrow.textContent = '→';
                    arrow.style.color = '';
                    text.style.color = '';
                });
            });
        })();
    </script>
    <?php
    wp_reset_postdata();
    return ob_get_clean();
}
add_shortcode('custom_news', 'custom_news_classic_shortcode');
Add the code to functions.php.
Use shortcode [custom_news posts_per_page="4" category="poleznoe"].
All styles and scripts are inline, no separate files needed.
Thumbnails: 400×240, placeholder – /wp-content/uploads/2025/09/400x240.avif.
🎨

Custom header v2 (with schedule)

Shortcode [custom_header] with editable contact center schedule and client-side phone number swapping.

<?php
/**
 * Шорткод [custom_header] – кастомный хедер с редактируемым расписанием и перестановкой номеров (v2)
 * Добавьте в functions.php
 * 
 * Код полностью обезличен: все номера телефонов заменены на шаблонные.
 */

add_shortcode('custom_header', 'custom_header_shortcode_v2');

function custom_header_shortcode_v2() {
    // === Расписание по умолчанию ===
    $default_schedule = [
        ['name' => 'вс', 'hours' => 'выходной'],
        ['name' => 'пн', 'hours' => '9.00-17.00'],
        ['name' => 'вт', 'hours' => '9.00-17.00'],
        ['name' => 'ср', 'hours' => '9.00-17.00'],
        ['name' => 'чт', 'hours' => '9.00-17.00'],
        ['name' => 'пт', 'hours' => '9.00-17.00'],
        ['name' => 'сб', 'hours' => '9.00-14.00']
    ];
    $saved_schedule = get_option('custom_schedule_v2', $default_schedule);
    if (!is_array($saved_schedule) || count($saved_schedule) != 7) {
        $saved_schedule = $default_schedule;
    }

    // === Контакты и их порядок (обезличенные номера) ===
    $contacts_data = [
        0 => ['label' => 'A1', 'viber' => true,  'phone' => '+375 (XX) XXX-XX-XX', 'tel' => '+375XXXXXXXXX'],
        1 => ['label' => 'МТС', 'viber' => true,  'phone' => '+375 (XX) XXX-XX-XX', 'tel' => '+375XXXXXXXXX'],
        2 => ['label' => 'A1', 'viber' => true,  'phone' => '+375 (XX) XXX-XX-XX', 'tel' => '+375XXXXXXXXX'],
        3 => ['label' => 'Белтелеком', 'viber' => false, 'phone' => '+375 (XX) XXX-XX-XX', 'tel' => '+375XXXXXXXXX']
    ];
    $default_order = [0, 1, 2, 3];
    $saved_order = get_option('custom_phone_order_v2', $default_order);
    if (!is_array($saved_order) || count($saved_order) != 4 || array_diff($saved_order, [0,1,2,3])) {
        $saved_order = $default_order;
    }

    // === Nonce для форм ===
    $schedule_nonce = wp_create_nonce('chs_save_schedule_v2');
    $swap_nonce     = wp_create_nonce('chs_swap_phones_v2');

    $is_user_logged_in = is_user_logged_in();

    // === Уникальный идентификатор экземпляра ===
    $uid = 'chs_v2_' . wp_generate_password(6, false, false);

    // === Логотип ===
    $logo_html = '';
    if (has_custom_logo()) {
        $logo_id = get_theme_mod('custom_logo');
        $logo_attrs = [
            'class' => 'custom-header-logo',
            'alt'   => get_bloginfo('name'),
            'style' => 'height: 80px; width: auto; max-width: 100%;',
        ];
        $logo_img = wp_get_attachment_image($logo_id, 'full', false, $logo_attrs);
        $logo_html = '<a href="' . esc_url(home_url()) . '" class="custom-logo-link">' . $logo_img . '</a>';
    } else {
        $logo_html = '<a href="' . esc_url(home_url()) . '" class="custom-logo-link"><span class="custom-header-logo-text" style="height:80px; display:flex; align-items:center;">' . get_bloginfo('name') . '</span></a>';
    }

    // === Корзина (шорткод) ===
    $cart_shortcode = do_shortcode('[custom_cart_button show_count="yes" show_text="yes" text="Смета" badge_color="#e62e6b"]');

    ob_start();
    ?>
    <?php if ($is_user_logged_in): ?>
        <!-- Модальное окно редактирования расписания (только для админов) -->
        <div id="<?php echo $uid; ?>_schedule_modal" class="chs-modal-v2" style="display:none;" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="<?php echo $uid; ?>_modal_title">
            <div class="chs-modal-content-v2">
                <div class="chs-modal-header-v2">
                    <h3 id="<?php echo $uid; ?>_modal_title">Редактировать расписание</h3>
                    <span class="chs-modal-close-v2" data-action="close-modal" aria-label="Закрыть">&times;</span>
                </div>
                <div class="chs-modal-body-v2">
                    <form id="<?php echo $uid; ?>_schedule_form">
                        <input type="hidden" name="chs_nonce" value="<?php echo esc_attr($schedule_nonce); ?>">
                        <?php foreach ($saved_schedule as $index => $day): ?>
                            <div class="chs-form-row-v2">
                                <label><?php echo esc_html($day['name']); ?>:</label>
                                <input type="text" name="day[<?php echo $index; ?>][name]" value="<?php echo esc_attr($day['name']); ?>" readonly class="chs-day-name-v2" style="width:50px;">
                                <input type="text" name="day[<?php echo $index; ?>][hours]" value="<?php echo esc_attr($day['hours']); ?>" placeholder="9.00-17.00 или выходной" class="chs-hours-input-v2" maxlength="120">
                            </div>
                        <?php endforeach; ?>
                    </form>
                </div>
                <div class="chs-modal-footer-v2">
                    <button type="button" class="chs-btn-v2 chs-btn-save-v2" data-action="save-schedule">Сохранить</button>
                    <button type="button" class="chs-btn-v2 chs-btn-cancel-v2" data-action="close-modal">Отмена</button>
                    <span class="chs-save-result-v2" style="margin-left:10px;"></span>
                </div>
            </div>
        </div>

        <style>
            /* Стили модального окна v2 */
            .chs-modal-v2 { display: none; position: fixed; z-index: 100002; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); align-items: center; justify-content: center; }
            .chs-modal-content-v2 { background-color: #fff; margin: auto; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.2); animation: chs-fadein-v2 0.3s; }
            @keyframes chs-fadein-v2 { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
            .chs-modal-header-v2 { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
            .chs-modal-header-v2 h3 { margin: 0; font-size: 1.3rem; color: #333; }
            .chs-modal-close-v2 { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; }
            .chs-modal-close-v2:hover { color: #000; }
            .chs-form-row-v2 { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
            .chs-form-row-v2 label { width: 30px; font-weight: 600; color: #555; }
            .chs-day-name-v2 { background: #f0f0f0; border: 1px solid #ddd; padding: 5px; border-radius: 4px; text-align: center; }
            .chs-hours-input-v2 { flex: 1; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.95rem; }
            .chs-hours-input-v2:focus { border-color: #007cba; outline: none; }
            .chs-modal-footer-v2 { margin-top: 20px; text-align: right; border-top: 1px solid #eee; padding-top: 15px; }
            .chs-btn-v2 { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.95rem; transition: background 0.2s; }
            .chs-btn-save-v2 { background-color: #007cba; color: white; }
            .chs-btn-save-v2:hover { background-color: #005a87; }
            .chs-btn-cancel-v2 { background-color: #f1f1f1; color: #333; margin-left: 10px; }
            .chs-btn-cancel-v2:hover { background-color: #ddd; }
            #schedule-save-result-v2 { font-size: 0.9rem; color: green; }
        </style>
    <?php endif; ?>

    <style>
        /* Все общие стили (v2) */
        .chs-header-v2 * { box-sizing: border-box; }
        .chs-header-v2 .custom-header-wrapper { max-width:1400px; margin:0 auto; padding:0 12px; }
        @media (max-width:1400px) { .chs-header-v2 .custom-header-wrapper { padding:0 16px; } }
        @media (max-width:767px) { .chs-header-v2 .custom-header-wrapper { padding:0 16px; } }
        @media (max-width:360px) { .chs-header-v2 .custom-header-wrapper { padding:0 12px; } }
        .chs-header-v2 .custom-header-container { display:flex; align-items:center; justify-content:space-between; width:100%; gap:20px; flex-wrap:wrap; }
        .chs-header-v2 .custom-header-left { flex:0 0 auto; }
        .chs-header-v2 .custom-header-center { flex:1 1 auto; display:flex; justify-content:space-around; gap:20px; flex-wrap:wrap; }
        .chs-header-v2 .custom-header-right { flex:0 0 auto; display:flex; align-items:center; gap:20px; }
        .chs-header-v2 .custom-contacts-wrapper { text-align:right !important; direction:rtl !important; }
        .chs-header-v2 .custom-contacts-wrapper .contacts-container { display:flex !important; flex-direction:column !important; gap:2px !important; font-size:0.9rem !important; align-items:flex-end !important; direction:ltr !important; text-align:left !important; }
        .chs-header-v2 .custom-contacts-wrapper .contact-item { color:#444 !important; text-decoration:none !important; white-space:nowrap !important; direction:ltr !important; outline:none !important; border:none !important; background:transparent !important; display:flex !important; align-items:center !important; gap:8px !important; position:relative !important; justify-content:flex-end !important; }
        .chs-header-v2 .custom-contacts-wrapper .phone-number { color:#444 !important; text-decoration:none !important; font-weight:600 !important; }
        .chs-header-v2 .custom-contacts-wrapper .operator-label { display:inline-block !important; font-size:0.7rem !important; font-weight:500 !important; padding:2px 6px !important; border-radius:3px !important; color:white !important; line-height:1 !important; flex-shrink:0 !important; background-color:#444 !important; text-decoration:none !important; cursor:pointer !important; }
        .chs-header-v2 .custom-contacts-wrapper .viber-label { display:inline-block !important; font-size:0.7rem !important; font-weight:500 !important; padding:2px 6px !important; border-radius:3px !important; color:white !important; line-height:1 !important; flex-shrink:0 !important; background-color:#7360F2 !important; text-decoration:none !important; cursor:pointer !important; }
        .chs-header-v2 .header-right-section { display:flex; align-items:center; gap:20px; }
        .chs-header-v2 .contact-center { display:inline-flex; align-items:center; gap:6px; color:#444; font-size:0.9rem; line-height:1.5; cursor:pointer; position:relative; }
        .chs-header-v2 .contact-center svg { width:22px; height:22px; stroke:#444; stroke-width:1.6; }
        .chs-header-v2 .contact-center strong { font-weight:600; }
        .chs-header-v2 .today { color:#2e7d32; font-weight:500; }
        .chs-header-v2 .today-dayoff { color:#d32f2f; font-weight:500; }
        .chs-header-v2 .contact-center .full-schedule { display:none; position:absolute; top:100%; left:0; background:white; border:1px solid #ddd; padding:12px 12px 12px 12px; border-radius:4px; box-shadow:0 2px 10px rgba(0,0,0,0.1); z-index:100001; min-width:180px; white-space:nowrap; font-size:0.9rem; margin-top:5px; }
        .chs-header-v2 .contact-center .full-schedule.active { display:block; }
        .chs-header-v2 .contact-center .full-schedule .close-btn { position:absolute; top:8px; right:8px; width:16px; height:16px; cursor:pointer; opacity:0.6; }
        .chs-header-v2 .contact-center .full-schedule .close-btn:hover { opacity:1; }
        .chs-header-v2 .contact-center .full-schedule .close-btn::before, .chs-header-v2 .contact-center .full-schedule .close-btn::after { content:""; position:absolute; top:50%; left:50%; width:12px; height:2px; background:#666; border-radius:1px; }
        .chs-header-v2 .contact-center .full-schedule .close-btn::before { transform:translate(-50%, -50%) rotate(45deg); }
        .chs-header-v2 .contact-center .full-schedule .close-btn::after { transform:translate(-50%, -50%) rotate(-45deg); }
        .chs-header-v2 .contact-center .full-schedule .schedule-content { margin-right:20px; }
        .chs-header-v2 .contact-center .full-schedule .schedule-content span { display:inline-block; line-height:1.6; }
        .chs-header-v2 .cart-btn { font-size:17px !important; }
        .chs-header-v2 .chs-edit-schedule-btn-v2 { background:transparent; border:none; color:#444; font-size:1.2rem; cursor:pointer; padding:4px; border-radius:4px; display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; transition:background-color 0.2s; }
        .chs-header-v2 .chs-edit-schedule-btn-v2:hover { background-color:#f0f0f0; }
        @media (min-width:1401px) {
            .chs-header-v2 .custom-contacts-wrapper .contacts-container { font-size:1.08rem !important; }
            .chs-header-v2 .custom-contacts-wrapper .operator-label, .chs-header-v2 .custom-contacts-wrapper .viber-label { font-size:0.84rem !important; }
            .chs-header-v2 .contact-center { font-size:1.08rem !important; }
            .chs-header-v2 .contact-center svg { width:26px !important; height:26px !important; }
        }
        .chs-header-v2 .custom-logo-link { position:relative; display:inline-block; overflow:hidden; }
        .chs-header-v2 .custom-logo-link::after { content:''; position:absolute; top:-50%; left:-50%; width:200%; height:200%; background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.6) 50%, rgba(255,255,255,0) 100%); transform:rotate(30deg); opacity:0; pointer-events:none; }
        .chs-header-v2 .custom-logo-link.shine::after { animation:shine-v2 2s ease-out 1; }
        @keyframes shine-v2 { 0% { opacity:0; transform:translateX(-100%) rotate(30deg); } 10% { opacity:0.6; } 90% { opacity:0.6; } 100% { opacity:0; transform:translateX(100%) rotate(30deg); } }
        @media (min-width:768px) and (max-width:1400px) {
            .chs-header-v2 .custom-header-container, .chs-header-v2 .custom-header-center { flex-wrap:nowrap !important; }
            .chs-header-v2 .custom-header-container { gap:10px; }
            .chs-header-v2 .custom-header-center { gap:10px; }
            .chs-header-v2 .custom-header-right { gap:10px; }
            .chs-header-v2 .contact-center { display:none !important; }
            .chs-header-v2 .custom-contacts-wrapper .contacts-container { font-size:0.8rem !important; }
            .chs-header-v2 .custom-contacts-wrapper .operator-label, .chs-header-v2 .custom-contacts-wrapper .viber-label { font-size:0.65rem !important; padding:1px 4px !important; }
            .chs-header-v2 .custom-contacts-wrapper .contact-item { gap:4px !important; }
        }
        @media (max-width:767px) {
            .chs-header-v2 .custom-header-right { display:none !important; }
            .chs-header-v2 .custom-header-container { flex-direction:column; align-items:center; gap:2px; }
            .chs-header-v2 .custom-header-left { padding:0 !important; margin:0 !important; text-align:center; }
            .chs-header-v2 .custom-header-logo { height:80px !important; width:auto; }
            .chs-header-v2 .custom-header-center { flex-direction:column; align-items:center; gap:2px; width:100%; }
            .chs-header-v2 .custom-contacts-wrapper { text-align:center !important; direction:ltr !important; width:100%; }
            .chs-header-v2 .custom-contacts-wrapper .contacts-container { align-items:center !important; text-align:center !important; direction:ltr !important; gap:2px !important; font-size:1rem !important; }
            .chs-header-v2 .custom-contacts-wrapper .contact-item { justify-content:center !important; gap:8px; }
            .chs-header-v2 .custom-contacts-wrapper .phone-number { font-size:1.1rem !important; }
            .chs-header-v2 .custom-contacts-wrapper .operator-label, .chs-header-v2 .custom-contacts-wrapper .viber-label { font-size:0.8rem !important; padding:2px 6px !important; }
            .chs-header-v2 .contact-item a { white-space:nowrap; }
        }
    </style>

    <div class="chs-header-v2" id="<?php echo $uid; ?>_container" data-uid="<?php echo $uid; ?>">
        <div class="custom-header-wrapper">
            <div class="custom-header-container">
                <!-- Логотип -->
                <div class="custom-header-left">
                    <?php echo $logo_html; ?>
                </div>

                <!-- Блоки контактов с учётом сохранённого порядка -->
                <?php
                $first_group_indices = array_slice($saved_order, 0, 2);
                $second_group_indices = array_slice($saved_order, 2, 2);
                ?>
                <div class="custom-header-center">
                    <div class="custom-contacts-wrapper">
                        <div class="contacts-container">
                            <?php foreach ($first_group_indices as $idx): 
                                $c = $contacts_data[$idx]; ?>
                                <div class="contact-item" data-phone-index="<?php echo $idx; ?>" data-is-mobile="<?php echo $c['viber'] ? 'true' : 'false'; ?>">
                                    <a href="tel:<?php echo esc_attr($c['tel']); ?>" class="operator-label"><?php echo esc_html($c['label']); ?></a>
                                    <?php if ($c['viber']): ?>
                                        <a href="viber://chat?number=%2B<?php echo preg_replace('/\D/', '', $c['tel']); ?>" class="viber-label">Viber</a>
                                    <?php endif; ?>
                                    <a href="tel:<?php echo esc_attr($c['tel']); ?>" class="phone-number"><?php echo esc_html($c['phone']); ?></a>
                                </div>
                            <?php endforeach; ?>
                        </div>
                    </div>
                    <div class="custom-contacts-wrapper">
                        <div class="contacts-container">
                            <?php foreach ($second_group_indices as $idx): 
                                $c = $contacts_data[$idx]; ?>
                                <div class="contact-item" data-phone-index="<?php echo $idx; ?>" data-is-mobile="<?php echo $c['viber'] ? 'true' : 'false'; ?>">
                                    <a href="tel:<?php echo esc_attr($c['tel']); ?>" class="operator-label"><?php echo esc_html($c['label']); ?></a>
                                    <?php if ($c['viber']): ?>
                                        <a href="viber://chat?number=%2B<?php echo preg_replace('/\D/', '', $c['tel']); ?>" class="viber-label">Viber</a>
                                    <?php endif; ?>
                                    <a href="tel:<?php echo esc_attr($c['tel']); ?>" class="phone-number"><?php echo esc_html($c['phone']); ?></a>
                                </div>
                            <?php endforeach; ?>
                        </div>
                    </div>
                </div>

                <!-- Правая колонка -->
                <div class="custom-header-right">
                    <div class="header-right-section">
                        <!-- Контакт-центр -->
                        <div class="contact-center" id="<?php echo $uid; ?>_contact_center" role="button" aria-expanded="false" aria-controls="<?php echo $uid; ?>_schedule_tooltip">
                            <svg viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <circle cx="12" cy="12" r="10"/>
                                <path d="M12 6v6l4 2"/>
                            </svg>
                            <div>
                                <strong>Контакт-центр</strong><br>
                                <span class="today" id="<?php echo $uid; ?>_today_schedule">Загрузка...</span>
                                <div class="full-schedule" id="<?php echo $uid; ?>_schedule_tooltip" aria-hidden="true">
                                    <div class="close-btn" id="<?php echo $uid; ?>_close_schedule" data-action="close-tooltip"></div>
                                    <div class="schedule-content"></div>
                                </div>
                            </div>
                        </div>

                        <!-- Кнопка редактирования расписания (только для админов) -->
                        <?php if ($is_user_logged_in): ?>
                            <button type="button" class="chs-edit-schedule-btn-v2" aria-label="Редактировать расписание" data-action="edit-schedule">✎</button>
                        <?php endif; ?>

                        <!-- Корзина -->
                        <div class="cart-container">
                            <?php echo $cart_shortcode; ?>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
    // Глобальная переменная с расписанием
    window.scheduleDataV2 = <?php echo json_encode($saved_schedule); ?>;

    (function() {
        var container = document.getElementById('<?php echo $uid; ?>_container');
        if (!container) return;

        // Данные расписания
        var scheduleData = <?php echo json_encode($saved_schedule); ?>;
        container.setAttribute('data-schedule', JSON.stringify(scheduleData));

        // Nonce для перестановки номеров
        var swapNonce = '<?php echo $swap_nonce; ?>';
        var ajaxurl = '<?php echo admin_url('admin-ajax.php'); ?>';

        // Элементы (расписание)
        var contactCenter = container.querySelector('.contact-center');
        var tooltip = document.getElementById('<?php echo $uid; ?>_schedule_tooltip');
        var closeBtn = document.getElementById('<?php echo $uid; ?>_close_schedule');
        var todaySpan = document.getElementById('<?php echo $uid; ?>_today_schedule');
        var contentDiv = tooltip ? tooltip.querySelector('.schedule-content') : null;
        var editBtn = container.querySelector('.chs-edit-schedule-btn-v2');
        var modal = document.getElementById('<?php echo $uid; ?>_schedule_modal');
        var form = modal ? modal.querySelector('form') : null;
        var saveBtn = modal ? modal.querySelector('[data-action="save-schedule"]') : null;
        var closeModalButtons = modal ? modal.querySelectorAll('[data-action="close-modal"]') : [];
        var saveResultSpan = modal ? modal.querySelector('.chs-save-result-v2') : null;

        // Вспомогательные функции для расписания
        function buildScheduleHTML(schedule) {
            var groups = {};
            schedule.forEach(function(item, index) {
                if (!groups[item.hours]) groups[item.hours] = [];
                groups[item.hours].push(index);
            });

            var html = '';
            for (var hours in groups) {
                var indexes = groups[hours];
                indexes.sort(function(a, b) { return a - b; });
                var isConsecutive = true;
                for (var i = 1; i < indexes.length; i++) {
                    if (indexes[i] !== indexes[i-1] + 1) {
                        isConsecutive = false;
                        break;
                    }
                }
                if (isConsecutive && indexes.length > 1) {
                    var firstDay = schedule[indexes[0]].name;
                    var lastDay = schedule[indexes[indexes.length-1]].name;
                    html += '<span style="color: black">' + escapeHtml(firstDay) + '-' + escapeHtml(lastDay) + ':</span> <span style="color: #444444">' + escapeHtml(hours) + '</span><br>';
                } else {
                    indexes.forEach(function(idx) {
                        html += '<span style="color: black">' + escapeHtml(schedule[idx].name) + ':</span> <span style="color: #444444">' + escapeHtml(hours) + '</span><br>';
                    });
                }
            }
            return html;
        }

        function escapeHtml(str) {
            if (typeof str !== 'string') return '';
            return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
        }

        function updateTooltip() {
            if (contentDiv && scheduleData) {
                contentDiv.innerHTML = buildScheduleHTML(scheduleData);
            }
        }

        function updateTodaySchedule() {
            var today = new Date().getDay();
            var info = scheduleData[today];
            if (todaySpan && info) {
                todaySpan.textContent = info.name + ': ' + info.hours;
                todaySpan.classList.remove('today', 'today-dayoff');
                todaySpan.classList.add(info.hours === 'выходной' ? 'today-dayoff' : 'today');
            }
        }

        // Управление тултипом
        var isTooltipOpen = false;
        function toggleTooltip() {
            if (!tooltip) return;
            if (isTooltipOpen) {
                tooltip.classList.remove('active');
                tooltip.setAttribute('aria-hidden', 'true');
                if (contactCenter) contactCenter.setAttribute('aria-expanded', 'false');
                isTooltipOpen = false;
            } else {
                tooltip.classList.add('active');
                tooltip.setAttribute('aria-hidden', 'false');
                if (contactCenter) contactCenter.setAttribute('aria-expanded', 'true');
                isTooltipOpen = true;
            }
        }

        function closeTooltip() {
            if (!tooltip) return;
            tooltip.classList.remove('active');
            tooltip.setAttribute('aria-hidden', 'true');
            if (contactCenter) contactCenter.setAttribute('aria-expanded', 'false');
            isTooltipOpen = false;
        }

        // Модальное окно
        function openModal() {
            if (!modal) return;
            modal.style.display = 'flex';
            modal.setAttribute('aria-hidden', 'false');
        }

        function closeModal() {
            if (!modal) return;
            modal.style.display = 'none';
            modal.setAttribute('aria-hidden', 'true');
            if (saveResultSpan) saveResultSpan.textContent = '';
        }

        // Обработчики расписания
        if (contactCenter) {
            contactCenter.addEventListener('click', function(e) {
                e.stopPropagation();
                if (modal) {
                    openModal();
                } else {
                    toggleTooltip();
                }
            });
        }

        if (closeBtn) {
            closeBtn.addEventListener('click', function(e) {
                e.stopPropagation();
                closeTooltip();
            });
        }

        if (editBtn) {
            editBtn.addEventListener('click', function(e) {
                e.stopPropagation();
                openModal();
            });
        }

        closeModalButtons.forEach(function(btn) {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                closeModal();
            });
        });

        if (modal) {
            modal.addEventListener('click', function(e) {
                if (e.target === modal) closeModal();
            });
        }

        document.addEventListener('click', function(e) {
            if (isTooltipOpen && !container.contains(e.target)) {
                closeTooltip();
            }
        });

        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                closeTooltip();
                if (modal && modal.style.display === 'flex') {
                    closeModal();
                }
            }
        });

        // Сохранение расписания (AJAX)
        if (form && saveBtn) {
            saveBtn.addEventListener('click', function(e) {
                e.preventDefault();
                var formData = new FormData(form);
                formData.append('action', 'save_schedule_v2');

                if (saveResultSpan) saveResultSpan.textContent = 'Сохранение...';

                fetch(ajaxurl, {
                    method: 'POST',
                    body: formData
                })
                .then(response => response.text())
                .then(text => {
                    text = text.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
                    try {
                        var data = JSON.parse(text);
                        if (data.success) {
                            scheduleData = data.data;
                            container.setAttribute('data-schedule', JSON.stringify(scheduleData));
                            window.scheduleDataV2 = scheduleData;
                            updateTooltip();
                            updateTodaySchedule();
                            if (saveResultSpan) saveResultSpan.textContent = '✓ Сохранено!';
                            setTimeout(closeModal, 1000);
                        } else {
                            if (saveResultSpan) saveResultSpan.textContent = '✗ Ошибка: ' + (data.data || 'неизвестная ошибка');
                        }
                    } catch (e) {
                        if (saveResultSpan) saveResultSpan.textContent = '✗ Ответ не JSON: ' + text.substring(0,100);
                        console.error('Ответ сервера:', text);
                    }
                })
                .catch(error => {
                    if (saveResultSpan) saveResultSpan.textContent = '✗ Ошибка соединения: ' + error.message;
                });
            });
        }

        // === НОВАЯ ЛОГИКА: перестановка мобильных номеров ===
        function getContainers() {
            var firstWrapper = container.querySelector('.custom-header-center .custom-contacts-wrapper:first-child .contacts-container');
            var secondWrapper = container.querySelector('.custom-header-center .custom-contacts-wrapper:last-child .contacts-container');
            return { first: firstWrapper, second: secondWrapper };
        }

        function handlePhoneClick(e) {
            var phoneLink = e.currentTarget;
            var contactItem = phoneLink.closest('.contact-item');
            if (!contactItem) return;

            var allItems = Array.from(container.querySelectorAll('.contact-item'));
            var mobileItems = allItems.filter(item => item.dataset.isMobile === 'true');
            var landlineItem = allItems.find(item => item.dataset.isMobile === 'false');

            var firstMobile = mobileItems[0];
            if (contactItem !== firstMobile) return;

            var newMobileOrder = mobileItems.slice(1);
            newMobileOrder.push(firstMobile);

            var finalOrder = newMobileOrder.concat([landlineItem]);

            var containers = getContainers();
            containers.first.innerHTML = '';
            containers.second.innerHTML = '';

            finalOrder.slice(0, 2).forEach(item => containers.first.appendChild(item));
            finalOrder.slice(2).forEach(item => containers.second.appendChild(item));

            var newIndices = finalOrder.map(item => parseInt(item.dataset.phoneIndex));

            var formData = new FormData();
            formData.append('action', 'swap_phones_v2');
            formData.append('chs_nonce', swapNonce);
            formData.append('phone_order', JSON.stringify(newIndices));

            fetch(ajaxurl, {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (!data.success) {
                    console.error('Ошибка сохранения порядка:', data.data);
                }
            })
            .catch(error => console.error('Ошибка соединения:', error));
        }

        container.querySelectorAll('.phone-number').forEach(function(link) {
            link.addEventListener('click', handlePhoneClick);
        });

        // Эффект блеска для логотипа
        var logoLink = container.querySelector('.custom-logo-link');
        if (logoLink) {
            logoLink.classList.add('shine');
            setTimeout(() => logoLink.classList.remove('shine'), 2000);
            logoLink.addEventListener('mouseenter', function() {
                this.classList.add('shine');
                setTimeout(() => this.classList.remove('shine'), 2000);
            });
        }

        // Инициализация
        updateTodaySchedule();
        updateTooltip();
    })();
    </script>
    <?php
    return ob_get_clean();
}

// === AJAX: сохранение расписания (только для админов) v2 ===
add_action('wp_ajax_save_schedule_v2', 'save_schedule_callback_v2');
function save_schedule_callback_v2() {
    while (ob_get_level()) ob_end_clean();

    if (!current_user_can('edit_theme_options')) {
        wp_send_json_error('Недостаточно прав');
    }

    if (!isset($_POST['chs_nonce']) || !wp_verify_nonce($_POST['chs_nonce'], 'chs_save_schedule_v2')) {
        wp_send_json_error('Неверный токен безопасности');
    }

    if (isset($_POST['day']) && is_array($_POST['day'])) {
        $new_schedule = [];
        foreach ($_POST['day'] as $index => $day) {
            $name = isset($day['name']) ? sanitize_text_field(wp_unslash($day['name'])) : '';
            $hours = isset($day['hours']) ? sanitize_text_field(wp_unslash($day['hours'])) : '';
            if (mb_strlen($hours) > 120) $hours = mb_substr($hours, 0, 120);
            if ($name === '') wp_send_json_error('Неверный формат имени дня');
            $new_schedule[] = [
                'name' => $name,
                'hours' => $hours === '' ? 'выходной' : $hours,
            ];
        }
        if (count($new_schedule) === 7) {
            update_option('custom_schedule_v2', $new_schedule, false);
            wp_send_json_success($new_schedule);
        } else {
            wp_send_json_error('Неверный формат данных — ожидалось 7 записей');
        }
    } else {
        wp_send_json_error('Нет данных для сохранения');
    }
}

// === AJAX: перестановка номеров (доступно всем) v2 ===
add_action('wp_ajax_swap_phones_v2', 'swap_phones_callback_v2');
add_action('wp_ajax_nopriv_swap_phones_v2', 'swap_phones_callback_v2');
function swap_phones_callback_v2() {
    while (ob_get_level()) ob_end_clean();

    if (!isset($_POST['chs_nonce']) || !wp_verify_nonce($_POST['chs_nonce'], 'chs_swap_phones_v2')) {
        wp_send_json_error('Неверный токен безопасности');
    }

    if (isset($_POST['phone_order'])) {
        $order_raw = $_POST['phone_order'];
        $order = json_decode(stripslashes($order_raw), true);
        if (is_array($order) && count($order) === 4) {
            $order = array_map('intval', $order);
            if (count(array_unique($order)) === 4 && min($order) === 0 && max($order) === 3) {
                update_option('custom_phone_order_v2', $order, false);
                wp_send_json_success($order);
            } else {
                wp_send_json_error('Неверный порядок чисел');
            }
        } else {
            wp_send_json_error('Некорректный формат данных');
        }
    } else {
        wp_send_json_error('Нет данных для сохранения');
    }
}
Add the complete code to functions.php. Use shortcode [custom_header].
Schedule is edited via a modal (admin only) and saved to options.
Phone numbers can be reordered by clicking the first mobile number – order persists via AJAX.
Logo is fetched from theme settings (custom_logo).
The cart is rendered via [custom_cart_button] shortcode, which must be defined separately.
🔢

Decimal quantities for WooCommerce

Support for decimal quantities (step 0.01) and comma as decimal separator in WooCommerce.

<?php
/**
 * Поддержка дробных количеств в WooCommerce
 */
add_filter('woocommerce_quantity_input_step', function($step, $product) { return 0.01; }, 10, 2);
add_filter('woocommerce_quantity_input_min', function($min, $product) { return 0.01; }, 10, 2);
add_filter('woocommerce_add_to_cart_quantity', function($quantity, $product_id) {
    return floatval(str_replace(',', '.', $quantity));
}, 10, 2);
add_filter('woocommerce_update_cart_quantity', function($quantity, $product_id) {
    return floatval(str_replace(',', '.', $quantity));
}, 10, 2);
remove_filter('woocommerce_stock_amount', 'intval');
add_filter('woocommerce_stock_amount', 'floatval');
add_filter('woocommerce_add_to_cart_validation', function($passed, $product_id, $quantity) {
    $q = str_replace(',', '.', $quantity);
    if (!is_numeric($q) || floatval($q) <= 0) {
        wc_add_notice(__('Пожалуйста, введите корректное положительное количество.'), 'error');
        return false;
    }
    return $passed;
}, 10, 3);
Insert the code into functions.php.
Quantity input fields will then accept values with a step of 0.01, and a comma will be converted to a dot automatically.
Works for add-to-cart, cart update, and stock validation.
📢

Promo banner with settings

Customizable promo banner with 14 visual styles, flexible positioning, animations, custom colors and buttons.

<?php
/**
 * Полный модуль промо-баннера с расширенными настройками (исправленная версия)
 * Добавьте этот код в functions.php вашей темы или в отдельный плагин.
 */

// 1. Добавляем страницу настроек в админку
add_action('admin_menu', 'promo_banner_add_admin_page');
function promo_banner_add_admin_page() {
    add_options_page(
        'Настройки промо-баннера',
        'Промо-баннер',
        'manage_options',
        'promo-banner-settings',
        'promo_banner_render_settings_page'
    );
}

// 2. Регистрируем настройки
add_action('admin_init', 'promo_banner_register_settings');
function promo_banner_register_settings() {
    // Основные поля
    register_setting('promo_banner_options_group', 'promo_banner_enabled', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_anchor_selector', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_storage_type', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_expire_hours', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_display_mode', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_position', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_border_radius', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_show_only_home', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_delay', 'floatval');
    register_setting('promo_banner_options_group', 'promo_banner_title', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_description', 'wp_kses_post');
    
    // Ранее добавленные настройки
    register_setting('promo_banner_options_group', 'promo_banner_close_on_click_outside', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_use_custom_colors', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_bg_color', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_text_color', 'sanitize_hex_color');

    // Индивидуальные цвета элементов
    register_setting('promo_banner_options_group', 'promo_banner_title_color', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_desc_color', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_go_bg', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_go_text', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_close_bg', 'sanitize_hex_color');
    register_setting('promo_banner_options_group', 'promo_banner_close_text', 'sanitize_hex_color');

    // Эффекты
    register_setting('promo_banner_options_group', 'promo_banner_effect_in', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_effect_out', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_effect_duration', 'floatval');

    // Поворот
    register_setting('promo_banner_options_group', 'promo_banner_rotate', 'intval');

    // Размер окна
    register_setting('promo_banner_options_group', 'promo_banner_max_width', 'intval');

    // Настройки кнопок
    register_setting('promo_banner_options_group', 'promo_banner_go_button_text', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_go_button_enabled', 'intval');
    register_setting('promo_banner_options_group', 'promo_banner_close_button_text', 'sanitize_text_field');
    register_setting('promo_banner_options_group', 'promo_banner_close_button_enabled', 'intval');

    add_settings_section(
        'promo_banner_main_section',
        'Основные настройки',
        null,
        'promo-banner-settings'
    );

    // --- Поля ---
    add_settings_field(
        'enabled',
        'Включить промо-баннер',
        'promo_banner_enabled_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'anchor_selector',
        'Якорь (CSS-селектор)',
        'promo_banner_anchor_selector_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'storage_type',
        'Тип хранилища',
        'promo_banner_storage_type_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'expire_hours',
        'Время скрытия (часы)',
        'promo_banner_expire_hours_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'display_mode',
        'Режим отображения',
        'promo_banner_display_mode_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'position',
        'Расположение баннера',
        'promo_banner_position_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'border_radius',
        'Скругление углов (px)',
        'promo_banner_border_radius_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'show_only_home',
        'Показывать только на главной',
        'promo_banner_show_only_home_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'delay',
        'Задержка перед показом (сек)',
        'promo_banner_delay_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'title_text',
        'Заголовок баннера',
        'promo_banner_title_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'description_text',
        'Описание баннера (можно использовать HTML)',
        'promo_banner_description_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'close_on_click_outside',
        'Закрывать при клике вне баннера',
        'promo_banner_close_on_click_outside_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    // Кнопки
    add_settings_field(
        'go_button',
        'Кнопка "Перейти к акции"',
        'promo_banner_go_button_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'close_button',
        'Кнопка "Закрыть"',
        'promo_banner_close_button_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    // Цвета
    add_settings_field(
        'use_custom_colors',
        'Использовать свои цвета (расширенные)',
        'promo_banner_use_custom_colors_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'bg_color',
        'Общий цвет фона',
        'promo_banner_bg_color_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'text_color',
        'Общий цвет текста',
        'promo_banner_text_color_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'title_color',
        'Цвет заголовка',
        'promo_banner_title_color_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'desc_color',
        'Цвет описания',
        'promo_banner_desc_color_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'go_bg',
        'Фон кнопки "Перейти к акции"',
        'promo_banner_go_bg_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'go_text',
        'Текст кнопки "Перейти к акции"',
        'promo_banner_go_text_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'close_bg',
        'Фон кнопки "Закрыть"',
        'promo_banner_close_bg_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'close_text',
        'Текст кнопки "Закрыть"',
        'promo_banner_close_text_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    // Эффекты
    add_settings_field(
        'effect_in',
        'Эффект появления',
        'promo_banner_effect_in_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'effect_out',
        'Эффект закрытия',
        'promo_banner_effect_out_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'effect_duration',
        'Длительность анимации (сек)',
        'promo_banner_effect_duration_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'rotate',
        'Поворот окна (градусы)',
        'promo_banner_rotate_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );

    add_settings_field(
        'max_width',
        'Максимальная ширина (px)',
        'promo_banner_max_width_callback',
        'promo-banner-settings',
        'promo_banner_main_section'
    );
}

// ====================== CALLBACK-ФУНКЦИИ ДЛЯ ПОЛЕЙ ======================

function promo_banner_enabled_callback() {
    $value = get_option('promo_banner_enabled', 1);
    ?>
    <label>
        <input type="checkbox" name="promo_banner_enabled" value="1" <?php checked($value, 1); ?>>
        Включить показ баннера на сайте
    </label>
    <p class="description">Если снять галочку, баннер не будет отображаться нигде.</p>
    <?php
}

function promo_banner_anchor_selector_callback() {
    $value = get_option('promo_banner_anchor_selector', '#promo-block');
    ?>
    <input type="text" name="promo_banner_anchor_selector" value="<?php echo esc_attr($value); ?>" class="regular-text">
    <p class="description">CSS-селектор элемента, к которому будет прокрутка или который добавится в URL (например, #promo-block или .promo-section).</p>
    <?php
}

function promo_banner_storage_type_callback() {
    $value = get_option('promo_banner_storage_type', 'localStorage');
    ?>
    <select name="promo_banner_storage_type">
        <option value="localStorage" <?php selected($value, 'localStorage'); ?>>localStorage (сохраняется 24 часа)</option>
        <option value="sessionStorage" <?php selected($value, 'sessionStorage'); ?>>sessionStorage (до закрытия вкладки)</option>
    </select>
    <p class="description">Выберите, где хранить метку о скрытии баннера.</p>
    <?php
}

function promo_banner_expire_hours_callback() {
    $value = get_option('promo_banner_expire_hours', 24);
    ?>
    <input type="number" name="promo_banner_expire_hours" value="<?php echo esc_attr($value); ?>" min="0" step="1">
    <p class="description">Через сколько часов показывать баннер снова (0 — всегда показывать заново при перезагрузке).</p>
    <?php
}

function promo_banner_display_mode_callback() {
    $value = get_option('promo_banner_display_mode', 'standard');
    $modes = [
        'standard'        => 'Стандартный (полупрозрачный)',
        'noticeable'      => 'Заметный (голубой градиент)',
        'very_noticeable' => 'Очень заметный (оранжевый с пульсацией)',
        'dark'            => 'Тёмный',
        'minimal'         => 'Минималистичный',
        'accent'          => 'Акцентный (фиолетовый)',
        'msdos'           => 'Эмуляция MS DOS',
        'action'          => 'Внимание Акция',
        'danger'          => 'Внимание Опасность',
        'important'       => 'Внимание Важно',
        'glass'           => 'Glassmorphism',
        'neon'            => 'Neon',
        'elegant'         => 'Elegant',
        'corporate'       => 'Corporate',
    ];
    echo '<select name="promo_banner_display_mode">';
    foreach ($modes as $key => $label) {
        echo '<option value="' . esc_attr($key) . '" ' . selected($value, $key, false) . '>' . esc_html($label) . '</option>';
    }
    echo '</select>';
    echo '<p class="description">Выберите стиль оформления баннера. Если включены свои цвета, этот выбор игнорируется.</p>';
}

function promo_banner_position_callback() {
    $value = get_option('promo_banner_position', 'bottom-center');
    $positions = [
        'bottom-center' => 'Снизу по центру',
        'bottom-left'   => 'Снизу слева',
        'bottom-right'  => 'Снизу справа',
        'top-center'    => 'Сверху по центру',
        'top-left'      => 'Сверху слева',
        'top-right'     => 'Сверху справа',
        'center-center' => 'По центру экрана',
        'center-left'   => 'По центру вертикали, слева',
        'center-right'  => 'По центру вертикали, справа',
    ];
    echo '<select name="promo_banner_position">';
    foreach ($positions as $key => $label) {
        echo '<option value="' . esc_attr($key) . '" ' . selected($value, $key, false) . '>' . esc_html($label) . '</option>';
    }
    echo '</select>';
    echo '<p class="description">Выберите, где будет отображаться баннер на экране.</p>';
}

function promo_banner_border_radius_callback() {
    $value = get_option('promo_banner_border_radius', 8);
    ?>
    <input type="number" name="promo_banner_border_radius" value="<?php echo esc_attr($value); ?>" min="0" max="50" step="1">
    <p class="description">Радиус скругления углов в пикселях.</p>
    <?php
}

function promo_banner_show_only_home_callback() {
    $value = get_option('promo_banner_show_only_home', 0);
    ?>
    <label>
        <input type="checkbox" name="promo_banner_show_only_home" value="1" <?php checked($value, 1); ?>>
        Показывать баннер только на главной странице
    </label>
    <p class="description">Если отмечено, баннер будет виден только на главной. Иначе — на всех страницах.</p>
    <?php
}

function promo_banner_delay_callback() {
    $value = get_option('promo_banner_delay', 0);
    ?>
    <input type="number" name="promo_banner_delay" value="<?php echo esc_attr($value); ?>" min="0" max="60" step="0.5">
    <p class="description">Задержка перед появлением баннера (в секундах). 0 — без задержки.</p>
    <?php
}

function promo_banner_title_callback() {
    $value = get_option('promo_banner_title', '📢 Важная информация');
    ?>
    <input type="text" name="promo_banner_title" value="<?php echo esc_attr($value); ?>" class="regular-text">
    <p class="description">Заголовок баннера.</p>
    <?php
}

function promo_banner_description_callback() {
    $value = get_option('promo_banner_description', 'На сайте есть специальные предложения и акции, которые могут затеряться среди других материалов.<br>Перейдите в промо-блок, чтобы ничего не пропустить.');
    ?>
    <textarea name="promo_banner_description" rows="4" class="large-text"><?php echo esc_textarea($value); ?></textarea>
    <p class="description">Описание баннера. Можно использовать простой HTML (например, &lt;br&gt;).</p>
    <?php
}

function promo_banner_close_on_click_outside_callback() {
    $value = get_option('promo_banner_close_on_click_outside', 0);
    ?>
    <label>
        <input type="checkbox" name="promo_banner_close_on_click_outside" value="1" <?php checked($value, 1); ?>>
        Закрывать баннер при клике вне его области
    </label>
    <p class="description">Если отмечено, баннер будет скрываться при клике на любое место вне баннера.</p>
    <?php
}

function promo_banner_go_button_callback() {
    $enabled = get_option('promo_banner_go_button_enabled', 1);
    $text = get_option('promo_banner_go_button_text', 'Перейти к акции');
    ?>
    <p>
        <label>
            <input type="checkbox" name="promo_banner_go_button_enabled" value="1" <?php checked($enabled, 1); ?>>
            Показывать кнопку
        </label>
    </p>
    <p>
        <label>Текст кнопки:</label>
        <input type="text" name="promo_banner_go_button_text" value="<?php echo esc_attr($text); ?>" class="regular-text">
    </p>
    <?php
}

function promo_banner_close_button_callback() {
    $enabled = get_option('promo_banner_close_button_enabled', 1);
    $text = get_option('promo_banner_close_button_text', 'Закрыть');
    ?>
    <p>
        <label>
            <input type="checkbox" name="promo_banner_close_button_enabled" value="1" <?php checked($enabled, 1); ?>>
            Показывать кнопку
        </label>
    </p>
    <p>
        <label>Текст кнопки:</label>
        <input type="text" name="promo_banner_close_button_text" value="<?php echo esc_attr($text); ?>" class="regular-text">
    </p>
    <?php
}

function promo_banner_use_custom_colors_callback() {
    $value = get_option('promo_banner_use_custom_colors', 0);
    ?>
    <label>
        <input type="checkbox" name="promo_banner_use_custom_colors" value="1" <?php checked($value, 1); ?>>
        Использовать свои цвета (расширенные)
    </label>
    <p class="description">Если включено, предустановленные режимы игнорируются, и применяются указанные ниже цвета для каждого элемента.</p>
    <?php
}

function promo_banner_bg_color_callback() {
    $value = get_option('promo_banner_bg_color', '#0066cc');
    ?>
    <input type="color" name="promo_banner_bg_color" value="<?php echo esc_attr($value); ?>">
    <p class="description">Общий цвет фона баннера (используется, если не заданы индивидуальные цвета элементов).</p>
    <?php
}

function promo_banner_text_color_callback() {
    $value = get_option('promo_banner_text_color', '#ffffff');
    ?>
    <input type="color" name="promo_banner_text_color" value="<?php echo esc_attr($value); ?>">
    <p class="description">Общий цвет текста баннера (используется, если не заданы индивидуальные цвета элементов).</p>
    <?php
}

function promo_banner_title_color_callback() {
    $value = get_option('promo_banner_title_color', '');
    ?>
    <input type="color" name="promo_banner_title_color" value="<?php echo esc_attr($value); ?>">
    <p class="description">Цвет заголовка (если не задан, используется общий цвет текста).</p>
    <?php
}

function promo_banner_desc_color_callback() {
    $value = get_option('promo_banner_desc_color', '');
    ?>
    <input type="color" name="promo_banner_desc_color" value="<?php echo esc_attr($value); ?>">
    <p class="description">Цвет описания (если не задан, используется общий цвет текста).</p>
    <?php
}

function promo_banner_go_bg_callback() {
    $value = get_option('promo_banner_go_bg', '');
    ?>
    <input type="color" name="promo_banner_go_bg" value="<?php echo esc_attr($value); ?>">
    <p class="description">Фон кнопки "Перейти к акции".</p>
    <?php
}

function promo_banner_go_text_callback() {
    $value = get_option('promo_banner_go_text', '');
    ?>
    <input type="color" name="promo_banner_go_text" value="<?php echo esc_attr($value); ?>">
    <p class="description">Цвет текста кнопки "Перейти к акции".</p>
    <?php
}

function promo_banner_close_bg_callback() {
    $value = get_option('promo_banner_close_bg', '');
    ?>
    <input type="color" name="promo_banner_close_bg" value="<?php echo esc_attr($value); ?>">
    <p class="description">Фон кнопки "Закрыть".</p>
    <?php
}

function promo_banner_close_text_callback() {
    $value = get_option('promo_banner_close_text', '');
    ?>
    <input type="color" name="promo_banner_close_text" value="<?php echo esc_attr($value); ?>">
    <p class="description">Цвет текста кнопки "Закрыть".</p>
    <?php
}

function promo_banner_effect_in_callback() {
    $value = get_option('promo_banner_effect_in', 'fade');
    $effects = [
        'fade'      => 'Плавное появление',
        'slide-up'  => 'Скольжение снизу',
        'slide-down'=> 'Скольжение сверху',
        'zoom'      => 'Увеличение',
        'none'      => 'Без эффекта',
    ];
    echo '<select name="promo_banner_effect_in">';
    foreach ($effects as $key => $label) {
        echo '<option value="' . esc_attr($key) . '" ' . selected($value, $key, false) . '>' . esc_html($label) . '</option>';
    }
    echo '</select>';
    echo '<p class="description">Эффект при появлении баннера.</p>';
}

function promo_banner_effect_out_callback() {
    $value = get_option('promo_banner_effect_out', 'fade');
    $effects = [
        'fade'      => 'Исчезновение',
        'slide-up'  => 'Скольжение вверх',
        'slide-down'=> 'Скольжение вниз',
        'zoom'      => 'Уменьшение',
        'none'      => 'Без эффекта',
    ];
    echo '<select name="promo_banner_effect_out">';
    foreach ($effects as $key => $label) {
        echo '<option value="' . esc_attr($key) . '" ' . selected($value, $key, false) . '>' . esc_html($label) . '</option>';
    }
    echo '</select>';
    echo '<p class="description">Эффект при закрытии баннера.</p>';
}

function promo_banner_effect_duration_callback() {
    $value = get_option('promo_banner_effect_duration', 0.22);
    ?>
    <input type="number" name="promo_banner_effect_duration" value="<?php echo esc_attr($value); ?>" min="0" max="2" step="0.01">
    <p class="description">Длительность анимации в секундах (по умолчанию 0.22).</p>
    <?php
}

function promo_banner_rotate_callback() {
    $value = get_option('promo_banner_rotate', 0);
    ?>
    <input type="number" name="promo_banner_rotate" value="<?php echo esc_attr($value); ?>" min="-90" max="90" step="1">
    <p class="description">Угол поворота баннера в градусах (от -90 до 90).</p>
    <?php
}

function promo_banner_max_width_callback() {
    $value = get_option('promo_banner_max_width', 890);
    ?>
    <input type="number" name="promo_banner_max_width" value="<?php echo esc_attr($value); ?>" min="200" max="2000" step="10">
    <p class="description">Максимальная ширина баннера в пикселях.</p>
    <?php
}

// 4. Рендер страницы настроек с кнопкой сброса
function promo_banner_render_settings_page() {
    // Обработка сброса
    if (isset($_POST['promo_banner_reset']) && check_admin_referer('promo_banner_reset_action', 'promo_banner_reset_nonce')) {
        // Значения по умолчанию
        $defaults = [
            'promo_banner_enabled' => 1,
            'promo_banner_anchor_selector' => '#promo-block',
            'promo_banner_storage_type' => 'localStorage',
            'promo_banner_expire_hours' => 24,
            'promo_banner_display_mode' => 'standard',
            'promo_banner_position' => 'bottom-center',
            'promo_banner_border_radius' => 8,
            'promo_banner_show_only_home' => 0,
            'promo_banner_delay' => 0,
            'promo_banner_title' => '📢 Важная информация',
            'promo_banner_description' => 'На сайте есть специальные предложения и акции, которые могут затеряться среди других материалов.<br>Перейдите в промо-блок, чтобы ничего не пропустить.',
            'promo_banner_close_on_click_outside' => 0,
            'promo_banner_use_custom_colors' => 0,
            'promo_banner_bg_color' => '#0066cc',
            'promo_banner_text_color' => '#ffffff',
            'promo_banner_title_color' => '',
            'promo_banner_desc_color' => '',
            'promo_banner_go_bg' => '',
            'promo_banner_go_text' => '',
            'promo_banner_close_bg' => '',
            'promo_banner_close_text' => '',
            'promo_banner_effect_in' => 'fade',
            'promo_banner_effect_out' => 'fade',
            'promo_banner_effect_duration' => 0.22,
            'promo_banner_rotate' => 0,
            'promo_banner_max_width' => 890,
            'promo_banner_go_button_enabled' => 1,
            'promo_banner_go_button_text' => 'Перейти к акции',
            'promo_banner_close_button_enabled' => 1,
            'promo_banner_close_button_text' => 'Закрыть',
        ];
        foreach ($defaults as $key => $value) {
            update_option($key, $value);
        }
        echo '<div class="notice notice-success"><p>Настройки сброшены к значениям по умолчанию.</p></div>';
    }
    ?>
    <div class="wrap">
        <h1>Настройки промо-баннера</h1>
        <form method="post" action="options.php">
            <?php
            settings_fields('promo_banner_options_group');
            do_settings_sections('promo-banner-settings');
            submit_button('Сохранить изменения');
            ?>
        </form>

        <hr>
        <h2>Сброс настроек</h2>
        <form method="post" action="">
            <?php wp_nonce_field('promo_banner_reset_action', 'promo_banner_reset_nonce'); ?>
            <input type="hidden" name="promo_banner_reset" value="1">
            <?php submit_button('Сбросить все настройки по умолчанию', 'secondary', 'submit', false); ?>
        </form>
    </div>
    <?php
}

// 5. Вывод баннера в подвале сайта
add_action('wp_footer', 'promo_info_banner', 100);
function promo_info_banner() {
    if (is_admin()) return;

    // Глобальная проверка: если баннер отключён – не показываем
    if (!get_option('promo_banner_enabled', 1)) {
        return;
    }

    // Если включена опция "только на главной" и мы не на главной – выходим
    if (get_option('promo_banner_show_only_home', 0) && !is_front_page() && !is_home()) {
        return;
    }

    // Читаем настройки из БД
    $storage_type  = get_option('promo_banner_storage_type', 'localStorage');
    $expire_hours  = (float) get_option('promo_banner_expire_hours', 24);
    $display_mode  = get_option('promo_banner_display_mode', 'standard');
    $position      = get_option('promo_banner_position', 'bottom-center');
    $border_radius = (int) get_option('promo_banner_border_radius', 8);
    $delay         = (float) get_option('promo_banner_delay', 0);
    $title         = get_option('promo_banner_title', '📢 Важная информация');
    $description   = get_option('promo_banner_description', 'На сайте есть специальные предложения и акции, которые могут затеряться среди других материалов.<br>Перейдите в промо-блок, чтобы ничего не пропустить.');
    
    $close_on_click_outside = (bool) get_option('promo_banner_close_on_click_outside', 0);
    $use_custom_colors = (bool) get_option('promo_banner_use_custom_colors', 0);
    $bg_color = get_option('promo_banner_bg_color', '#0066cc');
    $text_color = get_option('promo_banner_text_color', '#ffffff');

    $title_color = get_option('promo_banner_title_color', '');
    $desc_color = get_option('promo_banner_desc_color', '');
    $go_bg = get_option('promo_banner_go_bg', '');
    $go_text = get_option('promo_banner_go_text', '');
    $close_bg = get_option('promo_banner_close_bg', '');
    $close_text = get_option('promo_banner_close_text', '');

    $effect_in = get_option('promo_banner_effect_in', 'fade');
    $effect_out = get_option('promo_banner_effect_out', 'fade');
    $effect_duration = (float) get_option('promo_banner_effect_duration', 0.22);

    $rotate = (int) get_option('promo_banner_rotate', 0);
    $max_width = (int) get_option('promo_banner_max_width', 890);

    $go_enabled = (bool) get_option('promo_banner_go_button_enabled', 1);
    $go_text_custom = get_option('promo_banner_go_button_text', 'Перейти к акции');
    $close_enabled = (bool) get_option('promo_banner_close_button_enabled', 1);
    $close_text_custom = get_option('promo_banner_close_button_text', 'Закрыть');

    $anchor_selector = get_option('promo_banner_anchor_selector', '#promo-block');

    // Конфигурация для JavaScript
    $config = array(
        'anchorSelector' => $anchor_selector,
        'homeUrl'        => home_url('/'),
        'storageKey'     => 'promo_banner_hidden',
        'expireHours'    => $expire_hours,
        'storageType'    => $storage_type,
        'displayMode'    => $display_mode,
        'position'       => $position,
        'borderRadius'   => $border_radius,
        'delay'          => $delay,
        'title'          => $title,
        'description'    => $description,
        'closeOnClickOutside' => $close_on_click_outside,
        'useCustomColors' => $use_custom_colors,
        'bgColor'        => $bg_color,
        'textColor'      => $text_color,
        'titleColor'     => $title_color,
        'descColor'      => $desc_color,
        'goBg'           => $go_bg,
        'goText'         => $go_text,
        'closeBg'        => $close_bg,
        'closeText'      => $close_text,
        'effectIn'       => $effect_in,
        'effectOut'      => $effect_out,
        'effectDuration' => $effect_duration,
        'rotate'         => $rotate,
        'maxWidth'       => $max_width,
        'goButtonEnabled' => $go_enabled,
        'goButtonText'    => $go_text_custom,
        'closeButtonEnabled' => $close_enabled,
        'closeButtonText'    => $close_text_custom,
    );
    $cfg_json = wp_json_encode($config);
    $host_id = 'promo-banner-host';
    ?>

    <div id="<?php echo $host_id; ?>" aria-hidden="true"></div>

    <script>
    (function() {
        const CONFIG = <?php echo $cfg_json; ?>;
        const HOST_ID = '<?php echo $host_id; ?>';
        const host = document.getElementById(HOST_ID);
        if (!host) return;

        const storage = CONFIG.storageType === 'sessionStorage' ? sessionStorage : localStorage;

        function isExpired(timestamp) {
            const now = Date.now();
            const diffHours = (now - timestamp) / (1000 * 60 * 60);
            return diffHours >= CONFIG.expireHours;
        }

        function getStored() {
            try {
                const data = storage.getItem(CONFIG.storageKey);
                if (!data) return null;
                const parsed = JSON.parse(data);
                if (parsed && parsed.timestamp && !isExpired(parsed.timestamp)) {
                    return parsed;
                } else {
                    storage.removeItem(CONFIG.storageKey);
                    return null;
                }
            } catch (e) {
                return null;
            }
        }

        function setStored() {
            try {
                const data = { timestamp: Date.now() };
                storage.setItem(CONFIG.storageKey, JSON.stringify(data));
            } catch (e) {}
        }

        if (getStored()) {
            host.style.display = 'none';
            return;
        }

        const useShadow = !!host.attachShadow;
        const root = useShadow ? host.attachShadow({ mode: 'open' }) : host;

        let modeClass = '';
        if (!CONFIG.useCustomColors) {
            switch (CONFIG.displayMode) {
                case 'noticeable': modeClass = 'noticeable'; break;
                case 'very_noticeable': modeClass = 'very-noticeable'; break;
                case 'dark': modeClass = 'dark'; break;
                case 'minimal': modeClass = 'minimal'; break;
                case 'accent': modeClass = 'accent'; break;
                case 'msdos': modeClass = 'msdos'; break;
                case 'action': modeClass = 'action'; break;
                case 'danger': modeClass = 'danger'; break;
                case 'important': modeClass = 'important'; break;
                case 'glass': modeClass = 'glass'; break;
                case 'neon': modeClass = 'neon'; break;
                case 'elegant': modeClass = 'elegant'; break;
                case 'corporate': modeClass = 'corporate'; break;
                default: modeClass = 'standard';
            }
        } else {
            modeClass = 'custom-colors';
        }

        const positionClass = 'position-' + CONFIG.position;
        const effectInClass = 'effect-in-' + CONFIG.effectIn;
        const effectOutClass = 'effect-out-' + CONFIG.effectOut;
        const animDuration = CONFIG.effectDuration;
        const rotateAngle = CONFIG.rotate;

        let customStyle = '';
        if (CONFIG.useCustomColors) {
            let cardBg = CONFIG.bgColor;
            let cardText = CONFIG.textColor;
            let titleColor = CONFIG.titleColor || cardText;
            let descColor = CONFIG.descColor || cardText;
            let goBg = CONFIG.goBg || cardText;
            let goText = CONFIG.goText || cardBg;
            let closeBg = CONFIG.closeBg || 'transparent';
            let closeText = CONFIG.closeText || cardText;

            customStyle = `
                <style>
                    .cc-card.custom-colors {
                        background: ${cardBg} !important;
                        color: ${cardText} !important;
                        border: 1px solid rgba(0,0,0,0.1);
                        box-shadow: 0 10px 25px rgba(0,0,0,0.2);
                    }
                    .cc-card.custom-colors .cc-title {
                        color: ${titleColor} !important;
                    }
                    .cc-card.custom-colors .cc-desc {
                        color: ${descColor} !important;
                    }
                    .cc-card.custom-colors .cc-btn.go {
                        background: ${goBg} !important;
                        color: ${goText} !important;
                        border: none;
                    }
                    .cc-card.custom-colors .cc-btn.ghost {
                        background: ${closeBg} !important;
                        color: ${closeText} !important;
                        border: 1px solid ${closeText} !important;
                    }
                    .cc-card.custom-colors .cc-btn.ghost:hover {
                        background: ${closeText}20 !important;
                    }
                </style>
            `;
        }

        const effectStyles = `
            <style>
                .cc-card {
                    transition-property: transform, opacity;
                    transition-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1);
                }
                .effect-in-fade.visible {
                    opacity: 1 !important;
                }
                .effect-in-slide-up.visible {
                    transform: translateY(0) rotate(${rotateAngle}deg) !important;
                }
                .effect-in-slide-down.visible {
                    transform: translateY(0) rotate(${rotateAngle}deg) !important;
                }
                .effect-in-zoom.visible {
                    transform: scale(1) rotate(${rotateAngle}deg) !important;
                }
                .effect-in-fade {
                    opacity: 0;
                }
                .effect-in-slide-up {
                    opacity: 1;
                    transform: translateY(30px) rotate(${rotateAngle}deg);
                }
                .effect-in-slide-down {
                    opacity: 1;
                    transform: translateY(-30px) rotate(${rotateAngle}deg);
                }
                .effect-in-zoom {
                    opacity: 1;
                    transform: scale(0.8) rotate(${rotateAngle}deg);
                }
                .effect-out-fade.hide {
                    opacity: 0 !important;
                }
                .effect-out-slide-up.hide {
                    opacity: 0 !important;
                    transform: translateY(-30px) rotate(${rotateAngle}deg) !important;
                }
                .effect-out-slide-down.hide {
                    opacity: 0 !important;
                    transform: translateY(30px) rotate(${rotateAngle}deg) !important;
                }
                .effect-out-zoom.hide {
                    opacity: 0 !important;
                    transform: scale(0.8) rotate(${rotateAngle}deg) !important;
                }
                .cc-card {
                    transition-duration: ${animDuration}s;
                }
            </style>
        `;

        let buttonsHtml = '';
        if (CONFIG.closeButtonEnabled) {
            buttonsHtml += `<button class="cc-btn ghost" id="closeBtn">${CONFIG.closeButtonText}</button>`;
        }
        if (CONFIG.goButtonEnabled) {
            buttonsHtml += `<button class="cc-btn go" id="goToBtn">${CONFIG.goButtonText}</button>`;
        }

        const template = `
            <style>
                :host { all: initial; font-family: Arial, sans-serif; box-sizing: border-box; }
                .cc-wrap {
                    position: fixed;
                    left: 0;
                    right: 0;
                    display: flex;
                    pointer-events: none;
                    z-index: 2147483646;
                    margin: 18px;
                }
                .cc-wrap.position-bottom-center {
                    bottom: 0;
                    justify-content: center;
                }
                .cc-wrap.position-bottom-left {
                    bottom: 0;
                    left: 0;
                    justify-content: flex-start;
                }
                .cc-wrap.position-bottom-right {
                    bottom: 0;
                    right: 0;
                    justify-content: flex-end;
                }
                .cc-wrap.position-top-center {
                    top: 0;
                    justify-content: center;
                }
                .cc-wrap.position-top-left {
                    top: 0;
                    left: 0;
                    justify-content: flex-start;
                }
                .cc-wrap.position-top-right {
                    top: 0;
                    right: 0;
                    justify-content: flex-end;
                }
                .cc-wrap.position-center-center {
                    top: 50%;
                    transform: translateY(-50%);
                    justify-content: center;
                }
                .cc-wrap.position-center-left {
                    top: 50%;
                    transform: translateY(-50%);
                    left: 0;
                    justify-content: flex-start;
                }
                .cc-wrap.position-center-right {
                    top: 50%;
                    transform: translateY(-50%);
                    right: 0;
                    justify-content: flex-end;
                }
                .cc-card {
                    pointer-events: auto;
                    max-width: ${CONFIG.maxWidth}px;
                    width: calc(100% - 32px);
                    border-radius: ${CONFIG.borderRadius}px;
                    padding: 14px 16px;
                    display: flex;
                    gap: 14px;
                    align-items: center;
                    transform-origin: center center;
                }
                .cc-card.standard {
                    background: rgba(255, 255, 255, 0.9);
                    color: #1e293b;
                    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
                    backdrop-filter: blur(8px);
                    -webkit-backdrop-filter: blur(8px);
                    border: 1px solid rgba(255, 255, 255, 0.3);
                }
                .cc-card.noticeable {
                    background: linear-gradient(135deg, #e6f0ff, #b8d9ff);
                    color: #003b6f;
                    border: 1px solid #7aa7ff;
                    box-shadow: 0 12px 30px rgba(0, 100, 200, 0.2);
                    backdrop-filter: none;
                }
                .cc-card.very-noticeable {
                    background: linear-gradient(145deg, #ffb347, #ff8c42);
                    color: #2c1b0f;
                    border: 1px solid #ffd966;
                    box-shadow: 0 0 25px rgba(255, 140, 0, 0.6);
                    animation: gentlePulse 2s infinite ease-in-out;
                }
                .cc-card.dark {
                    background: #1e293b;
                    color: #f1f5f9;
                    border: 1px solid #334155;
                    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
                }
                .cc-card.minimal {
                    background: #ffffff;
                    color: #0f172a;
                    border: 1px solid #cbd5e1;
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
                }
                .cc-card.accent {
                    background: #8b5cf6;
                    color: #ffffff;
                    border: 1px solid #a78bfa;
                    box-shadow: 0 12px 30px rgba(139, 92, 246, 0.4);
                }
                .cc-card.msdos {
                    background: #000000;
                    color: #00ff00;
                    font-family: 'Courier New', monospace;
                    border: 2px solid #00ff00;
                    box-shadow: 0 0 10px #00ff00;
                }
                .cc-card.action {
                    background: #ffd700;
                    color: #b22222;
                    font-weight: bold;
                    border: 2px solid #b22222;
                    box-shadow: 0 0 15px rgba(178, 34, 34, 0.5);
                }
                .cc-card.danger {
                    background: #dc143c;
                    color: #ffffff;
                    font-weight: bold;
                    border: 2px solid #ffffff;
                    box-shadow: 0 0 15px rgba(220, 20, 60, 0.7);
                }
                .cc-card.important {
                    background: #1e3a8a;
                    color: #ffffff;
                    font-weight: bold;
                    border: 2px solid #ffffff;
                    box-shadow: 0 0 15px rgba(30, 58, 138, 0.7);
                }
                .cc-card.glass {
                    background: rgba(255, 255, 255, 0.2);
                    backdrop-filter: blur(10px);
                    -webkit-backdrop-filter: blur(10px);
                    color: #1e293b;
                    border: 1px solid rgba(255, 255, 255, 0.5);
                    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
                }
                .cc-card.neon {
                    background: #0f172a;
                    color: #0ff;
                    border: 2px solid #0ff;
                    box-shadow: 0 0 20px #0ff, inset 0 0 10px #0ff;
                    text-shadow: 0 0 5px #0ff;
                }
                .cc-card.elegant {
                    background: #f8fafc;
                    color: #334155;
                    border: 1px solid #cbd5e1;
                    box-shadow: 0 20px 35px -8px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.02);
                    font-family: 'Georgia', serif;
                }
                .cc-card.corporate {
                    background: #1e293b;
                    color: #ffffff;
                    border-left: 5px solid #3b82f6;
                    box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3);
                }
                @keyframes gentlePulse {
                    0% { transform: scale(1); box-shadow: 0 0 25px rgba(255, 140, 0, 0.6); }
                    50% { transform: scale(1.01); box-shadow: 0 0 35px rgba(255, 140, 0, 0.8); }
                    100% { transform: scale(1); box-shadow: 0 0 25px rgba(255, 140, 0, 0.6); }
                }
                .cc-text {
                    flex: 1;
                    min-width: 0;
                }
                .cc-title {
                    margin: 0;
                    font-weight: 600;
                    font-size: 16px;
                }
                .cc-desc {
                    margin: 6px 0 0 0;
                    font-size: 13px;
                    line-height: 1.5;
                }
                .cc-actions {
                    display: flex;
                    gap: 8px;
                    align-items: center;
                    flex-wrap: wrap;
                }
                .cc-btn {
                    background: #0066cc;
                    color: #fff;
                    border: 0;
                    padding: 10px 16px;
                    border-radius: 4px;
                    cursor: pointer;
                    font-weight: 600;
                    font-size: 13px;
                    transition: background 0.15s;
                }
                .cc-btn:hover {
                    background: #004999;
                }
                .cc-btn.ghost {
                    background: transparent;
                    color: #444;
                    border: 1px solid rgba(0, 102, 204, 0.3);
                    padding: 8px 14px;
                }
                .cc-btn.ghost:hover {
                    background: rgba(0, 102, 204, 0.05);
                }
                .cc-card.noticeable .cc-btn.ghost,
                .cc-card.very-noticeable .cc-btn.ghost,
                .cc-card.dark .cc-btn.ghost,
                .cc-card.accent .cc-btn.ghost,
                .cc-card.msdos .cc-btn.ghost,
                .cc-card.action .cc-btn.ghost,
                .cc-card.danger .cc-btn.ghost,
                .cc-card.important .cc-btn.ghost,
                .cc-card.glass .cc-btn.ghost,
                .cc-card.neon .cc-btn.ghost,
                .cc-card.elegant .cc-btn.ghost,
                .cc-card.corporate .cc-btn.ghost {
                    color: white;
                    border-color: white;
                }
                .cc-card.noticeable .cc-btn.ghost:hover,
                .cc-card.very-noticeable .cc-btn.ghost:hover,
                .cc-card.dark .cc-btn.ghost:hover,
                .cc-card.accent .cc-btn.ghost:hover,
                .cc-card.msdos .cc-btn.ghost:hover,
                .cc-card.action .cc-btn.ghost:hover,
                .cc-card.danger .cc-btn.ghost:hover,
                .cc-card.important .cc-btn.ghost:hover,
                .cc-card.glass .cc-btn.ghost:hover,
                .cc-card.neon .cc-btn.ghost:hover,
                .cc-card.elegant .cc-btn.ghost:hover,
                .cc-card.corporate .cc-btn.ghost:hover {
                    background: rgba(255, 255, 255, 0.2);
                }
                .cc-card.minimal .cc-btn.ghost {
                    color: #0f172a;
                    border-color: #0f172a;
                }
                @media (max-width: 520px) {
                    .cc-card {
                        flex-direction: column;
                        align-items: stretch;
                        gap: 10px;
                        padding: 12px;
                    }
                    .cc-actions {
                        justify-content: flex-end;
                    }
                }
            </style>
            ${effectStyles}
            ${customStyle}

            <div class="cc-wrap ${positionClass}" id="bannerWrap">
                <div class="cc-card ${modeClass} ${effectInClass} ${effectOutClass}" id="bannerCard" role="alert" style="transform: rotate(${rotateAngle}deg);">
                    <div class="cc-text">
                        <p class="cc-title">${CONFIG.title}</p>
                        <div class="cc-desc">${CONFIG.description}</div>
                    </div>
                    <div class="cc-actions">
                        ${buttonsHtml}
                    </div>
                </div>
            </div>
        `;

        root.innerHTML = template;

        const $ = (sel) => (useShadow ? root.querySelector(sel) : host.querySelector(sel));
        const bannerCard = $('#bannerCard');
        const closeBtn = $('#closeBtn');
        const goToBtn = $('#goToBtn');

        function showBanner() {
            if (!bannerCard) return;
            bannerCard.classList.add('visible');
        }

        function hideBanner() {
            if (!bannerCard) return;
            bannerCard.classList.remove('visible');
            bannerCard.classList.add('hide');
            setStored();
            setTimeout(() => {
                if (host && host.style) host.style.display = 'none';
            }, CONFIG.effectDuration * 1000 + 50);
        }

        function isHomePage() {
            const currentPath = window.location.pathname.replace(/\/$/, '');
            const homePath = new URL(CONFIG.homeUrl).pathname.replace(/\/$/, '');
            return currentPath === homePath;
        }

        if (goToBtn) {
            goToBtn.addEventListener('click', () => {
                setStored();
                if (isHomePage()) {
                    const anchor = document.querySelector(CONFIG.anchorSelector);
                    if (anchor) {
                        anchor.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    }
                    hideBanner();
                } else {
                    bannerCard.classList.remove('visible');
                    bannerCard.classList.add('hide');
                    setTimeout(() => {
                        window.location.href = CONFIG.homeUrl + CONFIG.anchorSelector;
                    }, CONFIG.effectDuration * 1000);
                }
            });
        }

        if (closeBtn) {
            closeBtn.addEventListener('click', hideBanner);
        }

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && host.style.display !== 'none') {
                hideBanner();
            }
        });

        if (CONFIG.closeOnClickOutside) {
            document.addEventListener('click', (e) => {
                if (host.style.display === 'none') return;
                const isClickInside = bannerCard && (bannerCard.contains(e.target) || e.target === bannerCard);
                if (!isClickInside) {
                    hideBanner();
                }
            });
        }

        if (CONFIG.delay > 0) {
            setTimeout(showBanner, CONFIG.delay * 1000);
        } else {
            showBanner();
        }
    })();
    </script>

    <?php
}
Add the code to functions.php.
A 'Settings → Promo banner' page will appear in the admin.
You can configure: enable/disable, CSS anchor, storage type, hide duration, display mode (14 variants), position, border radius, show only on home page, delay, title and description text, buttons, colors, effects, rotation, width.
The banner is automatically output in the footer.
🗺️

Sitemap shortcode [sitemap]

Hierarchical sitemap with CSS tree lines, collapsible branches, Schema.org SEO markup, and caching.

<?php
/**
 * Plugin Name: Карта сайта 
 * Version: 4.4.3
 * Description: Шорткод [sitemap] – иерархическая карта сайта с CSS-линиями, SEO-разметкой, исключением страниц WooCommerce, кешированием и ручным обновлением для администраторов.
 * Author: Ваш отдел разработки
 * Text Domain: missmebel-sitemap
 */

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

// ====================== Ключ кеша ======================
if ( ! defined( 'SITEMAP_CACHE_OPTION' ) ) {
    define( 'SITEMAP_CACHE_OPTION', 'sitemap_cache_v443' );
}

// ====================== Локализация ======================
add_action( 'init', 'missmebel_sitemap_load_textdomain' );
function missmebel_sitemap_load_textdomain() {
    load_plugin_textdomain( 'missmebel-sitemap', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}

// ====================== Константа максимальной глубины рекурсии =================
if ( ! defined( 'SITEMAP_MAX_DEPTH' ) ) {
    define( 'SITEMAP_MAX_DEPTH', 10 );
}

// ====================== Шорткод [sitemap] ======================
add_shortcode( 'sitemap', 'missmebel_sitemap_shortcode' );
function missmebel_sitemap_shortcode() {
    $cached = get_option( SITEMAP_CACHE_OPTION, '' );
    if ( empty( $cached ) ) {
        $cached = missmebel_generate_sitemap();
        update_option( SITEMAP_CACHE_OPTION, $cached );
    }

    $styles = '
    <style>
        .mismebel-page-wrapper {
            background: #e9ecef;
            width: 100%;
            min-height: 100vh;
            padding: 24px 0 32px;
            box-sizing: border-box;
        }
        .mismebel-page-content {
            max-width: 1400px;
            margin: 0 auto;
            padding: 0 24px;
        }
        @media (max-width: 767px) {
            .mismebel-page-wrapper {
                padding: 10px 0 28px;
            }
            .mismebel-page-content {
                padding: 0 10px !important;
            }
.sitemap-tree {padding-left: 0rem !important;}
.sitemap-page__title {margin-bottom: 1rem !important;}
        }

        .sitemap-page__title {
            font-weight: 600;
            font-size: clamp(1.8rem, 4vw, 2.6rem);
            line-height: 1.3;
            margin-bottom: 2rem;
            color: #0f172a;
        }

        .sitemap-container {
            font-family: inherit;
            color: #1e293b;
            line-height: 1.6;
            word-break: break-word;
        }
        .sitemap-block {
            border: 1px solid #e2e8f0;
            border-radius: 0.75rem;
            background: #fff;
            padding: 1.5rem 1.8rem;
            margin-bottom: 1.8rem;
        }
        .sitemap-block__title {
            font-weight: 600;
            font-size: 1.5rem;
            margin-bottom: 1rem;
            color: #0f172a;
            border-bottom: 1px solid #f1f5f9;
            padding-bottom: 0.5rem;
        }
        .sitemap-section-title {
            font-weight: 600;
            font-size: 1.15rem;
            margin: 1rem 0 0.5rem;
            color: #0f172a;
        }

        .sitemap-tree {
            list-style: none;
            padding-left: 1.8rem;
            margin: 0;
            position: relative;
        }
        .sitemap-tree li {
            position: relative;
            padding-left: 0.4rem;
            margin: 0.15rem 0;
            line-height: 1.6;
        }

        .sitemap-tree ul {
            list-style: none;
            padding-left: 1.8rem;
            margin: 0.15rem 0 0 0;
            position: relative;
        }
        .sitemap-tree ul::before {
            content: "";
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            border-left: 1px solid #cbd5e1;
        }

        .sitemap-tree > li::before,
        .sitemap-tree > li:last-child::before {
            content: none;
            width: 0;
            height: 0;
            border: none;
        }

        .sitemap-toggle {
            position: relative;
            left: auto;
            top: auto;
            display: inline-block;
            background: transparent;
            border: none;
            border-radius: 0;
            width: 1.2rem;
            height: 1.2rem;
            font-size: 0.85rem;
            line-height: 1.2rem;
            text-align: center;
            cursor: pointer;
            color: #0f172a;
            user-select: none;
            margin-right: 0.2rem;
            vertical-align: middle;
            padding: 0;
            outline: none;
        }
        .sitemap-toggle:focus,
        .sitemap-toggle:hover {
            background: transparent;
            border: none;
            outline: none;
            color: #0f172a;
        }

        .sitemap-folder::before { content: "📁 "; }
        .sitemap-file::before { content: "📄 "; }

        .sitemap-link {
            color: inherit;
            text-decoration: none !important;
            transition: color 0.2s;
        }
        .sitemap-link:hover {
            color: #046BD2;
            text-decoration: none !important;
        }
        .sitemap-link:focus {
            color: inherit;
            outline: none;
        }

        .sitemap-count {
            color: #cbd5e1;
            font-size: 0.78em;
            font-weight: 400;
            margin-left: 0.4rem;
            white-space: nowrap;
            vertical-align: baseline;
        }

        .sitemap-actions { margin-top: 1.5rem; }
        .sitemap-btn {
            display: inline-block;
            background: #046BD2;
            color: #fff;
            border: none;
            padding: 0.6rem 1.5rem;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            font-size: 1rem;
            text-decoration: none;
            transition: background 0.2s;
        }
        .sitemap-btn:hover,
        .sitemap-btn:focus,
        .sitemap-btn:active {
            background: #0556a3;
            color: #fff;
            outline: none;
        }
        .sitemap-btn:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        @media (max-width: 768px) {
            .sitemap-block {
                padding: 1rem;
            }
        }
    </style>';

    $html  = '<div class="mismebel-page-wrapper"><div class="mismebel-page-content">';
    $html .= '<h1 class="sitemap-page__title">' . esc_html__( 'Карта сайта', 'missmebel-sitemap' ) . '</h1>';
    $html .= '<div class="sitemap-container">' . $cached . '</div>';

    if ( current_user_can( 'manage_options' ) ) {
        $ajax_url = admin_url( 'admin-ajax.php' );
        $nonce    = wp_create_nonce( 'sitemap_rebuild' );

        $html .= '<div class="sitemap-actions">';
        $html .= '<button id="sitemap-rebuild-btn" class="sitemap-btn">' . esc_html__( 'Обновить карту сайта', 'missmebel-sitemap' ) . '</button>';
        $html .= '<span id="sitemap-status" style="display:none;margin-left:10px;"></span>';
        $html .= '</div>';

        $html .= '<script>
            (function() {
                var btn = document.getElementById("sitemap-rebuild-btn");
                if (!btn) return;

                btn.addEventListener("click", async function() {
                    btn.disabled = true;
                    var status = document.getElementById("sitemap-status");
                    status.style.display = "inline";
                    status.textContent = "' . esc_js( __( 'Обновление…', 'missmebel-sitemap' ) ) . '";

                    try {
                        var formData = new FormData();
                        formData.append("action", "rebuild_sitemap");
                        formData.append("_wpnonce", "' . esc_js( $nonce ) . '");

                        var response = await fetch("' . esc_url( $ajax_url ) . '", {
                            method: "POST",
                            body: formData
                        });

                        if (!response.ok) {
                            throw new Error("HTTP error " + response.status);
                        }

                        var result = await response.json();
                        if (result.success) {
                            document.querySelector(".sitemap-container").innerHTML = result.data;
                            initTreeToggles();
                            status.textContent = "' . esc_js( __( '✓ Обновлено', 'missmebel-sitemap' ) ) . '";
                        } else {
                            status.textContent = "' . esc_js( __( 'Ошибка обновления', 'missmebel-sitemap' ) ) . '";
                        }
                    } catch (err) {
                        status.textContent = "' . esc_js( __( 'Сетевая ошибка', 'missmebel-sitemap' ) ) . '";
                    }

                    btn.disabled = false;
                    setTimeout(function() { status.style.display = "none"; }, 3000);
                });
            })();
        </script>';
    }

    $html .= '</div></div>';

    $script = '
    <script>
        function initTreeToggles() {
            document.querySelectorAll(".sitemap-toggle").forEach(function(btn) {
                btn.removeEventListener("click", toggleHandler);
                btn.addEventListener("click", toggleHandler);
            });
        }

        function toggleHandler(e) {
            e.preventDefault();
            var btn = e.currentTarget;
            var li = btn.closest("li");
            var childUl = li ? li.querySelector(":scope > ul") : null;
            if (!childUl) return;

            if (childUl.style.display === "none") {
                childUl.style.display = "block";
                btn.textContent = "–";
            } else {
                childUl.style.display = "none";
                btn.textContent = "+";
            }
        }

        document.addEventListener("DOMContentLoaded", function() {
            initTreeToggles();
        });
    </script>';

    return $styles . $html . $script;
}

// ====================== AJAX-обработчик ======================
add_action( 'wp_ajax_rebuild_sitemap', 'missmebel_rebuild_sitemap' );
function missmebel_rebuild_sitemap() {
    check_ajax_referer( 'sitemap_rebuild' );
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'Недостаточно прав', 'missmebel-sitemap' ) );
    }

    $html = missmebel_generate_sitemap();
    update_option( SITEMAP_CACHE_OPTION, $html );
    wp_send_json_success( $html );
}

// ====================== Генерация HTML карты ======================
function missmebel_generate_sitemap() {
    $html = '';

    $exclude_page_ids = [];
    $current_page_id = get_queried_object_id();
    if ( $current_page_id ) {
        $exclude_page_ids[] = $current_page_id;
    }

    if ( class_exists( 'WooCommerce' ) ) {
        $exclude_page_ids[] = wc_get_page_id( 'cart' );
        $exclude_page_ids[] = wc_get_page_id( 'checkout' );
        $exclude_page_ids[] = wc_get_page_id( 'myaccount' );
    }

    $exclude_page_ids = array_unique( array_filter( $exclude_page_ids ) );

    $html .= '<div class="sitemap-block">';
    $html .= '<h2 class="sitemap-block__title">' . esc_html__( 'Страницы и записи', 'missmebel-sitemap' ) . '</h2>';

    $pages = get_pages( [ 'sort_column' => 'menu_order,post_title' ] );
    $pages = array_filter( $pages, function ( $page ) use ( $exclude_page_ids ) {
        return ! in_array( $page->ID, $exclude_page_ids, true );
    } );

    $page_tree = missmebel_build_page_tree( $pages, 0 );
    if ( ! empty( $page_tree ) ) {
        $html .= '<h3 class="sitemap-section-title">' . esc_html__( 'Страницы', 'missmebel-sitemap' ) . '</h3>';
        $html .= '<ul class="sitemap-tree" itemscope itemtype="https://schema.org/ItemList">';
        $html .= '<meta itemprop="name" content="' . esc_attr__( 'Страницы', 'missmebel-sitemap' ) . '">';
        $html .= missmebel_render_page_tree( $page_tree, true );
        $html .= '</ul>';
    } else {
        $html .= '<p>' . esc_html__( 'Страницы не найдены.', 'missmebel-sitemap' ) . '</p>';
    }

    $categories = get_categories( [ 'hide_empty' => false, 'parent' => 0 ] );
    $html .= '<h3 class="sitemap-section-title">' . esc_html__( 'Записи по рубрикам', 'missmebel-sitemap' ) . '</h3>';
    if ( ! empty( $categories ) ) {
        $html .= '<ul class="sitemap-tree" itemscope itemtype="https://schema.org/ItemList">';
        $html .= '<meta itemprop="name" content="' . esc_attr__( 'Записи по рубрикам', 'missmebel-sitemap' ) . '">';
        $i = 1;
        foreach ( $categories as $cat ) {
            $html .= missmebel_render_category_node( $cat, true, $i++ );
        }
        $html .= '</ul>';
    } else {
        $html .= '<p>' . esc_html__( 'Рубрик не найдено.', 'missmebel-sitemap' ) . '</p>';
    }
    $html .= '</div>';

    $html .= '<div class="sitemap-block">';
    $html .= '<h2 class="sitemap-block__title">' . esc_html__( 'Категории и товары', 'missmebel-sitemap' ) . '</h2>';

    if ( class_exists( 'WooCommerce' ) ) {
        $product_cats = get_terms( [
            'taxonomy'   => 'product_cat',
            'hide_empty' => false,
            'parent'     => 0,
        ] );

        if ( ! empty( $product_cats ) && ! is_wp_error( $product_cats ) ) {
            $html .= '<ul class="sitemap-tree" itemscope itemtype="https://schema.org/ItemList">';
            $html .= '<meta itemprop="name" content="' . esc_attr__( 'Категории и товары', 'missmebel-sitemap' ) . '">';
            $j = 1;
            foreach ( $product_cats as $term ) {
                $html .= missmebel_render_product_category_node( $term, true, $j++ );
            }
            $html .= '</ul>';
        } else {
            $html .= '<p>' . esc_html__( 'Категорий товаров не найдено.', 'missmebel-sitemap' ) . '</p>';
        }
    } else {
        $html .= '<p>' . esc_html__( 'Для отображения товаров необходим WooCommerce.', 'missmebel-sitemap' ) . '</p>';
    }
    $html .= '</div>';

    return $html;
}

// ====================== Вспомогательные функции ======================

function missmebel_build_page_tree( $pages, $parent = 0, $depth = 0 ) {
    if ( $depth >= SITEMAP_MAX_DEPTH ) {
        return [];
    }

    $branch = [];
    foreach ( $pages as $page ) {
        if ( (int) $page->post_parent === (int) $parent ) {
            $children = missmebel_build_page_tree( $pages, $page->ID, $depth + 1 );
            $branch[] = [ 'page' => $page, 'children' => $children ];
        }
    }
    return $branch;
}

function missmebel_render_page_tree( $tree, $root = false, $index = 0, $depth = 0 ) {
    if ( $depth >= SITEMAP_MAX_DEPTH ) {
        return '';
    }

    $html = '';
    $i = 0;

    foreach ( $tree as $node ) {
        $page     = $node['page'];
        $children = $node['children'];
        $url      = wp_make_link_relative( get_permalink( $page ) );

        $li_attr  = '';
        $pos_meta = '';
        if ( $root ) {
            $pos = $index + $i + 1;
            $li_attr  = ' itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"';
            $pos_meta = '<meta itemprop="position" content="' . esc_attr( $pos ) . '">';
        }

        $count_html = '';
        if ( ! empty( $children ) ) {
            $count_html = '<span class="sitemap-count">(' . esc_html( count( $children ) ) . ')</span>';
        }

        $html .= '<li' . $li_attr . '>';
        if ( ! empty( $children ) ) {
            $html .= '<button class="sitemap-toggle" aria-label="' . esc_attr__( 'Развернуть', 'missmebel-sitemap' ) . '">+</button>';
            $html .= '<span class="sitemap-folder"></span>';
            $html .= $pos_meta;
            $html .= '<a href="' . esc_url( $url ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $page->post_title ) . '</span>' . $count_html . '</a>';
            $html .= '<ul style="display:none;">' . missmebel_render_page_tree( $children, false, 0, $depth + 1 ) . '</ul>';
        } else {
            $html .= '<span class="sitemap-file"></span>';
            $html .= $pos_meta;
            $html .= '<a href="' . esc_url( $url ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $page->post_title ) . '</span></a>';
        }
        $html .= '</li>';
        $i++;
    }

    return $html;
}

/**
 * Рендер узла рубрики (категории записей)
 * Исправлен подсчёт: показывает общее количество записей в рубрике и всех дочерних.
 */
function missmebel_render_category_node( $category, $root = false, $index = 0, $depth = 0 ) {
    if ( $depth >= SITEMAP_MAX_DEPTH ) {
        return '';
    }

    $html     = '';
    $children = get_categories( [ 'parent' => $category->term_id, 'hide_empty' => false ] );

    // Записи, непосредственно привязанные к этой рубрике (без дочерних)
    $posts = get_posts( [
        'post_type'   => 'post',
        'numberposts' => -1,
        'orderby'     => 'date',
        'order'       => 'DESC',
        'post_status' => 'publish',
        'tax_query'   => [
            [
                'taxonomy'         => 'category',
                'field'            => 'term_id',
                'terms'            => $category->term_id,
                'include_children' => false,
            ],
        ],
    ] );

    // +++ ИСПРАВЛЕНИЕ: общее количество записей в рубрике и всех дочерних
    $total_posts_query = new WP_Query( [
        'post_type'      => 'post',
        'posts_per_page' => -1,
        'fields'         => 'ids',
        'post_status'    => 'publish',
        'cat'            => $category->term_id,   // включает дочерние рубрики автоматически
    ] );
    $total_post_count = $total_posts_query->found_posts;
    // ---

    $has_children = ! empty( $children ) || ! empty( $posts );

    $li_attr  = '';
    $pos_meta = '';
    if ( $root ) {
        $li_attr  = ' itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"';
        $pos_meta = '<meta itemprop="position" content="' . esc_attr( $index ) . '">';
    }

    $count_html = $total_post_count > 0
        ? '<span class="sitemap-count">(' . esc_html( $total_post_count ) . ')</span>'
        : '';

    $html .= '<li' . $li_attr . '>';
    if ( $has_children ) {
        $html .= '<button class="sitemap-toggle" aria-label="' . esc_attr__( 'Развернуть', 'missmebel-sitemap' ) . '">+</button>';
        $html .= '<span class="sitemap-folder"></span>';
        $html .= $pos_meta;
        $html .= '<a href="' . esc_url( wp_make_link_relative( get_category_link( $category ) ) ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $category->name ) . '</span>' . $count_html . '</a>';
        $html .= '<ul style="display:none;">';

        if ( ! empty( $posts ) ) {
            foreach ( $posts as $post ) {
                $url = wp_make_link_relative( get_permalink( $post ) );
                $html .= '<li><span class="sitemap-file"></span><a href="' . esc_url( $url ) . '" class="sitemap-link">' . esc_html( $post->post_title ) . '</a></li>';
            }
        }

        if ( ! empty( $children ) ) {
            foreach ( $children as $child ) {
                $html .= missmebel_render_category_node( $child, false, 0, $depth + 1 );
            }
        }

        $html .= '</ul>';
    } else {
        $html .= '<span class="sitemap-folder"></span>';
        $html .= $pos_meta;
        $html .= '<a href="' . esc_url( wp_make_link_relative( get_category_link( $category ) ) ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $category->name ) . '</span></a>';
    }
    $html .= '</li>';

    return $html;
}

/**
 * Рендер узла товарной категории (WooCommerce)
 * Исправлен подсчёт: показывает общее количество товаров в категории и всех дочерних.
 */
function missmebel_render_product_category_node( $term, $root = false, $index = 0, $depth = 0 ) {
    if ( $depth >= SITEMAP_MAX_DEPTH ) {
        return '';
    }

    $html     = '';
    $subterms = get_terms( [
        'taxonomy'   => 'product_cat',
        'parent'     => $term->term_id,
        'hide_empty' => false,
    ] );

    // Товары, привязанные НЕПОСРЕДСТВЕННО к этой категории (без дочерних)
    $products = get_posts( [
        'post_type'      => 'product',
        'posts_per_page' => -1,
        'tax_query'      => [
            [
                'taxonomy'         => 'product_cat',
                'field'            => 'term_id',
                'terms'            => $term->term_id,
                'include_children' => false,
            ],
        ],
        'orderby'     => 'title',
        'order'       => 'ASC',
        'post_status' => 'publish',
    ] );

    // +++ ИСПРАВЛЕНИЕ: общее количество товаров в категории и всех её потомках
    $total_query = new WP_Query( [
        'post_type'      => 'product',
        'posts_per_page' => -1,
        'fields'         => 'ids',
        'post_status'    => 'publish',
        'tax_query'      => [
            [
                'taxonomy'         => 'product_cat',
                'field'            => 'term_id',
                'terms'            => $term->term_id,
                'include_children' => true,   // включаем все дочерние категории
            ],
        ],
    ] );
    $total_product_count = $total_query->found_posts;
    // ---

    $has_children = ! empty( $subterms ) || ! empty( $products );

    $li_attr  = '';
    $pos_meta = '';
    if ( $root ) {
        $li_attr  = ' itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"';
        $pos_meta = '<meta itemprop="position" content="' . esc_attr( $index ) . '">';
    }

    $count_html = $total_product_count > 0
        ? '<span class="sitemap-count">(' . esc_html( $total_product_count ) . ')</span>'
        : '';

    $html .= '<li' . $li_attr . '>';
    if ( $has_children ) {
        $html .= '<button class="sitemap-toggle" aria-label="' . esc_attr__( 'Развернуть', 'missmebel-sitemap' ) . '">+</button>';
        $html .= '<span class="sitemap-folder"></span>';
        $html .= $pos_meta;

        $term_url = get_term_link( $term );
        if ( is_wp_error( $term_url ) ) {
            $term_url = '';
        }

        $html .= '<a href="' . esc_url( wp_make_link_relative( $term_url ) ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $term->name ) . '</span>' . $count_html . '</a>';
        $html .= '<ul style="display:none;">';

        if ( ! empty( $products ) ) {
            foreach ( $products as $product ) {
                $url = wp_make_link_relative( get_permalink( $product ) );
                $html .= '<li><span class="sitemap-file"></span><a href="' . esc_url( $url ) . '" class="sitemap-link">' . esc_html( $product->post_title ) . '</a></li>';
            }
        }

        if ( ! empty( $subterms ) && ! is_wp_error( $subterms ) ) {
            foreach ( $subterms as $sub ) {
                $html .= missmebel_render_product_category_node( $sub, false, 0, $depth + 1 );
            }
        }

        $html .= '</ul>';
    } else {
        $html .= '<span class="sitemap-folder"></span>';
        $html .= $pos_meta;

        $term_url = get_term_link( $term );
        if ( is_wp_error( $term_url ) ) {
            $term_url = '';
        }

        $html .= '<a href="' . esc_url( wp_make_link_relative( $term_url ) ) . '" class="sitemap-link" itemprop="url"><span itemprop="name">' . esc_html( $term->name ) . '</span></a>';
    }
    $html .= '</li>';

    return $html;
}
Install as a plugin or add to functions.php.
Use shortcode [sitemap] on any page.
Automatically excludes WooCommerce cart, checkout and my-account pages.
Administrators see a 'Refresh sitemap' button to manually clear the cache.
Supports pages, posts (by category), and WooCommerce products (by category).
🍪

Cookie consent (reliable version)

Cookie consent banner with detailed settings, Shadow DOM, and localStorage.

<?php
/**
 * Cookie consent — финальный, надежный вариант.
 */
add_action('wp_footer', 'cc_final_banner_keephost', 100);
function cc_final_banner_keephost() {
    if (is_admin()) return;

    $privacy_url = esc_url( home_url('/privacy/') );
    $cookies_url = esc_url( home_url('/cookies/') );
    $config = array(
        'privacy' => $privacy_url,
        'cookies' => $cookies_url,
        'cookieName' => 'site_cc_v2',
        'cookieDays' => 365
    );
    $cfg_json = wp_json_encode($config);
    ?>

    <div id="cc-host" aria-hidden="true"></div>

    <script>
    (function(){
      const CONFIG = <?php echo $cfg_json; ?>;
      const HOST_ID = 'cc-host';
      const host = document.getElementById(HOST_ID);
      if(!host){ console.error('CookieConsent: host element not found'); return; }

      const useShadow = !!host.attachShadow;
      const root = useShadow ? host.attachShadow({mode:'open'}) : host;

      // === Template (Shadow DOM or fallback) ===
      const template = `
        <style>
          :host{ all: initial; font-family: Arial; box-sizing:border-box; }
          .cc-wrap{ position:fixed; left:0; right:0; bottom:18px; display:flex; justify-content:center; pointer-events:none; z-index:2147483646; }
          .cc-card{
            pointer-events:auto;
            max-width:980px;
            width:calc(100% - 32px);
            background: rgba(255,255,255,0.84);
            color: #072034;
            border-radius:8px;
            box-shadow: 0 10px 40px rgba(2,6,23,0.18);
            padding:14px 16px;
            display:flex;
            gap:14px;
            align-items:center;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            transform-origin: center bottom;
            transition: transform .22s cubic-bezier(.2,.9,.3,1), opacity .22s cubic-bezier(.2,.9,.3,1);
          }
          .cc-card.hide { transform: translateY(14px); opacity:0; pointer-events:none; }
          .cc-text{ flex:1; min-width:0; }
          .cc-title{ margin:0; font-weight:600; font-size:15px; color:#072034; }
          .cc-desc{ margin:6px 0 0 0; font-size:13px; color:#334155; line-height:1.35; }
          .cc-actions{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
          .cc-btn{ background:#07c; color:#fff; border:0; padding:10px 14px; border-radius:4px; cursor:pointer; font-weight:600; font-size:13px; }
          .cc-btn.ghost{ background:transparent; color:#444; border:1px solid rgba(7,140,204,0.12); padding:8px 12px; border-radius:4px; font-weight: 600; }
          .cc-settings-toggle{ background:#fff; color:#444; border-radius:4px; padding:8px 10px; border:1px solid rgba(7,140,204,0.08); cursor:pointer; }
          .cc-link{ color:#07c; text-decoration:underline; font-size:13px; }
          /* overlay modal */
          .cc-overlay{ position:fixed; left:0; top:0; right:0; bottom:0; display:none; align-items:center; justify-content:center; z-index:2147483647; background:rgba(7,12,20,0.44); }
          .cc-modal{ background: rgba(255,255,255,0.98); border-radius:10px; max-width:720px; width:92%; padding:18px; box-shadow:0 12px 40px rgba(10,20,40,0.35); }
          .cc-modal h3{ margin:0 0 8px 0; font-size:16px; color:#072034; }
          .cc-row{ display:flex; justify-content:space-between; align-items:center; padding:10px 0; border-top:1px solid #f1f5f9; }
          .cc-row:first-of-type{ border-top:0; }
          .cc-toggle input[type="checkbox"]{ width:18px; height:18px; }
          @media (max-width:520px){
            .cc-card{ flex-direction:column; align-items:stretch; gap:10px; padding:12px; }
            .cc-actions{ justify-content:flex-end; }
          }
        </style>

        <div class="cc-wrap" id="ccWrap" role="region" aria-label="Cookie consent">
          <div class="cc-card" id="ccCard" role="dialog" aria-modal="false" aria-labelledby="ccTitle">
            <div class="cc-text">
              <p id="ccTitle" class="cc-title">Мы используем файлы cookie</p>
              <p class="cc-desc">Мы используем куки для работы сайта, аналитики и показа персонализированных предложений. Подробнее: <a class="cc-link" href="${CONFIG.privacy}" target="_blank" rel="noopener">Обработка персональных данных</a> и <a class="cc-link" href="${CONFIG.cookies}" target="_blank" rel="noopener">Обработка файлов cookie</a>.</p>
            </div>

            <div class="cc-actions" aria-hidden="false">
              <button class="cc-btn ghost" id="ccDeclineBtn" title="Отклонить">Отклонить</button>
              <button class="cc-btn" id="ccAcceptBtn" title="Принять всё">Принять всё</button>
              <button class="cc-settings-toggle" id="ccOpenSettings" aria-haspopup="dialog" aria-controls="ccSettings">Настроить</button>
            </div>
          </div>
        </div>

        <div class="cc-overlay" id="ccOverlay" aria-hidden="true">
          <div class="cc-modal" role="dialog" aria-modal="true" aria-labelledby="ccModalTitle" id="ccSettings">
            <h3 id="ccModalTitle">Настройки cookie</h3>
            <p style="margin:0 0 12px 0;color:#475569;font-size:13px">Выберите, какие типы cookie вы разрешаете.</p>

            <div class="cc-row">
              <div>
                <div style="font-weight:600">Необходимые</div>
                <div style="font-size:13px;color:#64748b">Обязательно — корзина, авторизация и базовая работа сайта</div>
              </div>
              <div class="cc-toggle"><input type="checkbox" disabled checked aria-disabled="true"></div>
            </div>

            <div class="cc-row">
              <div>
                <div style="font-weight:600">Аналитика</div>
                <div style="font-size:13px;color:#64748b">Google Analytics, Yandex.Metrika и прочая статистика</div>
              </div>
              <div class="cc-toggle"><input type="checkbox" id="ccOptAnalytics"></div>
            </div>

            <div class="cc-row">
              <div>
                <div style="font-weight:600">Маркетинг / Реклама</div>
                <div style="font-size:13px;color:#64748b">Пиксели и персонализация (Facebook, рекламные сети)</div>
              </div>
              <div class="cc-toggle"><input type="checkbox" id="ccOptMarketing"></div>
            </div>

            <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px">
              <button class="cc-btn ghost" id="ccCancelSettings">Отмена</button>
              <button class="cc-btn" id="ccSaveSettings">Сохранить</button>
            </div>

            <div style="font-size:12px;color:#94a3b8;margin-top:12px">Вы можете в любой момент изменить настройки на странице <a href="${CONFIG.cookies}" target="_blank" rel="noopener" class="cc-link">Обработка файлов cookie</a>.</div>
          </div>
        </div>
      `;

      // вставляем шаблон
      root.innerHTML = template;

      // универсальный селектор
      const q = (sel) => (useShadow ? root.querySelector(sel) : host.querySelector(sel));

      const bannerWrap = q('#ccWrap');
      const ccCard = q('#ccCard');
      const acceptBtn = q('#ccAcceptBtn');
      const declineBtn = q('#ccDeclineBtn');
      const openSettingsBtn = q('#ccOpenSettings');
      const overlay = q('#ccOverlay');
      const saveSettingsBtn = q('#ccSaveSettings');
      const cancelSettingsBtn = q('#ccCancelSettings');
      const optAnalytics = q('#ccOptAnalytics');
      const optMarketing = q('#ccOptMarketing');

      // cookie helpers
      function setConsentCookie(val){
        const d = new Date();
        d.setTime(d.getTime() + (CONFIG.cookieDays*24*60*60*1000));
        if(val === null){
          document.cookie = `${CONFIG.cookieName}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Lax`;
          return;
        }
        document.cookie = `${CONFIG.cookieName}=${encodeURIComponent(JSON.stringify(val))};path=/;expires=${d.toUTCString()};SameSite=Lax`;
      }
      function getConsentCookie(){
        const re = new RegExp('(?:^|; )' + CONFIG.cookieName.replace(/([.*+?^${}()|[\\]\\\\])/g,'\\$1') + '=([^;]*)');
        const m = document.cookie.match(re);
        if(!m) return null;
        try{ return JSON.parse(decodeURIComponent(m[1])); }catch(e){ return null; }
      }

      function alreadyLoadedScript(src){
        if(!src) return false;
        const scripts = Array.from(document.getElementsByTagName('script'));
        return scripts.some(s => s.src && s.src.indexOf(src) !== -1);
      }

      // apply consent: заменяет placeholders на реальные теги
      function applyConsent(consent){
        window.CookieConsent = window.CookieConsent || {};
        window.CookieConsent._consent = consent;

        const placeholders = document.querySelectorAll('script[type="text/plain"][data-cookiecategory], iframe[data-cookiecategory], div[data-cookiecategory]');
        placeholders.forEach(node => {
          const tag = node.tagName.toLowerCase();
          const cat = (node.getAttribute('data-cookiecategory') || 'analytics').toLowerCase();
          const allowed = (cat === 'necessary') ? true : !!consent[cat];
          if(!allowed) return;

          if(tag === 'script'){
            const src = node.getAttribute('data-src');
            const id = node.getAttribute('id') || null;
            const isAsync = node.hasAttribute('data-async');
            const typeAttr = node.getAttribute('data-type') || null;
            if(src && alreadyLoadedScript(src)) return;
            const newScript = document.createElement('script');
            if(id) newScript.id = id;
            if(typeAttr) newScript.type = typeAttr;
            if(isAsync) newScript.async = true;
            if(src){ newScript.src = src; } else { newScript.text = node.innerText || node.textContent || ''; }
            document.head.appendChild(newScript);
          } else if(tag === 'iframe'){
            const src = node.getAttribute('data-src');
            if(src && !node.getAttribute('src')) node.setAttribute('src', src);
          } else if(tag === 'div'){
            const noscript = node.getAttribute('data-noscript');
            const src = node.getAttribute('data-src');
            if(noscript === 'img' && src && node.children.length === 0){
              const img = document.createElement('img');
              img.src = src; img.alt = ''; img.style.position = 'absolute'; img.style.left = '-9999px';
              node.appendChild(img);
            } else if(src && node.children.length === 0){
              const ifr = document.createElement('iframe');
              ifr.setAttribute('src', src);
              ifr.setAttribute('width', node.getAttribute('data-width') || '560');
              ifr.setAttribute('height', node.getAttribute('data-height') || '315');
              ifr.setAttribute('frameborder','0'); ifr.setAttribute('allowfullscreen','');
              node.appendChild(ifr);
            }
          }
        });

        if(consent.analytics && typeof window.initAfterConsent === 'function'){
          try{ window.initAfterConsent(); }catch(e){ console.error('initAfterConsent error', e); }
        }
      }

      // ПЛАН: прятать host (не удалять) — чтобы API всегда был доступен
      function hideBannerKeepHost(){
        try{
          if(ccCard) ccCard.classList.add('hide');
          if(overlay){ overlay.style.display = 'none'; overlay.setAttribute('aria-hidden','true'); }
          setTimeout(function(){
            try{
              if(host && host.style){
                host.style.display = 'none';
                host.setAttribute('aria-hidden','true');
              }
            }catch(e){}
          }, 300);
        }catch(e){
          if(host) host.style.display = 'none';
        }
      }

      // показать (восстановить display), чтобы открыть настройки позже
      function showHostIfHidden(){
        try{
          if(host && host.style && host.style.display === 'none'){
            host.style.display = ''; // восстановить
            host.setAttribute('aria-hidden','false');
            // убрать класс .hide у карточки, чтобы animation сработал при открытии
            if(ccCard) ccCard.classList.remove('hide');
          }
        }catch(e){}
      }

      // Привязка событий к кнопкам и логика
      function bindEvents(){
        if(acceptBtn){
          acceptBtn.addEventListener('click', function(){
            const c = { analytics:true, marketing:true };
            setConsentCookie(c);
            applyConsent(c);
            hideBannerKeepHost();
          });
        }
        if(declineBtn){
          declineBtn.addEventListener('click', function(){
            const c = { analytics:false, marketing:false };
            setConsentCookie(c);
            // not calling applyConsent -> do not load analytics
            hideBannerKeepHost();
          });
        }
        if(openSettingsBtn){
          openSettingsBtn.addEventListener('click', function(){
            // show modal and prefill checkboxes
            const existing = getConsentCookie() || {};
            if(optAnalytics) optAnalytics.checked = !!existing.analytics;
            if(optMarketing) optMarketing.checked = !!existing.marketing;
            if(overlay){ overlay.style.display = 'flex'; overlay.setAttribute('aria-hidden','false'); }
          });
        }
        if(cancelSettingsBtn){
          cancelSettingsBtn.addEventListener('click', function(){
            if(overlay){ overlay.style.display = 'none'; overlay.setAttribute('aria-hidden','true'); }
          });
        }
        if(saveSettingsBtn){
          saveSettingsBtn.addEventListener('click', function(){
            const c = { analytics: !!(optAnalytics && optAnalytics.checked), marketing: !!(optMarketing && optMarketing.checked) };
            setConsentCookie(c);
            applyConsent(c);
            if(overlay){ overlay.style.display = 'none'; overlay.setAttribute('aria-hidden','true'); }
            hideBannerKeepHost();
          });
        }

        document.addEventListener('keydown', function(e){
          if(e.key === 'Escape' && overlay && overlay.style.display === 'flex'){
            overlay.style.display = 'none';
            overlay.setAttribute('aria-hidden','true');
          }
        });
      }

      // Инициализация: если согласие есть — применяем, но НЕ удаляем host (прячем)
      (function init(){
        setTimeout(function(){
          bindEvents();
          const existing = getConsentCookie();
          if(existing){
            applyConsent(existing);
            // прячем баннер, но не удаляем host: host.style.display = 'none'
            try{
              if(ccCard) ccCard.classList.add('hide');
              if(host && host.style) host.style.display = 'none';
            }catch(e){}
          } else {
            // показать banner (host по умолчанию отображается)
          }
          document.addEventListener('DOMContentLoaded', function(){
            const ex = getConsentCookie();
            if(ex) applyConsent(ex);
          });
        }, 10);
      })();

      // === API: expose robust methods ===
      window.CookieConsent = window.CookieConsent || {};
      // getConsent
      window.CookieConsent.getConsent = function(){ return getConsentCookie(); };
      // openSettings: гарантированно покажет host и откроет модал
      window.CookieConsent.openSettings = function(){
        try{
          showHostIfHidden(); // если был скрыт — восстановим
          // откроем модал: попробуем вызвать внутреннюю кнопку
          if(useShadow){
            const innerBtn = root.querySelector('#ccOpenSettings');
            if(innerBtn) { innerBtn.click(); return; }
          } else {
            const innerBtn = host.querySelector('#ccOpenSettings');
            if(innerBtn) { innerBtn.click(); return; }
          }
          // fallback: если нет кнопки, просто покажем overlay
          if(overlay){ overlay.style.display = 'flex'; overlay.setAttribute('aria-hidden','false'); }
        }catch(e){ console.warn('CookieConsent.openSettings error', e); }
      };
      // resetConsent: удаляет cookie (client-side)
      window.CookieConsent.resetConsent = function(){
        try{ setConsentCookie(null); }catch(e){}
      };

    })();
    </script>

    <?php
} // end function
Add the code to functions.php.
The banner automatically appears in the footer.
Visitors can accept all, decline, or configure categories (necessary/analytics/marketing).
The choice is stored in a cookie for 365 days.
API methods available: CookieConsent.getConsent(), CookieConsent.openSettings(), CookieConsent.resetConsent().
Privacy and cookie policy URLs are set via $privacy_url and $cookies_url variables.
🚀

WordPress / WooCommerce optimization

Disable emoji, REST API links, oEmbed, XML-RPC, Heartbeat API, Cart Fragments, marketplace suggestions, and other bloat.

<?php
// Отключаем всё, что генерирует WordPress по умолчанию и не нужно
add_action( 'after_setup_theme', 'mismebel_theme_setup' );
function mismebel_theme_setup() {
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
    remove_action( 'wp_print_styles', 'print_emoji_styles' );
    remove_action( 'wp_head', 'rest_output_link_wp_head' );
    remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
    remove_action( 'wp_head', 'wp_generator' );
    remove_action( 'wp_head', 'wlwmanifest_link' );
    remove_action( 'wp_head', 'wp_shortlink_wp_head' );
    add_theme_support( 'title-tag' );
    add_theme_support( 'custom-logo' );
    add_theme_support( 'site-icon' );
}

add_action( 'wp_enqueue_scripts', 'mismebel_disable_unused_assets', 100 );
function mismebel_disable_unused_assets() {
    wp_dequeue_style( 'wp-block-library' );
    wp_dequeue_style( 'wp-block-library-theme' );
}

add_filter( 'astra_get_sidebar_layout', function ( $layout ) {
    if ( is_shop() || is_product_category() ) {
        return 'no-sidebar';
    }
    return $layout;
});

add_filter( 'woocommerce_demo_store', '__return_false' );

add_filter( 'emoji_svg_url', '__return_false' );
add_filter( 'tiny_mce_plugins', function( $plugins ) {
    return array_diff( $plugins, array( 'wpemoji' ) );
});

add_action( 'wp_enqueue_scripts', function() {
    wp_deregister_script( 'wp-embed' );
}, 99 );

add_filter( 'xmlrpc_enabled', '__return_false' );

add_filter( 'rest_authentication_errors', function( $access ) {
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_forbidden', __( 'REST API доступен только авторизованным.' ), array( 'status' => 401 ) );
    }
    return $access;
});

add_filter( 'heartbeat_settings', function( $settings ) {
    $settings['interval'] = 60;
    return $settings;
});

add_filter( 'woocommerce_allow_marketplace_suggestions', '__return_false' );

add_action( 'wp_enqueue_scripts', function() {
    if ( ! is_cart() && ! is_checkout() ) {
        wp_dequeue_script( 'wc-cart-fragments' );
        wp_deregister_script( 'wc-cart-fragments' );
    }
}, 99 );

add_filter( 'woocommerce_admin_disabled', '__return_true' );
Paste into functions.php.
Takes effect immediately: removes unnecessary requests and scripts, improves load speed.
🛒

Shortcode [custom_checkout]

Custom checkout page: wrapper, breadcrumbs, field and order table styling, custom button.

<?php
/**
 * Шорткод [custom_checkout] – кастомная страница оформления заказа
 * 
 * Код полностью обезличен: не содержит реальных номеров телефонов,
 * email-адресов, физических адресов или персональных данных клиентов.
 * 
 * Использование: [custom_checkout]
 * Добавьте в functions.php вашей темы.
 */

add_shortcode( 'custom_checkout', 'bco_custom_checkout_shortcode' );

add_action( 'woocommerce_checkout_before_order_review_heading', 'bco_open_order_review_card', 5 );
function bco_open_order_review_card() {
    echo '<div class="bco-order-column"><div class="bco-order-card">';
}

add_action( 'woocommerce_checkout_after_order_review', 'bco_close_order_review_card', 50 );
function bco_close_order_review_card() {
    echo '</div></div>';
}

function bco_custom_checkout_shortcode() {
    if ( ! function_exists( 'WC' ) ) {
        return '';
    }

    ob_start();
    ?>
    <div class="mismebel-page-wrapper bco-page-wrapper">
        <div class="mismebel-page-content bco-page-content">
            <div class="bco-catalog">

                <!-- Иконка для хлебных крошек (SVG) -->
                <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
                    <symbol id="icon-home" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
                    </symbol>
                </svg>

                <!-- Хлебные крошки -->
                <nav class="bco-breadcrumbs" aria-label="Хлебные крошки">
                    <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="bco-breadcrumbs__home" aria-label="Главная">
                        <svg class="bco-breadcrumbs__home-icon" aria-hidden="true"><use href="#icon-home"/></svg>
                    </a>
                    <span class="bco-breadcrumbs__sep">/</span>
                    <a href="<?php echo esc_url( wc_get_cart_url() ); ?>"><?php esc_html_e( 'Корзина', 'woocommerce' ); ?></a>
                    <span class="bco-breadcrumbs__sep">/</span>
                    <span class="bco-breadcrumbs__current" aria-current="page"><?php esc_html_e( 'Оформление заказа', 'woocommerce' ); ?></span>
                </nav>

                <!-- Контент чекаута -->
                <div class="bco-checkout-content">
                    <?php echo do_shortcode( '[woocommerce_checkout]' ); ?>
                </div>

            </div>
        </div>
    </div>

    <style>
        /* =========================================
           ОСНОВА СТРАНИЦЫ
        ========================================= */
        .bco-page-wrapper {
            background: #e9ecef;
            width: 100%;
            min-height: 100vh;
            padding: 24px 0 32px !important;
            box-sizing: border-box;
        }

        .bco-page-content {
            max-width: 1400px;
            margin: 0 auto;
            padding: 0 24px;
            box-sizing: border-box;
        }

        /* =========================================
           ХЛЕБНЫЕ КРОШКИ
        ========================================= */
        .bco-breadcrumbs {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 6px;
            font-size: 14px;
            color: #6b7280;
            margin-bottom: 24px;
        }

        .bco-breadcrumbs a {
            color: #6b7280;
            text-decoration: none;
            transition: color .15s ease;
        }

        .bco-breadcrumbs a:hover {
            color: #111827;
        }

        .bco-breadcrumbs__home {
            display: inline-flex;
            align-items: center;
            justify-content: center;
        }

        .bco-breadcrumbs__home-icon {
            width: 18px;
            height: 18px;
        }

        .bco-breadcrumbs__sep {
            color: #cbd5e1;
        }

        .bco-breadcrumbs__current {
            color: #111827;
            font-weight: 600;
        }

        /* =========================================
           ЛЕВАЯ КОЛОНКА
        ========================================= */
        /* Убираем пустую колонку Astra/Woo */
        .woocommerce-checkout #customer_details .col-2,
        .woocommerce-checkout wc-order-attribution-inputs,
        .woocommerce-checkout .wc-order-attribution-inputs {
            display: none !important;
        }

        /* Левая карточка */
        .woocommerce-checkout #customer_details .col-1 {
            background: #fff !important;
            border-radius: 12px !important;
            padding: 24px !important;
            box-shadow:
                0 1px 3px rgba(0,0,0,.06),
                0 8px 24px rgba(0,0,0,.04) !important;
            box-sizing: border-box;
        }

        /* =========================================
           ПРАВАЯ КОЛОНКА ASTRA
        ========================================= */
        /*
           ВАЖНО:
           НЕ ТРОГАЕМ float и width Astra
           Только оформление
        */
        .woocommerce-checkout #order_review_heading {
            background: #fff !important;
            margin: 0 !important;
            padding: 24px 24px 12px !important;
            border-radius: 12px 12px 0 0 !important;
            border: 1px solid #eef0f3 !important;
            border-bottom: 0 !important;
            box-shadow:
                0 1px 3px rgba(0,0,0,.06),
                0 8px 24px rgba(0,0,0,.04) !important;
        }

        .woocommerce-checkout #order_review {
            background: #fff !important;
            padding: 0 24px 24px !important;
            border: 1px solid #eef0f3 !important;
            border-top: 0 !important;
            border-radius: 0 0 12px 12px !important;
            box-shadow:
                0 1px 3px rgba(0,0,0,.06),
                0 8px 24px rgba(0,0,0,.04) !important;
            margin: 0 !important;
        }

        /* =========================================
           ТАБЛИЦА ЗАКАЗА
        ========================================= */
        .woocommerce-checkout-review-order-table {
            width: 100%;
            border-collapse: collapse;
        }

        .woocommerce-checkout-review-order-table th,
        .woocommerce-checkout-review-order-table td {
            padding: 12px 0;
            border-color: #e5e7eb !important;
        }

        .woocommerce-checkout-review-order-table thead th {
            font-size: 14px;
            font-weight: 700;
            color: #111827;
        }

        /* =========================================
           ПЛАТЕЖНЫЙ БЛОК
        ========================================= */
        #payment {
            margin-top: 16px !important;
            border-radius: 10px !important;
            background: #fff !important;
        }

        #payment ul.payment_methods {
            padding: 0 !important;
            margin: 0 !important;
            border: 0 !important;
        }

        #payment ul.payment_methods li {
            padding: 10px 0;
        }

        #payment .place-order {
            padding: 16px 0 0 !important;
            margin: 0 !important;
            border: 0 !important;
        }

        /* =========================================
           ПОЛЯ ФОРМЫ
        ========================================= */
        .woocommerce form .form-row {
            margin-bottom: 16px;
        }

        .woocommerce form .form-row input.input-text,
        .woocommerce form .form-row textarea,
        .woocommerce form .form-row select {
            min-height: 46px;
            border-radius: 10px;
            border: 1px solid #d1d5db;
            background: #fff;
            box-shadow: none;
            transition:
                border-color .15s ease,
                box-shadow .15s ease;
        }

        .woocommerce form .form-row textarea {
            min-height: 110px;
        }

        .woocommerce form .form-row input.input-text:focus,
        .woocommerce form .form-row textarea:focus,
        .woocommerce form .form-row select:focus {
            border-color: #2563eb;
            box-shadow: 0 0 0 3px rgba(37,99,235,.12);
            outline: none;
        }

        /* =========================================
           КНОПКА
        ========================================= */
        #place_order {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 10px;

            width: 100%;
            height: 48px;

            border: none !important;

            background: #2563eb !important;
            color: #fff !important;

            border-radius: 10px;

            font-size: 16px;
            font-weight: 800;

            cursor: pointer;

            transition:
                background-color .18s ease,
                box-shadow .18s ease;

            box-shadow:
                0 4px 10px rgba(0,0,0,.06);

            transform: none !important;
        }

        #place_order:hover {
            background: #1d4ed8 !important;
            box-shadow:
                0 6px 14px rgba(0,0,0,.12);
        }

        #place_order::before {
            content: '';
            width: 20px;
            height: 20px;
            display: inline-block;
            flex: 0 0 auto;
            background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M188 184H91.17a16 16 0 0 1-15.74-13.14L48.73 24H24' fill='none' stroke='%23ffffff' stroke-width='16' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='92' cy='204' r='20' fill='none' stroke='%23ffffff' stroke-width='16'/%3E%3Ccircle cx='188' cy='204' r='20' fill='none' stroke='%23ffffff' stroke-width='16'/%3E%3Cpath d='M70.55 144H196.1a16 16 0 0 0 15.74-13.14L224 64H56' fill='none' stroke='%23ffffff' stroke-width='16' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
            background-size: contain;
        }

        /* =========================================
           МОБИЛЬНАЯ ВЕРСИЯ
        ========================================= */
        @media (max-width: 767px) {
            .bco-page-wrapper {
                padding: 20px 0 28px !important;
            }

            .bco-page-content {
                padding: 0 16px;
            }

            .woocommerce-checkout #customer_details .col-1 {
                padding: 16px !important;
                border-radius: 10px !important;
            }

            .woocommerce-checkout #order_review_heading {
                padding: 16px 16px 10px !important;
                font-size: 18px;
                border-radius: 10px 10px 0 0 !important;
            }

            .woocommerce-checkout #order_review {
                padding: 0 16px 16px !important;
                border-radius: 0 0 10px 10px !important;
            }
        }
    </style>
    <?php
    return ob_get_clean();
}
Add code to functions.php.
Create a page with [custom_checkout] shortcode and set it as the checkout page in WooCommerce settings.
✅

Modern inline thank-you page

Replaces the default order-received page: card with checkmark, action buttons, product list, billing address, and recommendations.

<?php
/**
 * Modern inline thank-you for WooCommerce — обезличенная версия
 *
 * Заменяет стандартную страницу благодарности (order-received) на современный
 * интерфейс с иконкой, кнопками действий, списком товаров и платёжным адресом.
 *
 * Вставьте в functions.php вашей темы.
 */

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

/* --- Helper: проверка, находимся ли на странице order-received --- */
if ( ! function_exists( 'mwt_is_thankyou_endpoint' ) ) {
    function mwt_is_thankyou_endpoint() {
        return function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-received' );
    }
}

/* --- Подключаем inline CSS/JS только на странице order-received --- */
add_action( 'wp_enqueue_scripts', function() {
    if ( ! mwt_is_thankyou_endpoint() ) {
        return;
    }

    // пустые хендлы для inline
    wp_register_style( 'bco-thankyou-inline-style', false );
    wp_enqueue_style( 'bco-thankyou-inline-style' );

    wp_register_script( 'bco-thankyou-inline-js', false, array( 'jquery' ), false, true );
    wp_enqueue_script( 'bco-thankyou-inline-js' );

    // CSS (inline)
    $css = <<<CSS
/* === Modern Thankyou styles (inline) === */
.bco-thankyou-container { max-width:1440px; margin:40px auto; padding:20px; color:#111827; box-sizing:border-box; }
.bco-thankyou-hero { display:grid; grid-template-columns: 62% 38%; gap:32px; align-items:start; margin-bottom: 24px; }
.bco-thankyou-hero-left {
    background: linear-gradient(180deg, #ffffffcc, #ffffff);
    padding:32px;
    border-radius:16px;
    box-shadow:0 8px 24px rgba(19,24,33,0.08);
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.bco-thankyou-hero-right { }
.bco-thankyou-hero-top {
    display: flex;
    align-items: flex-start;
    gap: 24px;
}
.bco-thankyou-checkmark {
    width:84px;
    height:84px;
    border-radius:50%;
    background:linear-gradient(135deg,#2ecc71,#27ae60);
    color:#fff;
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:44px;
    margin-bottom:0;
    box-shadow:0 6px 18px rgba(39,174,96,0.18);
    flex-shrink: 0;
}
.bco-thankyou-title { font-size:28px; margin:0 0 8px; }
.bco-thankyou-sub { color:#444; margin:0; line-height:1.5; }
.bco-thankyou-meta { display:flex; gap:18px; color:#444; flex-wrap:wrap; }
.bco-thankyou-actions { display:flex; gap:12px; flex-wrap:wrap; }
.bco-thankyou-btn { background:#f8f9fa; color:#444; padding:12px 20px; border-radius:10px; text-decoration:none; display:inline-block; font-weight:600; border:1px solid #e9ecef; transition:all 0.2s ease; }
.bco-thankyou-btn:hover { background:#e9ecef; transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.08); color:#444; text-decoration:none; }
.bco-thankyou-btn-outline { background:transparent; border:1px solid #e9ecef; color:#444; padding:12px 20px; border-radius:10px; text-decoration:none; display:inline-block; font-weight:600; transition:all 0.2s ease; }
.bco-thankyou-btn-outline:hover { background:#f8f9fa; border-color:#dee2e6; transform:translateY(-2px); color:#444; text-decoration:none; }
.bco-thankyou-copy { cursor:pointer; border: 1px solid #e9ecef; background:#f8f9fa; color:#444; padding:12px 20px; border-radius:10px; font-weight:600; transition:all 0.2s ease; }
.bco-thankyou-copy:hover { background:#e9ecef; transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.08); color:#111827; }
.bco-thankyou-order-card { background:#fff; padding:20px; border-radius:12px; box-shadow:0 6px 18px rgba(15,23,42,0.04); margin-bottom:16px; }
.bco-thankyou-order-list { list-style:none; margin:0; padding:0; max-height:260px; overflow:auto; }
.bco-thankyou-order-list li { display:grid; grid-template-columns:56px 1fr auto auto; gap:10px; align-items:center; padding:8px 0; border-bottom:1px dashed #f1f1f1; }
.bco-thankyou-item-thumb img { width:56px; height:56px; object-fit:cover; border-radius:8px; }
.bco-thankyou-item-title { font-weight:500; }
.bco-thankyou-item-qty { opacity:0.7; }
.bco-thankyou-item-price { font-weight:500; }
.bco-thankyou-summary-row { display:flex; justify-content:space-between; padding:8px 0; border-top:1px solid #f7f7f7; }
.bco-thankyou-summary-row.bco-thankyou-total { font-size:18px; font-weight:500; margin-top:8px; }
.bco-thankyou-plugin-hooks { margin:0; }
.bco-thankyou-plugin-hooks .button { margin-right: 12px !important; margin-bottom: 8px !important; }
/* Скрываем стандартный блок с адресом WooCommerce */
.bco-thankyou-plugin-hooks .woocommerce-customer-details,
.bco-thankyou-plugin-hooks address { display: none !important; }
/* Уменьшаем заголовок h2 "Информация о заказе" */
.bco-thankyou-plugin-hooks h2 { font-size: 1.25rem; }
.bco-thankyou-address-card { background:#fff; padding:20px; border-radius:12px; box-shadow:0 6px 18px rgba(15,23,42,0.04); margin-bottom:16px; }
.bco-thankyou-address-card h3 { margin-top:0; margin-bottom:16px; }
.bco-thankyou-address-content { color:#444; line-height:1.6; }
.bco-thankyou-reco { width:100%; margin-top:32px; padding-top:32px; border-top:1px solid #eaeaea; }
a { color:#444; text-decoration:none; }
a:hover { color:#444; text-decoration:none; }

/* Новые стили для кнопок оплаты и отмены */
.bco-thankyou-payment-fallback { margin-top: 24px; padding-top: 24px; border-top: 1px dashed #e9ecef; }
.bco-thankyou-btn.bco-thankyou-btn-pay { background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; border: none; }
.bco-thankyou-btn.bco-thankyou-btn-pay:hover { background: linear-gradient(135deg, #27ae60, #2ecc71); transform: translateY(-2px); box-shadow: 0 6px 16px rgba(39, 174, 96, 0.25); }
.bco-thankyou-btn-outline.bco-thankyou-btn-cancel { border-color: #e74c3c; color: #e74c3c; }
.bco-thankyou-btn-outline.bco-thankyou-btn-cancel:hover { background-color: #e74c3c; color: white; border-color: #e74c3c; }

/* Автоматически скрываем наши кнопки, если плагин вывел что-то специфичное */
.bco-thankyou-plugin-hooks .woocommerce-order-details + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks .payment_box + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks .wc_payment_method + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks .payment_method_bacs + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks .payment_method_paypal + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks .payment_method_stripe + .bco-thankyou-payment-fallback,
.bco-thankyou-plugin-hooks form + .bco-thankyou-payment-fallback { display: none; }

/* Дополнительные стили для улучшения отображения */
.bco-thankyou-plugin-hooks .woocommerce-bacs-bank-details,
.bco-thankyou-plugin-hooks .woocommerce-order-details,
.bco-thankyou-plugin-hooks .woocommerce-order-downloads { background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 16px; border: 1px solid #e9ecef; }

/* ===== Стили для стандартной таблицы заказа — ТОЛЬКО НА ДЕСКТОПЕ (>980px) ===== */
@media (min-width: 981px) {
    .bco-thankyou-plugin-hooks .woocommerce-order-details table.woocommerce-table--order-details {
        table-layout: fixed !important;
        width: 100% !important;
    }
    /* Колонка "Товар" — 82% */
    .bco-thankyou-plugin-hooks .woocommerce-order-details th.product-name,
    .bco-thankyou-plugin-hooks .woocommerce-order-details td.product-name {
        width: 82% !important;
    }
    /* Колонка "Итого" — 18% */
    .bco-thankyou-plugin-hooks .woocommerce-order-details th.product-total,
    .bco-thankyou-plugin-hooks .woocommerce-order-details td.product-total {
        width: 18% !important;
    }
    /* Разрешаем перенос длинных названий товаров */
    .bco-thankyou-plugin-hooks .woocommerce-order-details td.product-name {
        word-break: break-word;
        white-space: normal;
    }
}

/* ===== Мобильные адаптации (ширина ≤980px) ===== */
@media (max-width: 980px) {
    .bco-thankyou-container {
        padding: 10px 0 !important;
    }
    .bco-thankyou-hero-left {
        padding: 25px 20px 20px 20px !important;
    }
    .bco-thankyou-checkmark {
        width: 54px !important;
        height: 54px !important;
        font-size: 28px !important;
    }
    .bco-thankyou-hero {
        grid-template-columns: 1fr;
        gap: 24px;
        margin-bottom: 16px;
    }
    .bco-thankyou-hero-left { order: 1; }
    .bco-thankyou-hero-right { order: 2; }
    .bco-thankyou-payment-fallback { margin-top: 16px; padding-top: 16px; }
}

@media (max-width: 640px) {
    .bco-thankyou-actions { flex-direction: column; }
    .bco-thankyou-btn,
    .bco-thankyou-btn-outline,
    .bco-thankyou-copy {
        width: 100%;
        text-align: center;
    }
    .bco-thankyou-payment-fallback { margin-top: 12px; padding-top: 12px; }
}
CSS;
    wp_add_inline_style( 'bco-thankyou-inline-style', $css );

    // JS (inline) — полностью обезличен
    $copy_text = esc_js( 'Скопировано' );
    $cancel_text = esc_js( __( 'Вы уверены, что хотите отменить заказ?', 'modern-woo-thankyou' ) );
    $cancelling_text = esc_js( __( 'Отменяем...', 'modern-woo-thankyou' ) );

    $js = <<<JS
jQuery(function($){
    // COPY button
    $(document).on('click', '.bco-thankyou-copy', function(e){
        e.preventDefault();
        var txt = $(this).attr('data-copy') || $(this).data('copy');
        if (!txt) return;
        var ta = document.createElement('textarea');
        ta.value = txt;
        document.body.appendChild(ta);
        ta.select();
        try {
            document.execCommand('copy');
            var \$btn = $(this);
            var prev = \$btn.text();
            \$btn.text('{$copy_text}');
            setTimeout(function(){ \$btn.text(prev); }, 1600);
        } catch(e) {
            console.warn('Copy failed', e);
        }
        document.body.removeChild(ta);
    });

    // Обработка кнопки отмены заказа
    $(document).on('click', '.bco-thankyou-btn-cancel', function(e){
        e.preventDefault();
        var cancelUrl = $(this).attr('href');
        if (!cancelUrl) return;
        if (confirm('{$cancel_text}')) {
            var \$btn = $(this);
            var originalText = \$btn.text();
            \$btn.text('{$cancelling_text}');
            \$btn.prop('disabled', true);
            window.location.href = cancelUrl;
        }
    });

    // Small checkmark pop animation
    var \$check = $('.bco-thankyou-checkmark');
    if (\$check.length) {
        \$check.css({transform:'scale(.6)', opacity:0}).animate({opacity:1}, {
            duration:650,
            step:function(now, fx) {
                var scale = 0.6 + 0.4*now;
                $(this).css('transform','scale('+scale+')');
            }
        });
    }

    // Удаляем стандартный блок с адресом WooCommerce
    setTimeout(function() {
        $('.bco-thankyou-plugin-hooks .woocommerce-customer-details').remove();
        $('.bco-thankyou-plugin-hooks address').remove();
    }, 100);
});
JS;
    wp_add_inline_script( 'bco-thankyou-inline-js', $js );
}, 20 );

/* --- Вставляем разметку прямо в контент страницы --- */
add_filter( 'the_content', function( $content ) {
    if ( ! mwt_is_thankyou_endpoint() || ! is_main_query() ) {
        return $content;
    }

    $order_id = absint( get_query_var( 'order-received' ) );
    if ( ! $order_id && isset( $_GET['order'] ) ) {
        $order_id = absint( wp_unslash( $_GET['order'] ) );
    }
    if ( ! $order_id && isset( $_GET['key'] ) && function_exists( 'wc_get_order_id_by_order_key' ) ) {
        $order_id = absint( wc_get_order_id_by_order_key( sanitize_text_field( wp_unslash( $_GET['key'] ) ) ) );
    }

    if ( ! $order_id || ! function_exists( 'wc_get_order' ) ) {
        return $content;
    }

    $order = wc_get_order( $order_id );
    if ( ! $order ) {
        return $content;
    }

    ob_start();
    ?>
    <div class="bco-thankyou-container" aria-live="polite">
        <div class="bco-thankyou-hero">
            <div class="bco-thankyou-hero-left">
                <div class="bco-thankyou-hero-top">
                    <div class="bco-thankyou-checkmark" aria-hidden="true">✔</div>
                    <div class="bco-thankyou-hero-text">
                        <h1 class="bco-thankyou-title"><?php esc_html_e( 'Спасибо за заказ!', 'modern-woo-thankyou' ); ?></h1>
                        <p class="bco-thankyou-sub">
                            <?php printf( esc_html__( 'Ваш заказ #%s успешно принят.', 'modern-woo-thankyou' ), '<strong>' . esc_html( $order->get_order_number() ) . '</strong>' ); ?>
                        </p>
                    </div>
                </div>

                <div class="bco-thankyou-meta">
                    <div><strong><?php esc_html_e( 'Сумма', 'modern-woo-thankyou' ); ?>:</strong> <?php echo wp_kses_post( $order->get_formatted_order_total() ); ?></div>
                    <div><strong><?php esc_html_e( 'Оплата', 'modern-woo-thankyou' ); ?>:</strong> <?php echo esc_html( $order->get_payment_method_title() ); ?></div>
                    <div><strong><?php esc_html_e( 'Статус', 'modern-woo-thankyou' ); ?>:</strong> <?php echo esc_html( wc_get_order_status_name( $order->get_status() ) ); ?></div>
                </div>

                <div class="bco-thankyou-actions">
                    <a class="bco-thankyou-btn" href="<?php echo esc_url( wc_get_page_permalink( 'shop' ) ); ?>"><?php esc_html_e( 'Вернуться в магазин', 'modern-woo-thankyou' ); ?></a>
                    <a class="bco-thankyou-btn-outline" href="<?php echo esc_url( $order->get_view_order_url() ); ?>"><?php esc_html_e( 'Просмотреть заказ', 'modern-woo-thankyou' ); ?></a>
                    <button class="bco-thankyou-copy" data-copy="<?php echo esc_attr( $order->get_order_number() ); ?>"><?php esc_html_e( 'Копировать номер заказа', 'modern-woo-thankyou' ); ?></button>
                </div>

                <div class="bco-thankyou-plugin-hooks">
                    <?php 
                    do_action( 'woocommerce_thankyou', $order_id );
                    do_action( 'woocommerce_thankyou_' . $order->get_payment_method(), $order_id );

                    if ( $order->get_status() === 'pending' ) {
                        $payment_url = $order->get_checkout_payment_url();
                        $cancel_url = $order->get_cancel_order_url();
                        if ( ! empty( $payment_url ) ) {
                            ?>
                            <div class="bco-thankyou-payment-fallback">
                                <a href="<?php echo esc_url( $payment_url ); ?>" class="bco-thankyou-btn bco-thankyou-btn-pay">
                                    <?php esc_html_e( 'Оплатить заказ', 'modern-woo-thankyou' ); ?>
                                </a>
                                <?php if ( ! empty( $cancel_url ) ) : ?>
                                <a href="<?php echo esc_url( $cancel_url ); ?>" class="bco-thankyou-btn-outline bco-thankyou-btn-cancel">
                                    <?php esc_html_e( 'Отменить заказ', 'modern-woo-thankyou' ); ?>
                                </a>
                                <?php endif; ?>
                            </div>
                            <?php
                        }
                    }
                    ?>
                </div>
            </div>

            <div class="bco-thankyou-hero-right">
                <div class="bco-thankyou-order-card" aria-label="<?php esc_attr_e( 'Краткая информация о заказе', 'modern-woo-thankyou' ); ?>">
                    <h3><?php esc_html_e( 'Краткая информация', 'modern-woo-thankyou' ); ?></h3>
                    <ul class="bco-thankyou-order-list">
                        <?php foreach ( $order->get_items() as $item_id => $item ) :
                            $product = $item->get_product();
                            ?>
                            <li>
                                <div class="bco-thankyou-item-thumb"><?php echo $product ? wp_kses_post( $product->get_image( 'thumbnail' ) ) : ''; ?></div>
                                <div class="bco-thankyou-item-title"><?php echo esc_html( $item->get_name() ); ?></div>
                                <div class="bco-thankyou-item-qty">×<?php echo esc_html( $item->get_quantity() ); ?></div>
                                <div class="bco-thankyou-item-price"><?php echo wp_kses_post( wc_price( $item->get_total() ) ); ?></div>
                            </li>
                        <?php endforeach; ?>
                    </ul>
                    <div class="bco-thankyou-order-summary" aria-hidden="false">
                        <?php
                        $totals = $order->get_order_item_totals();
                        if ( is_array( $totals ) ) {
                            foreach ( $totals as $key => $row ) {
                                $class = ( $key === 'order_total' ) ? 'bco-thankyou-summary-row bco-thankyou-total' : 'bco-thankyou-summary-row';
                                echo '<div class="' . esc_attr( $class ) . '"><span>' . esc_html( $row['label'] ) . '</span><span>' . wp_kses_post( $row['value'] ) . '</span></div>';
                            }
                        }
                        ?>
                    </div>
                </div>

                <?php if ( $order->get_formatted_billing_address() ) : ?>
                <div class="bco-thankyou-address-card" aria-label="<?php esc_attr_e( 'Платёжный адрес', 'modern-woo-thankyou' ); ?>">
                    <h3><?php esc_html_e( 'Платёжный адрес', 'modern-woo-thankyou' ); ?></h3>
                    <div class="bco-thankyou-address-content">
                        <?php echo wp_kses( $order->get_formatted_billing_address(), array(
                            'br' => array(),
                            'p' => array(),
                            'div' => array(),
                            'span' => array(),
                            'strong' => array(),
                            'em' => array()
                        ) ); ?>
                    </div>
                </div>
                <?php endif; ?>
            </div>
        </div>

        <?php if ( shortcode_exists( 'bco_product_tabs' ) ) : ?>
            <div class="bco-thankyou-reco">
                <?php echo do_shortcode( '[bco_product_tabs]' ); ?>
            </div>
        <?php endif; ?>
    </div>
    <?php
    return ob_get_clean();
}, 20 );
Paste into functions.php.
Automatically applied to all WooCommerce thank-you pages.
Requires [bco_product_tabs] shortcode for recommendations block (can be removed if not needed).
✏️

Checkout fields override + styling

Simplified field set (name, phone, email, city, address) + disable payment + professional styling and scripts.

<?php
/**
 * Переопределение полей оформления заказа WooCommerce + профессиональная стилизация
 * 
 * Код полностью обезличен: не содержит реальных номеров телефонов,
 * email-адресов, физических адресов или персональных данных клиентов.
 * 
 * Вставьте в functions.php вашей темы.
 */

/**
 * 1. Переопределяем поля оформления заказа.
 *    Оставляем: контактное лицо, телефон, email, город, адрес доставки (одна строка) и примечание.
 *    Все остальные поля (компания, страна, индекс и т. д.) удалены.
 */
add_filter( 'woocommerce_checkout_fields', 'custom_checkout_fields_only' );
function custom_checkout_fields_only( $fields ) {
    $new_fields = array();

    // Секция billing — «Контактная информация» (заголовок меняется через JS)
    $new_fields['billing'] = array(
        'billing_first_name' => array(
            'label'       => 'Контактное лицо',
            'placeholder' => 'Ваше имя',
            'required'    => true,
            'class'       => array( 'form-row-wide' ),
            'priority'    => 10,
        ),
        'billing_phone' => array(
            'label'       => 'Телефон',
            'placeholder' => 'Номер телефона',
            'required'    => true,
            'class'       => array( 'form-row-wide' ),
            'priority'    => 20,
        ),
        'billing_email' => array(
            'label'       => 'Email',
            'placeholder' => 'example@mail.com',
            'required'    => false,   // необязательное
            'class'       => array( 'form-row-wide' ),
            'priority'    => 30,
        ),
        'billing_city' => array(
            'label'       => 'Город',
            'placeholder' => 'Ваш город',
            'required'    => false,
            'class'       => array( 'form-row-wide' ),
            'priority'    => 40,
        ),
        'billing_address_1' => array(
            'label'       => 'Адрес доставки',
            'placeholder' => 'Улица, дом, квартира',
            'required'    => false,
            'class'       => array( 'form-row-wide' ),
            'priority'    => 50,
        ),
    );

    // Секция order — «Примечание к заказу»
    $new_fields['order'] = array(
        'order_comments' => array(
            'label'       => 'Примечание к заказу',
            'placeholder' => 'Ваши пожелания или уточнения',
            'type'        => 'textarea',
            'required'    => false,
            'class'       => array( 'notes' ),
            'priority'    => 50,
        ),
    );

    return $new_fields;
}

/**
 * 2. Отключаем необходимость оплаты (полностью убираем платёжные методы).
 *    Если оплата нужна, удалите этот фильтр.
 */
add_filter( 'woocommerce_cart_needs_payment', '__return_false' );

/**
 * 3. Меняем заголовки секций через JavaScript.
 *    «Платёжные реквизиты» → «Контактная информация»
 *    «Дополнительная информация» → «Примечание к заказу»
 */
add_action( 'wp_footer', 'rename_checkout_sections_js' );
function rename_checkout_sections_js() {
    if ( ! is_checkout() ) {
        return;
    }
    ?>
    <script type="text/javascript">
    (function() {
        function replaceHeadings() {
            var billingHeading = document.querySelector('.woocommerce-billing-fields h3');
            if ( billingHeading && billingHeading.textContent.includes('Платёжные реквизиты') ) {
                billingHeading.textContent = 'Контактная информация';
            }

            var orderHeading = document.querySelector('.woocommerce-additional-fields h3');
            if ( orderHeading && orderHeading.textContent.includes('Дополнительная информация') ) {
                orderHeading.textContent = 'Примечание к заказу';
            }
        }

        if ( document.readyState === 'loading' ) {
            document.addEventListener('DOMContentLoaded', replaceHeadings);
        } else {
            replaceHeadings();
        }
    })();
    </script>
    <?php
}

/**
 * 4. Финальные стили оформления + скрываем текст «(необязательно)» у полей.
 *    Разрешён перенос для правой ячейки доставки (стоимости), если не помещается.
 */
add_action( 'wp_footer', 'professional_checkout_styling_final', 9999 );
function professional_checkout_styling_final() {
    if ( ! is_checkout() ) return;
    ?>
    <style>
        :root {
            --checkout-field-bg: #ffffff;
            --checkout-field-border: #e0e0e0;
            --checkout-field-focus-border: #4a90e2;
            --checkout-field-focus-shadow: 0 0 0 3px rgba(74,144,226,0.15);
            --checkout-label-color: #1a1a1a;
            --checkout-text-color: #333;
            --checkout-placeholder-color: #aaa;
            --checkout-radius: 4px;
        }

        /* -------------------------------------------------
           ГЛОБАЛЬНЫЕ СБРОСЫ
        ------------------------------------------------- */
        body.woocommerce-checkout #customer_details .col-1,
        body.woocommerce-checkout #order_review_heading,
        body.woocommerce-checkout #order_review {
            border-radius: var(--checkout-radius) !important;
        }
        body.woocommerce-checkout #order_review_heading,
        body.woocommerce-checkout #order_review {
            background: #fff !important;
            border: 1px solid #eef0f3 !important;
        }
        body.woocommerce-checkout #order_review_heading {
            border-bottom: 0 !important;
            border-radius: var(--checkout-radius) var(--checkout-radius) 0 0 !important;
        }
        body.woocommerce-checkout #order_review {
            border-radius: 0 0 var(--checkout-radius) var(--checkout-radius) !important;
        }

        /* -------------------------------------------------
           ЗАГОЛОВКИ СЕКЦИЙ
        ------------------------------------------------- */
        body.woocommerce-checkout .woocommerce-billing-fields > h3 {
            margin-top: 0 !important;
            margin-bottom: 0.2rem !important;
            padding: 0px 0 14px !important;
        }
        body.woocommerce-checkout .woocommerce-additional-fields > h3 {
            margin-top: 0px !important;
            margin-bottom: 0 !important;
        }
        @media (max-width: 768px) {
            body.woocommerce-checkout #order_review_heading {
                margin-top: 20px !important;
                margin-bottom: 0 !important;
            }
        }
        @media (min-width: 769px) {
            body.woocommerce-checkout #order_review_heading {
                margin-top: 0 !important;
            }
        }

        /* -------------------------------------------------
           ПОЛЯ ФОРМЫ
        ------------------------------------------------- */
        body.woocommerce-checkout .woocommerce-billing-fields__field-wrapper,
        body.woocommerce-checkout .woocommerce-additional-fields {
            display: flex !important;
            flex-direction: column !important;
            gap: 0.5rem !important;
        }
        body.woocommerce-checkout .woocommerce-billing-fields .form-row,
        body.woocommerce-checkout .woocommerce-additional-fields .form-row {
            float: none !important;
            width: 100% !important;
            margin: 0 !important;
            padding: 0 !important;
        }
        body.woocommerce-checkout .woocommerce-billing-fields label,
        body.woocommerce-checkout .woocommerce-additional-fields label {
            display: block;
            margin-bottom: 0.5rem;
            font-size: 0.95rem;
            font-weight: 600;
            color: var(--checkout-label-color);
        }
        body.woocommerce-checkout .optional {
            display: none !important;
        }
        body.woocommerce-checkout .woocommerce-billing-fields .form-row:not(.validate-required) .required,
        body.woocommerce-checkout .woocommerce-additional-fields .form-row:not(.validate-required) .required {
            display: none;
        }
        body.woocommerce-checkout .woocommerce-billing-fields input[type="text"],
        body.woocommerce-checkout .woocommerce-billing-fields input[type="tel"],
        body.woocommerce-checkout .woocommerce-billing-fields input[type="email"],
        body.woocommerce-checkout .woocommerce-additional-fields textarea {
            width: 100% !important;
            padding: 0.95rem 1.1rem !important;
            font-size: 1rem !important;
            background-color: var(--checkout-field-bg) !important;
            border: 1.5px solid var(--checkout-field-border) !important;
            border-radius: var(--checkout-radius) !important;
            box-shadow: none !important;
            transition: border-color 0.2s, box-shadow 0.2s !important;
        }
        body.woocommerce-checkout .woocommerce-billing-fields input:focus,
        body.woocommerce-checkout .woocommerce-additional-fields textarea:focus {
            border-color: var(--checkout-field-focus-border) !important;
            box-shadow: var(--checkout-field-focus-shadow) !important;
            outline: none !important;
        }
        body.woocommerce-checkout .woocommerce-privacy-policy-text p {
            font-size: 0.8rem !important;
            color: #777 !important;
        }

        /* -------------------------------------------------
           ТАБЛИЦА ЗАКАЗА — фиксированная компоновка
           колонка «Товар» 60%, «Цена» 40%
        ------------------------------------------------- */
        body.woocommerce-checkout .woocommerce-checkout-review-order-table {
            table-layout: fixed !important;
            width: 100% !important;
        }

        .woocommerce-checkout-review-order-table th.product-name,
        .woocommerce-checkout-review-order-table td.product-name {
            width: 60% !important;
            font-size: 0.9rem;
            word-wrap: break-word;
        }

        .woocommerce-checkout-review-order-table th.product-total,
        .woocommerce-checkout-review-order-table td.product-total {
            width: 40% !important;
            font-size: 0.9rem;
            white-space: normal;
            word-break: break-word;
        }

        /* -------------------------------------------------
           ДОСТАВКА — колонки ровно 50/50,
           правая ячейка (стоимость) теперь может переноситься
        ------------------------------------------------- */
        body.woocommerce-checkout .woocommerce-checkout-review-order-table tr.woocommerce-shipping-totals th,
        body.woocommerce-checkout .woocommerce-checkout-review-order-table tr.woocommerce-shipping-totals td {
            width: 50% !important;
        }

        body.woocommerce-checkout .woocommerce-checkout-review-order-table tr.woocommerce-shipping-totals td:first-child {
            white-space: normal;
            word-break: break-word;
        }

        /* Правая ячейка (стоимость) — разрешён перенос */
        body.woocommerce-checkout .woocommerce-checkout-review-order-table tr.woocommerce-shipping-totals td:last-child {
            white-space: normal;          /* вернули обычный перенос */
            word-break: break-word;       /* чтобы длинные слова корректно разрывались */
            text-align: right;            /* выравнивание по правому краю сохраняется */
        }

        /* -------------------------------------------------
           КНОПКА ОФОРМЛЕНИЯ
        ------------------------------------------------- */
        #place_order {
            border-radius: var(--checkout-radius) !important;
        }

        /* -------------------------------------------------
           АДАПТИВ
        ------------------------------------------------- */
        @media (max-width: 768px) {
            body.woocommerce-checkout .woocommerce-billing-fields__field-wrapper,
            body.woocommerce-checkout .woocommerce-additional-fields {
                gap: 0.5rem !important;
            }
            body.woocommerce-checkout .woocommerce-billing-fields input,
            body.woocommerce-checkout .woocommerce-billing-fields textarea {
                font-size: 16px !important;
                padding: 0.85rem 1rem !important;
            }
            .woocommerce-checkout-review-order-table th.product-name,
            .woocommerce-checkout-review-order-table td.product-name {
                width: 55% !important;
            }
            .woocommerce-checkout-review-order-table th.product-total,
            .woocommerce-checkout-review-order-table td.product-total {
                width: 45% !important;
            }
        }
    </style>
    <?php
}
Add to functions.php.
Fields are changed immediately, section headings are updated via JS.
Disables payment requirement (remove woocommerce_cart_needs_payment hook if payment is needed).
🔗

Reciprocal WooCommerce upsells

Automatic upsell synchronization: if product A → B, then B → A. Works via WooCommerce CRUD hooks.

<?php
/**
 * Взаимные апсейлы WooCommerce — обезличенная версия
 *
 * Автоматическая синхронизация апсейлов:
 * если товар А → Б, то Б → А.
 * Работает через CRUD-хуки WooCommerce.
 *
 * Вставьте в functions.php дочерней темы или через Code Snippets.
 */

if ( ! function_exists( 'WC' ) ) {
    return;
}

/**
 * Единое хранилище старых upsell ID.
 * Храним и по ID товара, и по hash объекта, чтобы работало и для новых товаров.
 */
if ( ! function_exists( 'my_reciprocal_upsells_state' ) ) {
    function &my_reciprocal_upsells_state() {
        static $state = [
            'by_id'    => [],
            'by_object' => [],
        ];

        return $state;
    }
}

/**
 * Логирование ошибок.
 */
if ( ! function_exists( 'my_reciprocal_upsells_log' ) ) {
    function my_reciprocal_upsells_log( $message, array $context = [] ) {
        if ( function_exists( 'wc_get_logger' ) ) {
            wc_get_logger()->error(
                $message,
                array_merge(
                    [
                        'source' => 'reciprocal-upsells',
                    ],
                    $context
                )
            );
        }
    }
}

/**
 * Нормализация списка upsell ID:
 * - только целые числа;
 * - без дублей;
 * - без пустых значений;
 * - без самого текущего товара;
 * - только существующие и опубликованные товары.
 */
if ( ! function_exists( 'my_reciprocal_upsells_normalize_ids' ) ) {
    function my_reciprocal_upsells_normalize_ids( $ids, $exclude_id = 0 ) {
        $exclude_id = (int) $exclude_id;

        $ids = array_map( 'intval', (array) $ids );
        $ids = array_values( array_unique( array_filter( $ids ) ) );

        $clean = [];

        foreach ( $ids as $id ) {
            $id = (int) $id;

            if ( $exclude_id > 0 && $id === $exclude_id ) {
                continue;
            }

            $post = get_post( $id );
            if ( $post && 'product' === $post->post_type && 'publish' === $post->post_status ) {
                $clean[] = $id;
            }
        }

        return array_values( array_unique( $clean ) );
    }
}

/**
 * Запоминаем старые upsell ID перед сохранением.
 * Работает и для существующих, и для новых товаров.
 */
add_action( 'woocommerce_before_product_object_save', 'my_reciprocal_upsells_capture_before_save', 5, 2 );
function my_reciprocal_upsells_capture_before_save( $product, $data_store = null ) {
    if ( ! $product instanceof WC_Product ) {
        return;
    }

    $product_id = (int) $product->get_id();

    $old_ids = [];
    if ( $product_id > 0 ) {
        $old_product = wc_get_product( $product_id );
        if ( $old_product ) {
            $old_ids = my_reciprocal_upsells_normalize_ids( $old_product->get_upsell_ids(), $product_id );
        }
    }

    $state  =& my_reciprocal_upsells_state();
    $hash    = spl_object_hash( $product );
    $old_ids = array_values( array_unique( $old_ids ) );

    $state['by_object'][ $hash ] = $old_ids;

    if ( $product_id > 0 ) {
        $state['by_id'][ $product_id ] = $old_ids;
    }
}

/**
 * Основная синхронизация после сохранения.
 */
add_action( 'woocommerce_after_product_object_save', 'my_reciprocal_upsells_sync_after_save', 20, 2 );
function my_reciprocal_upsells_sync_after_save( $product, $data_store = null ) {
    if ( ! $product instanceof WC_Product ) {
        return;
    }

    $product_id = (int) $product->get_id();
    if ( $product_id <= 0 ) {
        return;
    }

    /**
     * Фильтр включения/выключения.
     * Пример:
     * add_filter( 'my_reciprocal_upsells_enabled', '__return_false' );
     */
    if ( ! apply_filters( 'my_reciprocal_upsells_enabled', true, $product_id, $product ) ) {
        return;
    }

    static $processing = [];

    if ( isset( $processing[ $product_id ] ) ) {
        return;
    }

    $processing[ $product_id ] = true;

    try {
        $allowed_types = (array) apply_filters(
            'my_reciprocal_upsells_allowed_types',
            [ 'simple', 'variable', 'external' ],
            $product_id,
            $product
        );

        if ( ! in_array( $product->get_type(), $allowed_types, true ) ) {
            return;
        }

        $state =& my_reciprocal_upsells_state();
        $hash   = spl_object_hash( $product );

        // Берём старое состояние из кэша.
        if ( isset( $state['by_object'][ $hash ] ) ) {
            $old_upsell_ids = $state['by_object'][ $hash ];
        } elseif ( isset( $state['by_id'][ $product_id ] ) ) {
            $old_upsell_ids = $state['by_id'][ $product_id ];
        } else {
            $old_upsell_ids = [];
        }

        // Очищаем кэш для этого товара.
        unset( $state['by_object'][ $hash ] );
        unset( $state['by_id'][ $product_id ] );

        $current_raw_upsells = array_map( 'intval', (array) $product->get_upsell_ids() );
        $current_raw_unique  = array_values( array_unique( array_filter( $current_raw_upsells ) ) );
        $current_upsell_ids  = my_reciprocal_upsells_normalize_ids( $current_raw_upsells, $product_id );
        $old_upsell_ids      = my_reciprocal_upsells_normalize_ids( $old_upsell_ids, $product_id );

        // Если есть битые/неопубликованные ID — перезаписываем очищенный список.
        if ( $current_upsell_ids !== $current_raw_unique ) {
            $product->set_upsell_ids( $current_upsell_ids );

            try {
                $product->save();
            } catch ( Throwable $e ) {
                my_reciprocal_upsells_log(
                    'Failed to clean upsells for product ' . $product_id . ': ' . $e->getMessage(),
                    [ 'product_id' => $product_id ]
                );
            }
        }

        $added   = array_values( array_diff( $current_upsell_ids, $old_upsell_ids ) );
        $removed = array_values( array_diff( $old_upsell_ids, $current_upsell_ids ) );

        $loaded_products = [];

        $get_product = function( $id ) use ( &$loaded_products ) {
            $id = (int) $id;

            if ( $id <= 0 ) {
                return null;
            }

            if ( ! array_key_exists( $id, $loaded_products ) ) {
                $loaded_products[ $id ] = wc_get_product( $id );
            }

            return $loaded_products[ $id ];
        };

        // Добавляем обратные связи.
        foreach ( $added as $target_id ) {
            $target_id = (int) $target_id;

            if ( $target_id === $product_id ) {
                continue;
            }

            $target = $get_product( $target_id );
            if ( ! $target ) {
                continue;
            }

            $target_upsells = my_reciprocal_upsells_normalize_ids( $target->get_upsell_ids(), $target_id );

            if ( ! in_array( $product_id, $target_upsells, true ) ) {
                $target_upsells[] = $product_id;
                $target->set_upsell_ids( array_values( array_unique( $target_upsells ) ) );

                try {
                    $target->save();
                } catch ( Throwable $e ) {
                    my_reciprocal_upsells_log(
                        'Failed to save reciprocal upsell for product ' . $target_id . ': ' . $e->getMessage(),
                        [
                            'product_id' => $product_id,
                            'target_id'  => $target_id,
                        ]
                    );
                }
            }
        }

        // Удаляем обратные связи.
        foreach ( $removed as $target_id ) {
            $target_id = (int) $target_id;

            if ( $target_id === $product_id ) {
                continue;
            }

            $target = $get_product( $target_id );
            if ( ! $target ) {
                continue;
            }

            $target_upsells = my_reciprocal_upsells_normalize_ids( $target->get_upsell_ids(), $target_id );
            $key            = array_search( $product_id, $target_upsells, true );

            if ( $key !== false ) {
                unset( $target_upsells[ $key ] );
                $target->set_upsell_ids( array_values( $target_upsells ) );

                try {
                    $target->save();
                } catch ( Throwable $e ) {
                    my_reciprocal_upsells_log(
                        'Failed to remove reciprocal upsell for product ' . $target_id . ': ' . $e->getMessage(),
                        [
                            'product_id' => $product_id,
                            'target_id'  => $target_id,
                        ]
                    );
                }
            }
        }
    } finally {
        unset( $processing[ $product_id ] );
    }
}
Insert into functions.php.
Upsells become bidirectional when a product is saved. Supports simple, variable, and external products.
Filter my_reciprocal_upsells_enabled can disable synchronization.
📨

Order notifications to Telegram

Sends new order data to an external handler.php for Telegram notifications.

<?php
/**
 * WooCommerce → Telegram — обезличенная версия
 * Отправляет уведомления о НОВЫХ ЗАКАЗАХ через handler.php
 *
 * Разместите этот файл на сервере example.com
 * и подключите его в functions.php вашей темы:
 * require_once '/полный/путь/к/woo-handler.php';
 */

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

// Настройки (замените на свои)
define( 'WOO_TG_HANDLER_URL', 'https://example.com/handler.php' );
define( 'WOO_TG_SITE_KEY', 'your_site_key_here' );

// ===== Хук: новый заказ =====
add_action( 'woocommerce_checkout_order_created', 'send_order_to_telegram' );

function send_order_to_telegram( $order ) {
    if ( ! $order ) return;

    // Устанавливаем корректный часовой пояс для local_time
    if ( function_exists( 'wp_timezone_string' ) ) {
        $timezone = wp_timezone_string();
        date_default_timezone_set( $timezone );
    } else {
        date_default_timezone_set( 'Europe/Minsk' );
    }

    // --- Собираем данные заказа ---
    $order_id    = $order->get_id();
    $total       = $order->get_total();
    $currency    = $order->get_currency();
    $first_name  = $order->get_billing_first_name();
    $last_name   = $order->get_billing_last_name();
    $phone       = $order->get_billing_phone();
    $email       = $order->get_billing_email();
    $payment     = $order->get_payment_method_title();
    $shipping    = $order->get_shipping_method();

    // Состав заказа
    $items = $order->get_items();
    $products_list = '';
    foreach ( $items as $item ) {
        $products_list .= '— ' . $item->get_name() . ' × ' . $item->get_quantity() . "\n";
    }

    // Комментарий клиента
    $customer_note = $order->get_customer_note();

    // Формируем комментарий для Telegram
    $comment = "🛍 Заказ #{$order_id}\n"
             . "💰 Сумма: {$total} {$currency}\n"
             . "📦 Товары:\n{$products_list}";
    if ( $payment ) {
        $comment .= "💳 Оплата: {$payment}\n";
    }
    if ( $customer_note ) {
        $comment .= "📝 Комментарий: {$customer_note}\n";
    }

    // Данные для отправки
    $form_data = [
        'site_key'    => WOO_TG_SITE_KEY,
        'name'        => trim( $first_name . ' ' . $last_name ),
        'phone'       => $phone ?: '—',
        'comment'     => $comment,
        'consent'     => 'on',
        'page_url'    => $order->get_checkout_order_received_url(),
        'local_time'  => date( 'Y-m-d H:i:s' ),
        'type'        => 'order',
        'order_total' => $total,
        'email'       => $email ?: '',
    ];

    send_to_handler( $form_data );

    // Восстанавливаем временную зону по умолчанию
    date_default_timezone_set( ini_get( 'date.timezone' ) ?: 'UTC' );
}

// ===== Отправка на handler.php =====
function send_to_handler( $data ) {
    $payload = http_build_query( $data );

    if ( function_exists( 'curl_init' ) ) {
        // Приоритетный способ через cURL
        $ch = curl_init( WOO_TG_HANDLER_URL );
        curl_setopt_array( $ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_SSL_VERIFYPEER => true,
        ] );
        $result = curl_exec( $ch );
        $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
        $curl_error = curl_error( $ch );
        curl_close( $ch );

        if ( ! $result || $http_code !== 200 ) {
            error_log( 'WooCommerce Telegram send failed. HTTP: ' . $http_code . '; cURL error: ' . $curl_error . '; Response: ' . (string) $result );
        }
    } elseif ( ini_get( 'allow_url_fopen' ) ) {
        // Fallback через file_get_contents
        $opts = [
            'http' => [
                'method'  => 'POST',
                'header'  => "Content-Type: application/x-www-form-urlencoded\r\n",
                'content' => $payload,
                'timeout' => 10,
            ],
            'ssl' => [
                'verify_peer' => true,
            ],
        ];
        $context = stream_context_create( $opts );
        $result = @file_get_contents( WOO_TG_HANDLER_URL, false, $context );
        if ( $result === false ) {
            error_log( 'WooCommerce Telegram send failed via file_get_contents.' );
        }
    } else {
        // Нет доступных способов отправки
        error_log( 'WooCommerce Telegram send failed: neither cURL nor allow_url_fopen is available.' );
    }
}
Add to functions.php or as a separate plugin.
Set your handler URL in WOO_TG_HANDLER_URL constant and site key in WOO_TG_SITE_KEY.
Triggers on woocommerce_checkout_order_created hook.