<?php
declare(strict_types=1);

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

/**
 * Site Header element renderer (shared).
 *
 * Phase 1, step 31. Frontend (330 lines) and editor (272 lines) had
 * diverged on responsive strategy and feature parity:
 *
 *   - Frontend emits @media(max-width:1024px) + @media(max-width:768px)
 *     CSS blocks with a per-setting boolean cascade (sticky / show_cart /
 *     show_account / show_cta / show_site_name / announcement / border /
 *     shadow each cascade desktop → tablet → mobile). Browser handles
 *     device switching at runtime.
 *
 *   - Editor pre-resolves each setting against the current preview-device
 *     toggle via `impulse_device_val()` and emits a single CSS block.
 *     The toolbar's device-toggle triggers a full re-render of the iframe.
 *
 * Both strategies stay — they serve different roles (browser-driven on
 * the live site, device-toggle-driven in the builder). The shared
 * renderer branches on `$context` to emit the right CSS strategy.
 *
 * Featurewise the frontend had drifted ahead with several improvements
 * the editor lacked:
 *
 *   - "Float burger over hero" on tablet/mobile: header strip becomes
 *     `position:absolute` with transparent background so the burger
 *     icon hovers directly above the first section instead of pushing
 *     it down behind an empty strip. `pointer-events:none` keeps clicks
 *     reaching the hero; the burger and the open-state mobile menu
 *     re-enable pointer-events.
 *
 *   - Real cart-count badge (computed from `CartSession::get('cart')`).
 *
 *   - Auto-hide scroll behavior (`sticky_mode === 'auto-hide'`).
 *
 *   - Tablet/mobile boolean cascade for sticky/cart/account/cta/site_name/
 *     announcement/border/shadow.
 *
 * All of those now ship in both contexts. The editor's device-toggle
 * preview still works because the editor branch reads `$GLOBALS['impulse_preview_device']`
 * and resolves each per-device value at PHP time, baking the resolved
 * state into the CSS for the current device.
 *
 * @sideeffect wp_add_inline_style('instinctor-page-inline', $css) with
 *             the responsive CSS block.
 * @sideeffect Emits a small auto-hide scroll JS inline when
 *             sticky_mode === 'auto-hide' (frontend only).
 */
function impulse_element_render_header($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');
    $device   = $isEditor ? (string) ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';

    // A header renders wherever it is placed, in every page mode. The 2026-07-23
    // gate that dropped it on classic pages was aimed at instinctor.com's own SEO
    // landings, but it applied to EVERY install and silently removed the NAVIGATION
    // from client sites whose classic pages carry a header block (phillyassembly,
    // joyavend: nine days with no nav). Header usage is not limited by mode
    // (Sergei 2026-08-01); an unwanted header is removed by deleting the block.

    // Per-context read helper: editor pre-resolves to the current preview
    // device; frontend takes the raw desktop value (media queries handle
    // the rest in the CSS step below).
    $read = function (string $key, $default) use ($settings, $isEditor, $device) {
        if ($isEditor && function_exists('impulse_device_val')) {
            return impulse_device_val($settings, $key, $device, $default);
        }
        return $settings[$key] ?? $default;
    };

    // ── Resolve settings ─────────────────────────────────────────────────
    $layout       = (string) ($settings['layout'] ?? 'logo-left');
    $bgColor      = (string) ($settings['bg_color']      ?? '#ffffff');
    $textColor    = (string) ($settings['text_color']    ?? '#1d2327');
    $hoverColor   = (string) ($settings['hover_color']   ?? '#666666');
    $borderColor  = (string) ($settings['border_color']  ?? '#e0e0e0');
    $borderWidth  = intval($settings['border_width'] ?? 1);

    // ── Header background mode ───────────────────────────────────────────
    // 'frosted' (default) — translucent + backdrop-blur; tone auto-picked
    // from the text colour (dark text → light frost, light text → dark
    // frost). 'solid' uses bg_color. 'transparent' lets the page show through.
    $headerBg = (string) ($settings['header_bg'] ?? 'frosted');
    $tcHex = ltrim($textColor, '#');
    if (strlen($tcHex) === 3) {
        $tcHex = $tcHex[0] . $tcHex[0] . $tcHex[1] . $tcHex[1] . $tcHex[2] . $tcHex[2];
    }
    $frostLight = true;
    if (strlen($tcHex) >= 6 && ctype_xdigit(substr($tcHex, 0, 6))) {
        $tLum = hexdec(substr($tcHex, 0, 2)) * 0.299
              + hexdec(substr($tcHex, 2, 2)) * 0.587
              + hexdec(substr($tcHex, 4, 2)) * 0.114;
        $frostLight = ($tLum < 140);
    }
    if ($headerBg === 'transparent') {
        $bgCss = 'background:transparent;';
    } elseif ($headerBg === 'solid') {
        $bgCss = 'background:' . esc_attr($bgColor) . ';';
    } else {
        $headerBg = 'frosted';
        if ($frostLight) {
            // Light page — translucent white frost.
            $bgCss = 'background:rgba(255,255,255,0.72)'
                   . ';-webkit-backdrop-filter:blur(16px) saturate(1.5)'
                   . ';backdrop-filter:blur(16px) saturate(1.5);';
        } else {
            // Dark page — the glass DARKENS the backdrop instead of
            // frosting it: a black wash plus a brightness cut so the
            // header reads as a distinctly darker band over the scene.
            $bgCss = 'background:rgba(0,0,0,0.45)'
                   . ';-webkit-backdrop-filter:blur(16px) brightness(0.5)'
                   . ';backdrop-filter:blur(16px) brightness(0.5);';
        }
    }

    $stickyMode   = (string) ($settings['sticky_mode'] ?? (!empty($settings['sticky']) ? 'sticky' : 'off'));
    $sticky       = $stickyMode !== 'off';
    $autoHide     = $stickyMode === 'auto-hide';

    $showBorder    = $read('show_border',          true)  !== false;
    $showShadow    = !empty($read('show_shadow',   false));
    $showSiteName  = $read('show_site_name',       true)  !== false;
    $showCart      = $read('show_cart',            true)  !== false;
    $showAccount   = !empty($read('show_account',  false));
    $showCta       = !empty($read('show_cta',      false));
    /* Wordmark style is OPT-IN. The site name is 20px/700 by default and that is
       every customer's own brand in their own header - restyling it globally to
       match ours would rewrite the fleet. Enabled per header it renders the locked
       Instinctor wordmark spec instead: 13px, 700, 3px of tracking, caps. It also
       stops a long name pushing the CTA off a phone screen, which is what
       surfaced it (Sergei 2026-08-08). */
    $wordmarkStyle = !empty($read('sitename_wordmark', false))
        ? 'font-size:13px;font-weight:700;letter-spacing:3px;text-transform:uppercase;'
        : 'font-size:20px;font-weight:700;';
    $announcementEnabled = !empty($read('announcement_enabled', false));

    $siteName = !empty($settings['site_name'])
        ? (string) $settings['site_name']
        : (function_exists('get_bloginfo') ? (string) get_bloginfo('name') : '');

    // "by Instinctor" endorsement suffix on the platform's own product-vertical
    // headers (Installer, InStudio, Instaurant, Ingine, ...). Rendered in a lighter
    // weight next to the product name. Auto-shown ONLY on instinctor.com (never on
    // client installs or demo subdomains — isInstinctorApp is false there), and never
    // when the brand IS "Instinctor" itself (avoids "Instinctor by Instinctor" on the
    // corporate home). An explicit brand_suffix setting overrides the text; set it to
    // '' with brand_suffix present to suppress it on a specific header. Sergei 2026-07-02.
    $brandSuffix = '';
    if (array_key_exists('brand_suffix', $settings)) {
        $brandSuffix = (string) $settings['brand_suffix'];
    } elseif ($siteName !== ''
              && strcasecmp(trim($siteName), 'Instinctor') !== 0
              && class_exists('\\Instinctor\\Core\\Plugin')
              && \Instinctor\Core\Plugin::isInstinctorApp()) {
        $brandSuffix = 'by Instinctor';
    }
    $siteNameHtml = esc_html($siteName);
    if ($brandSuffix !== '') {
        $siteNameHtml .= '<span class="impulse-hdr-brandsuffix" style="font-weight:300;font-size:0.7em;opacity:0.6;margin-left:6px;white-space:nowrap">' . esc_html($brandSuffix) . '</span>';
    }
    $logoImg     = (string) ($settings['logo'] ?? '');
    // Instinctor brand mark: when no custom logo image is set AND the brand is
    // Instinctor itself, prepend the circle+curve mark before the site name.
    // Gated on the brand so client-site headers (their own name) are untouched.
    $brandMark = '';
    if ($logoImg === '' && strcasecmp(trim($siteName), 'Instinctor') === 0) {
        $brandMark = '<svg viewBox="0 0 10.49 10.49" style="height:0.92em;width:auto;fill:currentColor;display:block;flex:0 0 auto" aria-hidden="true"><path d="M3.02 0.02l2.23 0c1.44,0 2.75,0.59 3.7,1.54 0.95,0.95 1.54,2.26 1.54,3.7 0,1.44 -0.59,2.75 -1.54,3.7 -0.95,0.95 -2.26,1.54 -3.7,1.54 -1.44,0 -2.75,-0.59 -3.7,-1.54 -0.95,-0.95 -1.54,-2.26 -1.54,-3.7l0 -2.23 2.31 0 0 2.23c0,0.8 0.33,1.53 0.86,2.06 0.53,0.53 1.26,0.86 2.06,0.86 0.8,0 1.53,-0.33 2.06,-0.86 0.53,-0.53 0.86,-1.26 0.86,-2.06 0,-0.8 -0.33,-1.53 -0.86,-2.06 -0.53,-0.53 -1.26,-0.86 -2.06,-0.86l-2.23 0 0 -2.31z"/><rect x="0.02" y="0.02" width="2.31" height="2.31"/></svg>';
    }
    // logo_url overrides the home link — designer/client pages link the logo
    // to their own home (e.g. /maxime), never the platform root.
    $logoLink    = !empty($settings['logo_url'])
        ? (string) $settings['logo_url']
        : (function_exists('home_url') ? home_url('/') : '/');
    $logoHeight  = intval($read('logo_height',   40));

    // Fallback menu for a header with no configured items. /shop and /contact are
    // not routes (404), so keep only live targets.
    $menuItems   = $settings['menu_items'] ?? [
        ['label' => 'Home',  'url' => '/'],
        ['label' => 'About', 'url' => '/about'],
    ];
    if (!is_array($menuItems)) $menuItems = [];

    // Normalize over-escaped URLs from legacy saves: \\/foo or \/foo → /foo
    $normUrl = static function(string $u): string {
        return ltrim(str_replace('\\/', '/', $u), '\\') ?: '#';
    };
    foreach ($menuItems as &$_item) {
        if (isset($_item['url'])) $_item['url'] = $normUrl((string) $_item['url']);
    }
    unset($_item);

    // Auto-surface the blog in the nav. Per-page headers each carry their own
    // menu, so rather than hand-adding "Blog" to every page (and every future
    // one), append it automatically when the site has a published /blog index
    // and the menu doesn't already include it. Cached per request; sites with
    // no published /blog page are completely unaffected.
    static $_impBlogPublished = null;
    if ($_impBlogPublished === null) {
        $_bp = \Instinctor\Services\PageService::getByPublicSlug('blog');
        $_impBlogPublished = ($_bp && ($_bp['status'] ?? '') === 'published');
    }
    if ($_impBlogPublished) {
        $_hasBlog = false;
        foreach ($menuItems as $_mi) {
            if (!is_array($_mi)) continue;
            $_lbl = strtolower(trim((string) ($_mi['label'] ?? '')));
            $_u   = rtrim((string) ($_mi['url'] ?? ''), '/');
            if ($_lbl === 'blog' || substr($_u, -5) === '/blog') { $_hasBlog = true; break; }
        }
        if (!$_hasBlog) $menuItems[] = ['label' => 'Blog', 'url' => '/blog'];
    }

    $menuGap      = intval($read('menu_gap',       32));
    $menuFontSize = intval($read('menu_font_size', 15));
    $menuPosition = (string) ($settings['menu_position'] ?? 'center');
    $iconSize     = intval($read('icon_size',      20));
    $sidePadding  = intval($read('side_padding',   40));
    $paddingVert  = intval($read('padding_top',    16));

    $ctaText      = (string) ($settings['cta_text']   ?? 'Get Started');
    $ctaUrl       = $normUrl((string) ($settings['cta_url']    ?? '#'));
    $ctaBg        = (string) ($settings['cta_bg']     ?? '#1d2327');
    $ctaColor     = (string) ($settings['cta_color']  ?? '#ffffff');
    $ctaRadius    = intval($settings['cta_radius']    ?? 6);

    $announcementText      = (string) ($settings['announcement_text']       ?? '');
    $announcementLink      = $normUrl((string) ($settings['announcement_link'] ?? ''));
    $announcementBg        = (string) ($settings['announcement_bg']         ?? '#1d2327');
    $announcementTextColor = (string) ($settings['announcement_text_color'] ?? '#ffffff');

    // Per-device boolean overrides — only meaningful in frontend mode
    // (where we emit CSS with @media rules). Editor already pre-resolved
    // each value via $read().
    $boolKeys = ['sticky', 'show_border', 'show_shadow', 'show_site_name', 'show_cart', 'show_account', 'show_cta', 'announcement_enabled', 'sitename_wordmark'];
    $tabletBools = [];
    $mobileBools = [];
    foreach ($boolKeys as $bk) {
        $tabletBools[$bk] = isset($settings[$bk . '_tablet']) ? !empty($settings[$bk . '_tablet']) : null;
        $mobileBools[$bk] = isset($settings[$bk . '_mobile']) ? !empty($settings[$bk . '_mobile']) : null;
    }

    // Per-device slider values (frontend's @media block uses these).
    $logoHeightTablet     = isset($settings['logo_height_tablet'])     ? intval($settings['logo_height_tablet'])     : $logoHeight;
    $logoHeightMobile     = isset($settings['logo_height_mobile'])     ? intval($settings['logo_height_mobile'])     : $logoHeightTablet;
    $menuGapTablet        = isset($settings['menu_gap_tablet'])        ? intval($settings['menu_gap_tablet'])        : $menuGap;
    $menuFontSizeTablet   = isset($settings['menu_font_size_tablet'])  ? intval($settings['menu_font_size_tablet'])  : $menuFontSize;
    $iconSizeTablet       = isset($settings['icon_size_tablet'])       ? intval($settings['icon_size_tablet'])       : $iconSize;
    $iconSizeMobile       = isset($settings['icon_size_mobile'])       ? intval($settings['icon_size_mobile'])       : $iconSizeTablet;

    // Cart count
    $cartCount = 0;
    if (class_exists('\\Instinctor\\Core\\CartSession')) {
        $cartData = \Instinctor\Core\CartSession::get('cart');
        if (is_array($cartData) && !empty($cartData['items'])) {
            foreach ($cartData['items'] as $_ci) {
                $cartCount += intval($_ci['quantity'] ?? 1);
            }
        }
    }

    $rawId    = (string) ($el['id'] ?? uniqid());
    $rawId    = preg_replace('/[^a-z0-9_-]/i', '', $rawId);
    $headerId = 'impulse-header-' . $rawId;
    $hid      = '#' . esc_attr($headerId); // Late-escape per WP coding standards

    // ── CSS ──────────────────────────────────────────────────────────────
    ob_start();
    echo esc_attr($hid) . ' .impulse-header-menu a{color:' . esc_attr($textColor) . ';text-decoration:none;font-size:' . (int) $menuFontSize . 'px;transition:color 0.2s}';
    echo esc_attr($hid) . ' .impulse-header-menu a:hover{color:' . esc_attr($hoverColor) . '}';
    echo esc_attr($hid) . ' .impulse-header-icons svg{transition:opacity 0.2s;cursor:pointer}';
    echo esc_attr($hid) . ' .impulse-header-icons svg:hover{opacity:0.7}';
    echo esc_attr($hid) . ' .impulse-header-logo img{height:' . (int) $logoHeight . 'px}';
    echo esc_attr($hid) . ' .impulse-header-menu{display:flex;gap:' . (int) $menuGap . 'px}';
    echo esc_attr($hid) . ' .impulse-header-icons svg{width:' . (int) $iconSize . 'px;height:' . (int) $iconSize . 'px}';
    echo esc_attr($hid) . ' .impulse-mobile-toggle{display:none}';
    echo esc_attr($hid) . ' .impulse-mobile-menu{display:none;flex-direction:column}';
    if ($sticky)               echo esc_attr($hid) . '{position:sticky;top:0;z-index:1000}';
    if ($autoHide)             echo esc_attr($hid) . '{transition:transform 0.3s ease-in-out}' . esc_attr($hid) . '.header-hidden{transform:translateY(-100%)}';
    // Every page is a stack of full-viewport screens (Hero zone + Body zone) —
    // the header MUST overlay, never push screens down. Regardless of
    // sticky_mode or viewport. This rule comes after $sticky so it wins.
    echo esc_attr($hid) . '{position:absolute!important;top:0;right:0;left:0;z-index:1000}';
    if (!$showCart)            echo esc_attr($hid) . ' .impulse-hdr-cart{display:none!important}';
    if (!$showAccount)         echo esc_attr($hid) . ' .impulse-hdr-account{display:none!important}';
    if (!$showCta)             echo esc_attr($hid) . ' .impulse-header-cta{display:none!important}';
    if (!$showSiteName)        echo esc_attr($hid) . ' .impulse-hdr-sitename{display:none!important}';
    if (!$announcementEnabled) echo esc_attr($hid) . ' .impulse-announcement-bar{display:none}';

    if (!$isEditor) {
        // ── Frontend: full responsive cascade via @media ─────────────────
        // Tablet styles (<=1024px) — burger menu + per-device visibility
        echo '@media(max-width:1024px){';
        // Float the header over the hero on tablet/mobile so the burger
        // appears directly over the first section. Background/border/
        // shadow stripped; pointer-events:none lets clicks reach the hero,
        // the burger and the open-state mobile menu re-enable them.
        echo esc_attr($hid) . '{position:absolute!important;top:0;right:0;left:0;background:transparent!important;border:none!important;box-shadow:none!important;pointer-events:none;z-index:1000}';
        echo esc_attr($hid) . ' .impulse-header-main{background:transparent!important;border:none!important;box-shadow:none!important;pointer-events:none}';
        // Every interactive child re-enables clicks - the blanket none on
        // header/main exists only so empty header strip lets clicks reach
        // the hero. Account/cart/logo/CTA were click-dead on phones when
        // only the burger was re-enabled. Sergei 2026-06-10.
        echo esc_attr($hid) . ' .impulse-mobile-toggle,' . esc_attr($hid) . ' .impulse-header-logo a,' . esc_attr($hid) . ' .impulse-header-icons a,' . esc_attr($hid) . ' .impulse-header-cta{pointer-events:auto}';
        // Open state keeps the header transparent - no solid repaint
        // (was background:menuSolidBg!important; dropped per Sergei
        // 2026-06-10: the dropdown floats over the hero instead).
        echo esc_attr($hid) . '.mobile-open{pointer-events:auto}';
        echo esc_attr($hid) . '.mobile-open .impulse-header-main{pointer-events:auto}';
        echo esc_attr($hid) . ' .impulse-header-logo img{height:' . (int) $logoHeightTablet . 'px}';
        echo esc_attr($hid) . ' .impulse-header-menu{display:none!important}';
        echo esc_attr($hid) . ' .impulse-mobile-toggle{display:flex!important}';
        echo esc_attr($hid) . '.mobile-open .impulse-mobile-menu{display:flex}';
        echo esc_attr($hid) . ' .impulse-header-menu{gap:' . (int) $menuGapTablet . 'px}';
        echo esc_attr($hid) . ' .impulse-header-menu a{font-size:' . (int) $menuFontSizeTablet . 'px}';
        echo esc_attr($hid) . ' .impulse-header-icons svg{width:' . (int) $iconSizeTablet . 'px;height:' . (int) $iconSizeTablet . 'px}';
        // Tablet boolean overrides
        if ($tabletBools['sticky'] === false)               echo esc_attr($hid) . '{position:relative!important}';
        elseif ($tabletBools['sticky'] === true && !$sticky) echo esc_attr($hid) . '{position:sticky;top:0;z-index:1000}';
        if ($tabletBools['show_cart'] === false)             echo esc_attr($hid) . ' .impulse-hdr-cart{display:none!important}';
        elseif ($tabletBools['show_cart'] === true && !$showCart)       echo esc_attr($hid) . ' .impulse-hdr-cart{display:flex!important}';
        if ($tabletBools['show_account'] === false)          echo esc_attr($hid) . ' .impulse-hdr-account{display:none!important}';
        elseif ($tabletBools['show_account'] === true && !$showAccount) echo esc_attr($hid) . ' .impulse-hdr-account{display:flex!important}';
        if ($tabletBools['show_cta'] === false)              echo esc_attr($hid) . ' .impulse-header-cta{display:none!important}';
        elseif ($tabletBools['show_cta'] === true && !$showCta)         echo esc_attr($hid) . ' .impulse-header-cta{display:inline-flex!important}';
        if ($tabletBools['show_site_name'] === false)        echo esc_attr($hid) . ' .impulse-hdr-sitename{display:none!important}';
        elseif ($tabletBools['show_site_name'] === true && !$showSiteName) echo esc_attr($hid) . ' .impulse-hdr-sitename{display:inline-flex!important}';
        if ($tabletBools['announcement_enabled'] === false)  echo esc_attr($hid) . ' .impulse-announcement-bar{display:none}';
        elseif ($tabletBools['announcement_enabled'] === true && !$announcementEnabled) echo esc_attr($hid) . ' .impulse-announcement-bar{display:block}';
        if ($tabletBools['show_border'] === false)           echo esc_attr($hid) . ' .impulse-header-main{border-bottom:none!important}';
        if ($tabletBools['show_shadow'] === false)           echo esc_attr($hid) . ' .impulse-header-main{box-shadow:none!important}';
        elseif ($tabletBools['show_shadow'] === true && !$showShadow)   echo esc_attr($hid) . ' .impulse-header-main{box-shadow:0 2px 8px rgba(0,0,0,0.15)}';
        echo '}';

        // Mobile (<=768px) — inherits tablet then applies mobile-specific overrides.
        echo '@media(max-width:768px){';
        // Tight side padding on phones - the desktop side_padding (40px)
        // left the logo floating with a large gap off the left edge.
        echo esc_attr($hid) . ' .impulse-header-main{padding-left:14px!important;padding-right:14px!important}';
        echo esc_attr($hid) . '{--impulse-content-padding:14px}';
        echo esc_attr($hid) . ' .impulse-header-logo img{height:' . (int) $logoHeightMobile . 'px}';
        echo esc_attr($hid) . ' .impulse-header-menu{display:none!important}';
        echo esc_attr($hid) . ' .impulse-mobile-toggle{display:flex!important}';
        echo esc_attr($hid) . '.mobile-open .impulse-mobile-menu{display:flex}';
        echo esc_attr($hid) . ' .impulse-header-icons svg{width:' . (int) $iconSizeMobile . 'px;height:' . (int) $iconSizeMobile . 'px}';
        // Mobile → tablet → desktop cascade for each toggle
        $mSticky        = $mobileBools['sticky']               ?? $tabletBools['sticky']               ?? $sticky;
        $mCart          = $mobileBools['show_cart']            ?? $tabletBools['show_cart']            ?? $showCart;
        $mAccount       = $mobileBools['show_account']         ?? $tabletBools['show_account']         ?? $showAccount;
        $mCta           = $mobileBools['show_cta']             ?? $tabletBools['show_cta']             ?? $showCta;
        $mSiteName      = $mobileBools['show_site_name']       ?? $tabletBools['show_site_name']       ?? $showSiteName;
        $mAnnouncement  = $mobileBools['announcement_enabled'] ?? $tabletBools['announcement_enabled'] ?? $announcementEnabled;
        $mBorder        = $mobileBools['show_border']          ?? $tabletBools['show_border']          ?? $showBorder;
        $mShadow        = $mobileBools['show_shadow']          ?? $tabletBools['show_shadow']          ?? $showShadow;
        if (!$mSticky)         echo esc_attr($hid) . '{position:relative!important}';
        elseif ($mSticky && !$sticky) echo esc_attr($hid) . '{position:sticky;top:0;z-index:1000}';
        if (!$mCart)           echo esc_attr($hid) . ' .impulse-hdr-cart{display:none!important}';
        elseif ($mCart)        echo esc_attr($hid) . ' .impulse-hdr-cart{display:flex!important}';
        if (!$mAccount)        echo esc_attr($hid) . ' .impulse-hdr-account{display:none!important}';
        elseif ($mAccount)     echo esc_attr($hid) . ' .impulse-hdr-account{display:flex!important}';
        if (!$mCta)            echo esc_attr($hid) . ' .impulse-header-cta{display:none!important}';
        elseif ($mCta)         echo esc_attr($hid) . ' .impulse-header-cta{display:inline-flex!important}';
        if (!$mSiteName)       echo esc_attr($hid) . ' .impulse-hdr-sitename{display:none!important}';
        elseif ($mSiteName)    echo esc_attr($hid) . ' .impulse-hdr-sitename{display:inline-flex!important}';
        if (!$mAnnouncement)   echo esc_attr($hid) . ' .impulse-announcement-bar{display:none}';
        elseif ($mAnnouncement) echo esc_attr($hid) . ' .impulse-announcement-bar{display:block}';
        if (!$mBorder)         echo esc_attr($hid) . ' .impulse-header-main{border-bottom:none!important}';
        if (!$mShadow)         echo esc_attr($hid) . ' .impulse-header-main{box-shadow:none!important}';
        elseif ($mShadow && !$showShadow) echo esc_attr($hid) . ' .impulse-header-main{box-shadow:0 2px 8px rgba(0,0,0,0.15)}';
        echo '}';
    } else {
        // ── Editor: just emit the burger/mobile state if the current
        // preview device is tablet/mobile. No @media queries because the
        // device-toggle re-renders this whole element.
        if ($device === 'tablet' || $device === 'mobile') {
            echo esc_attr($hid) . ' .impulse-header-menu{display:none!important}';
            echo esc_attr($hid) . ' .impulse-mobile-toggle{display:flex!important}';
            echo esc_attr($hid) . '.mobile-open .impulse-mobile-menu{display:flex!important}';
        }
        if ($device === 'mobile') {
            // Mirror the frontend's <=768px side-padding override.
            echo esc_attr($hid) . ' .impulse-header-main{padding-left:14px!important;padding-right:14px!important}';
            echo esc_attr($hid) . '{--impulse-content-padding:14px}';
        }
    }

    $css = ob_get_clean();

    // ── Markup ───────────────────────────────────────────────────────────
    // The header's scoped CSS must travel WITH the markup on editor AJAX
    // re-renders: a structural toggle (sticky_mode, show_cart, layout, ...)
    // re-fetches ONLY this element's HTML via refreshElement — no full page
    // load, so no wp_footer, so wp_add_inline_style's queued CSS never
    // reaches the iframe. The icons then fall back to their inline
    // display:flex defaults: cart / account / CTA / burger / mobile-menu all
    // appear regardless of toggle state and the bar "explodes" into a broken
    // multi-row layout. So in the editor we emit an inline <style> that ships
    // with the header HTML; the frontend keeps wp_add_inline_style for WP.org
    // Guideline-11 compliance (full page loads flush the handle correctly).
    // Regression fixed 2026-07-04: the inline-style->enqueue conversion had
    // replaced the inline <style> unconditionally, dropping the editor path
    // this comment already warned about.
    ob_start();
    if ($isEditor) {
        // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- editor-only preview CSS; it MUST ship inline with the AJAX-re-rendered header markup because wp_add_inline_style is dropped on element-level re-renders (no wp_footer fires).
        echo '<style>' . esc_html($css) . '</style>';
    } else {
        wp_add_inline_style('instinctor-page-inline', $css);
    }
    echo '<header id="' . esc_attr($headerId) . '" class="impulse-header">';

    // Announcement bar — always rendered; CSS controls per-device visibility.
    echo '<div class="impulse-announcement-bar" style="background:' . esc_attr($announcementBg) . ';color:' . esc_attr($announcementTextColor) . ';text-align:center;padding:10px 20px;font-size:13px">';
    // Only emit content when there is announcement text. $normUrl turns an empty
    // link into '#', so without this guard an empty bar renders a stray
    // <a href="#"></a>. A link needs BOTH text and a real (non-#) href; text with
    // no link renders as plain text.
    if ($announcementText !== '') {
        if ($announcementLink !== '' && $announcementLink !== '#') {
            echo '<a href="' . esc_url($announcementLink) . '" class="imp-link-reset">' . esc_html($announcementText) . '</a>';
        } else {
            echo esc_html($announcementText);
        }
    }
    echo '</div>';

    // Main header — three layouts.
    //
    // Padding ownership (Sergei 2026-06-08, v2.1.1315):
    // padding lives on .impulse-header-main, NOT .impulse-header-inner.
    // The editor's element-config maps padding_top / padding_bottom /
    // side_padding to .impulse-header-main (see assets/js/impulse-element-
    // config.js lines 1882-1898), so on first selection the live-update
    // writes the SAME values onto main — if PHP also put padding on inner,
    // the header gains the saved padding twice (visibly: clicking the
    // header bumps its height by ~padding_top). Single source = main.
    if ($layout === 'logo-center') {
        $headerStyle = $bgCss . 'position:relative;'
                     . 'padding:' . $paddingVert . 'px ' . $sidePadding . 'px;';
        if ($showBorder && $borderWidth > 0) $headerStyle .= 'border-bottom:' . $borderWidth . 'px solid ' . esc_attr($borderColor) . ';';
        if ($showShadow)                    $headerStyle .= 'box-shadow:0 2px 8px rgba(0,0,0,0.15);';
        $innerStyle = 'display:flex;flex-direction:column;align-items:center;width:100%;box-sizing:border-box;position:relative;';

        echo '<div class="impulse-header-main" style="' . esc_attr($headerStyle) . '">';
        echo '<div class="impulse-header-inner" style="' . esc_attr($innerStyle) . '">';

        // Logo centered
        echo '<div class="impulse-header-logo" style="margin-bottom:12px;display:flex;align-items:center;justify-content:center;gap:10px">';
        if ($logoImg) {
            echo '<a href="' . esc_url($logoLink) . '"><img src="' . esc_url($logoImg) . '" alt="' . esc_attr($siteName) . '" style="height:' . (int) $logoHeight . 'px;width:auto;display:block"></a>';
        }
        // Graphic + textual logo can show together — the text's visibility is
        // governed solely by the Site Name toggle (CSS rule emitted above).
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $brandMark is a hardcoded static SVG logo mark (no dynamic input); other values esc'd inline.
        echo '<a class="impulse-hdr-sitename" href="' . esc_url($logoLink) . '" style="' . esc_attr($wordmarkStyle) . 'color:' . esc_attr($textColor) . ';text-decoration:none;display:inline-flex;align-items:center;gap:8px">' . $brandMark . wp_kses_post($siteNameHtml) . '</a>';
        echo '</div>';

        // Menu below logo
        $centerMenuJustify = 'center';
        switch ($menuPosition) {
            case 'left':           $centerMenuJustify = 'flex-start';    break;
            case 'right':          $centerMenuJustify = 'flex-end';      break;
            case 'space-between':  $centerMenuJustify = 'space-between'; break;
        }
        echo '<nav class="impulse-header-menu" style="gap:' . (int) $menuGap . 'px;justify-content:' . esc_attr($centerMenuJustify) . '">';
        foreach ($menuItems as $item) {
            if (!empty($item['label'])) {
                echo '<a href="' . esc_url((string) ($item['url'] ?? '#')) . '">' . esc_html((string) $item['label']) . '</a>';
            }
        }
        echo '</nav>';

        // Icons positioned absolute right
        echo '<div class="impulse-header-icons" style="position:absolute;right:var(--impulse-content-padding, 40px);top:50%;transform:translateY(-50%);display:flex;gap:16px;align-items:center">';
    } else {
        // Left or right layout — padding on .impulse-header-main per the
        // ownership note above (v2.1.1315).
        $headerStyle = $bgCss . 'padding:' . $paddingVert . 'px ' . $sidePadding . 'px;';
        if ($showBorder && $borderWidth > 0) $headerStyle .= 'border-bottom:' . $borderWidth . 'px solid ' . esc_attr($borderColor) . ';';
        if ($showShadow)                    $headerStyle .= 'box-shadow:0 2px 8px rgba(0,0,0,0.15);';
        $flexDir    = $layout === 'logo-right' ? 'row-reverse' : 'row';
        $innerStyle = 'display:flex;flex-direction:' . $flexDir . ';align-items:center;justify-content:space-between;width:100%;box-sizing:border-box;';

        echo '<div class="impulse-header-main" style="' . esc_attr($headerStyle) . '">';
        echo '<div class="impulse-header-inner" style="' . esc_attr($innerStyle) . '">';

        // Logo
        echo '<div class="impulse-header-logo" style="flex-shrink:0;display:flex;align-items:center;gap:10px">';
        if ($logoImg) {
            echo '<a href="' . esc_url($logoLink) . '"><img src="' . esc_url($logoImg) . '" alt="' . esc_attr($siteName) . '" style="height:' . (int) $logoHeight . 'px;width:auto;display:block"></a>';
        }
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $brandMark is a hardcoded static SVG logo mark (no dynamic input); other values esc'd inline.
        echo '<a class="impulse-hdr-sitename" href="' . esc_url($logoLink) . '" style="' . esc_attr($wordmarkStyle) . 'color:' . esc_attr($textColor) . ';text-decoration:none;display:inline-flex;align-items:center;gap:8px">' . $brandMark . wp_kses_post($siteNameHtml) . '</a>';
        echo '</div>';

        // Menu
        $navStyle = 'gap:' . $menuGap . 'px;align-items:center;';
        switch ($menuPosition) {
            case 'left':          $navStyle .= 'margin-right:auto;'; break;
            case 'right':         $navStyle .= 'margin-left:auto;';  break;
            case 'space-between': $navStyle .= 'flex:1;justify-content:center;'; break;
            default:              $navStyle .= 'margin-left:auto;margin-right:auto;'; break;
        }
        echo '<nav class="impulse-header-menu" style="' . esc_attr($navStyle) . '">';
        foreach ($menuItems as $item) {
            if (!empty($item['label'])) {
                echo '<a href="' . esc_url((string) ($item['url'] ?? '#')) . '">' . esc_html((string) $item['label']) . '</a>';
            }
        }
        echo '</nav>';

        // Right icons
        // margin-left separates the cluster from the nav - with menu_position:
        // right the nav's margin-left:auto pressed the last menu item flush
        // against the first icon. Sergei 2026-06-10.
        echo '<div class="impulse-header-icons" style="display:flex;gap:16px;align-items:center;margin-left:28px">';
    }

    // CTA button — always rendered, CSS controls visibility.
    echo '<a class="impulse-header-cta" href="' . esc_url($ctaUrl) . '" style="display:inline-flex;padding:8px 18px;background:' . esc_attr($ctaBg) . ';color:' . esc_attr($ctaColor) . ';border-radius:' . (int) $ctaRadius . 'px;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap">' . esc_html($ctaText) . '</a>';

    // Mobile menu toggle — hidden on desktop via CSS. Also skipped
    // entirely when there are no menu items with labels (Sergei: a
    // burger that opens an empty container should not be shown).
    $_hasMenuItems = false;
    foreach ($menuItems as $_mi) {
        if (is_array($_mi) && !empty($_mi['label'])) { $_hasMenuItems = true; break; }
    }
    if ($_hasMenuItems) {
        // A11y contract for the burger (Sergei 2026-08-12). The icon carries no
        // text, so without aria-label a screen reader announces bare "button".
        // aria-expanded has to be written by the same handler that flips the
        // class or the two states drift apart, and focus moves into the menu on
        // open and back to the burger on close so keyboard users are not
        // stranded at the top of the page. The SVG is decorative: hiding it
        // keeps the label from competing with the path data.
        $_mmId = $headerId . '-mobile-menu';
        echo '<button class="impulse-mobile-toggle" type="button" aria-label="' . esc_attr__('Menu', 'instinctor') . '"'
            . ' aria-expanded="false" aria-controls="' . esc_attr($_mmId) . '"'
            . ' style="background:none;border:none;cursor:pointer;padding:0"'
            . ' onclick="var h=this.closest(\'.impulse-header\'),o=h.classList.toggle(\'mobile-open\');this.setAttribute(\'aria-expanded\',o?\'true\':\'false\');var m=h.querySelector(\'.impulse-mobile-menu\'),f=m&&m.querySelector(\'a[href]\');if(o){if(f)f.focus();}else{this.focus();}">';
        echo '<svg aria-hidden="true" focusable="false" width="' . (int) $iconSize . '" height="' . (int) $iconSize . '" viewBox="0 0 24 24" fill="none" stroke="' . esc_attr($textColor) . '" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>';
        echo '</button>';
    }

    // Cart + account icons. The cart is also hidden for memberships-only stores
    // (subscribers go straight to checkout and the cart page is trashed, so the
    // link would 404).
    //
    // VISIBILITY IS CSS, NOT MARKUP (see the show_cart/show_account rules above and
    // the tablet/mobile overrides): the icons are emitted once and then shown or
    // hidden per breakpoint, because one server render has to serve all three
    // devices. Googlebot does not read CSS, so a display:none'd <a> is still
    // crawled and followed — instinctor.com has show_account=false on every header
    // yet Search Console filled up with "Not found (404)" for /account, which is
    // neither a page nor a route there (it 200s only on aokarate). So: skip the
    // markup entirely when the icon is hidden on EVERY device, and keep emitting it
    // whenever any breakpoint shows it, so the cascade still works. rel="nofollow"
    // on top — these are per-user utility links, never content, and nothing is
    // gained by spending crawl budget on them. Sergei 2026-07-16.
    $cartUrl    = (string) ($settings['cart_url']    ?? '/cart');
    $accountUrl = (string) ($settings['account_url'] ?? '/account');
    $_storeType = class_exists('\\Instinctor\\Core\\StoreFeatures') ? \Instinctor\Core\StoreFeatures::getStoreType() : '';
    $_cartAnyDevice    = $showCart
        || ($tabletBools['show_cart'] ?? null) === true
        || ($mobileBools['show_cart'] ?? null) === true;
    $_accountAnyDevice = $showAccount
        || ($tabletBools['show_account'] ?? null) === true
        || ($mobileBools['show_account'] ?? null) === true;
    if ($_storeType !== 'memberships' && $_cartAnyDevice) {
    echo '<a class="impulse-hdr-cart" href="' . esc_url($cartUrl) . '" rel="nofollow" style="position:relative;display:flex;color:' . esc_attr($textColor) . ';text-decoration:none">';
    echo '<svg width="' . (int) $iconSize . '" height="' . (int) $iconSize . '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>';
    echo '<span class="impulse-cart-count" style="position:absolute;top:-8px;right:-8px;background:#1d2327;color:#fff;font-size:10px;min-width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center">' . intval($cartCount) . '</span>';
    echo '</a>';
    }
    if ($_accountAnyDevice) {
    echo '<a class="impulse-hdr-account" href="' . esc_url($accountUrl) . '" rel="nofollow" style="display:flex;color:' . esc_attr($textColor) . ';text-decoration:none">';
    echo '<svg width="' . (int) $iconSize . '" height="' . (int) $iconSize . '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
    echo '</a>';
    }
    echo '</div>'; // .impulse-header-icons

    echo '</div>'; // .impulse-header-inner
    echo '</div>'; // .impulse-header-main

    // Mobile menu (hidden by default, shown by .mobile-open).
    // Compact float-over dropdown: transparent (no solid panel), no
    // border-top, no per-link divider lines, tight vertical rhythm.
    // Sergei 2026-06-10.
    // Frosted backdrop (same glass recipes as the header band): light
    // text gets the dark glass, dark text gets the white frost. The
    // fully transparent first cut made light menu items invisible on
    // white pages. Translucent + blurred, never a solid panel.
    $menuFrost = $frostLight
        ? 'background:rgba(255,255,255,0.72);-webkit-backdrop-filter:blur(16px) saturate(1.5);backdrop-filter:blur(16px) saturate(1.5);'
        : 'background:rgba(0,0,0,0.45);-webkit-backdrop-filter:blur(16px) brightness(0.5);backdrop-filter:blur(16px) brightness(0.5);';
    // id is the aria-controls target of the burger above. The menu is shown and
    // hidden with display, which already removes it from the accessibility tree
    // and the tab order when closed, so no aria-hidden/inert bookkeeping is
    // needed here (verified live 2026-08-12: a link inside the closed menu
    // refuses focus at every width).
    echo '<nav id="' . esc_attr($headerId . '-mobile-menu') . '" class="impulse-mobile-menu" style="' . esc_attr($menuFrost) . 'padding:4px var(--impulse-content-padding, 14px) 12px">';
    foreach ($menuItems as $item) {
        if (!empty($item['label'])) {
            echo '<a href="' . esc_url((string) ($item['url'] ?? '#')) . '" style="color:' . esc_attr($textColor) . ';text-decoration:none;padding:6px 0">' . esc_html((string) $item['label']) . '</a>';
        }
    }
    echo '</nav>';

    // Breadcrumbs (optional)
    $showBreadcrumbs = !empty($read('show_breadcrumbs', false));
    if ($showBreadcrumbs) {
        $bcColor = '#666';
        echo '<nav class="impulse-breadcrumbs" style="font-size:13px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:8px var(--impulse-content-padding, 40px);max-width:1200px;margin:0 auto;color:' . esc_attr($bcColor) . '">';
        if (function_exists('wp_add_inline_style')) {
            wp_add_inline_style('instinctor-page-inline', '.impulse-breadcrumbs a{color:' . esc_attr($textColor) . ';text-decoration:none}.impulse-breadcrumbs a:hover{text-decoration:underline}');
        }
        $breadcrumbs = [];
        $homeUrl = !empty($settings['logo_url']) ? (string) $settings['logo_url'] : (function_exists('home_url') ? home_url('/') : '/');
        $breadcrumbs[] = '<a href="' . esc_url($homeUrl) . '">Home</a>';

        $productSlug = function_exists('get_query_var') ? get_query_var('instinctor_product') : '';
        if (!$productSlug) {
            // WP.org B2 — sanitize $_SERVER on both branches.
            $path = function_exists('wp_parse_url')
                ? trim((string) wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '/')), PHP_URL_PATH), '/')
                : trim(sanitize_text_field(wp_unslash((string) ($_SERVER['REQUEST_URI'] ?? '/'))), '/');
            if (preg_match('#^product/([^/]+)#', $path, $matches)) { $productSlug = $matches[1]; }
        }
        if ($productSlug) {
            $product = null;
            if (class_exists('\\Instinctor\\Core\\DB')) {
                $product = \Instinctor\Core\DB::get_row(\Instinctor\Core\DB::prepare(
                    "SELECT id, name FROM " . \Instinctor\Core\DB::prefix() . "instinctor_products WHERE slug = %s",
                    sanitize_text_field((string) $productSlug)
                ));
            } else {
                global $wpdb;
                if (isset($wpdb)) {
                    $product = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- plugin's own custom table; query is prepared; per-request render value, not object-cacheable
                        "SELECT id, name FROM {$wpdb->prefix}instinctor_products WHERE slug = %s",
                        sanitize_text_field((string) $productSlug)
                    ));
                }
            }
            if ($product) {
                $shopUrl = class_exists('\\Instinctor\\Services\\PageService')
                    ? \Instinctor\Services\PageService::resolveUrl('shop')
                    : '/shop';
                $breadcrumbs[] = '<a href="' . esc_url($shopUrl) . '">Shop</a>';
                if (class_exists('\\Instinctor\\Services\\CategoryService')) {
                    $categories = \Instinctor\Services\CategoryService::getProductCategories($product->id);
                    if (!empty($categories)) {
                        $breadcrumbs[] = '<span>' . esc_html((string) ($categories[0]->name ?? '')) . '</span>';
                    }
                }
                $breadcrumbs[] = '<span style="color:' . esc_attr($bcColor) . '">' . esc_html((string) ($product->name ?? '')) . '</span>';
            }
        } elseif (function_exists('is_page') && is_page()) {
            $breadcrumbs[] = '<span style="color:' . esc_attr($bcColor) . '">' . esc_html(function_exists('get_the_title') ? get_the_title() : '') . '</span>';
        } elseif (function_exists('is_single') && is_single()) {
            if (function_exists('get_the_category')) {
                $cats = get_the_category();
                if ($cats) {
                    $breadcrumbs[] = '<a href="' . esc_url(get_category_link($cats[0]->term_id)) . '">' . esc_html($cats[0]->name) . '</a>';
                }
            }
            $breadcrumbs[] = '<span style="color:' . esc_attr($bcColor) . '">' . esc_html(function_exists('get_the_title') ? get_the_title() : '') . '</span>';
        }
        echo wp_kses(implode(' <span style="margin:0 4px">/</span> ', $breadcrumbs), impulse_richtext_allowed());
        echo '</nav>';
    }

    echo '</header>';

    // Auto-hide scroll JS (frontend only — editor has no real scroll).
    if ($autoHide && !$isEditor) {
        wp_add_inline_script('instinctor-frontend-js', '(function(){var ly=0,tk=false,hdr=document.getElementById("' . $headerId . '");if(!hdr)return;window.addEventListener("scroll",function(){if(!tk){requestAnimationFrame(function(){var cy=window.scrollY||document.documentElement.scrollTop;if(cy>ly&&cy>60){hdr.classList.add("header-hidden")}else{hdr.classList.remove("header-hidden")}ly=cy<0?0:cy;tk=false});tk=true}},{passive:true})})();', 'after');
    }

    // Header-height publisher (v2.1.1332). Measures the rendered header
    // and writes `--impulse-header-height` to <html> so every section's
    // padding-top calc() reserves room for it. Re-runs on resize + a
    // few delayed ticks to catch font/image loads that change the
    // header's intrinsic size.
    wp_add_inline_script('instinctor-frontend-js', '(function(){var id="' . $headerId . '";function p(){var h=document.getElementById(id);if(!h)return;var v=Math.round(h.getBoundingClientRect().height);if(v>0)document.documentElement.style.setProperty("--impulse-header-height",v+"px");}p();window.addEventListener("resize",p,{passive:true});window.addEventListener("load",p);setTimeout(p,150);setTimeout(p,800);setTimeout(p,2000);})();', 'after');

    return (string) ob_get_clean();
}
