=== DSS Header Level 1 ===
Contributors: ejscorp
Tags: header, woocommerce, mobile, responsive, shortcode
Requires at least: 5.8
Tested up to: 6.6
Requires PHP: 7.4
Stable tag: 1.4.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A fully responsive, admin-configurable header for WordPress + WooCommerce. Drop the [dss_header_level1] shortcode anywhere — page content, Gutenberg block, theme template, or Timber/Twig.

== Description ==

DSS Header Level 1 ships TWO synchronised layouts in one shortcode:

* **Desktop header** — logo, search bar, My Account link, favourites icon with badge, cart link with subtotal & count, AU-wide delivery map graphic.
* **Mobile-first shell** — fixed header, promo bar, bottom navigation (Home / Browse / Action / Cart / Account), full-screen drawer, search overlay, and four bottom sheets (Browse / Quick Actions / Cart / Account).

The active layout is picked purely with CSS media queries — no UA sniffing, fully cache-safe. WooCommerce AJAX cart fragments keep all four cart badges and the mini-cart panel in sync after every add-to-cart.

= Highlights =

* **Single shortcode** — `[dss_header_level1]`. Works in posts, pages, widgets, Gutenberg blocks, theme templates (`do_shortcode()`), and Timber/Twig (`{{ "[dss_header_level1]"|shortcodes }}`).
* **Tabbed admin UI** — 10 tabs covering Branding, Colors, Desktop, Mobile, Drawer, Search, Quick Actions, Cart, Account, and Advanced settings.
* **CSS-variable theming** — change five colours in the admin and the entire palette updates everywhere.
* **Configurable breakpoint** (480–1600 px).
* **Self-contained CSS/JS** — scoped under `.dss-hl1-wrap`, won't conflict with the rest of your theme.
* **WooCommerce-aware** — real cart fragments, real login form, real account menu. Works gracefully without WooCommerce too.
* **Custom CSS** field for last-mile tweaks.
* **Developer filters** for favourites count, favourites URL, and browse-category queries.
* **Nav-menu icons** — give any menu item the CSS class `sale-icon-fire` in Appearance -> Menus and the bundled flame icon is attached to the left of its label, automatically. Works even with themes that render their menu through a custom walker and strip per-item CSS classes: the plugin reads the tagged item from the menu data and matches the rendered link by URL. Fully namespaced and `!important`-hardened, so no theme, plugin, or global stylesheet can move, hide, recolour, or resize it.

== Installation ==

1. Upload the `dss-header-level1` folder to `/wp-content/plugins/` (or upload the .zip via Plugins → Add New).
2. Activate the plugin.
3. Go to **Settings → DSS Header** to configure logos, colours, and layout.
4. Paste `[dss_header_level1]` into your theme template, a page, or a widget.

If an inline `[header_level1]` shortcode is already registered elsewhere (e.g. in `functions.php` or a mu-plugin), the plugin will quietly skip registering the alias to avoid fatal redeclaration. Remove the inline version to let the plugin handle it.

== Frequently Asked Questions ==

= Where do I put the shortcode? =

Anywhere `do_shortcode()` runs: page/post content, Gutenberg shortcode blocks, widgets, theme templates, or Twig templates using `{{ "[dss_header_level1]"|shortcodes }}`.

= I want to change just one icon link =

Open **Settings → DSS Header → Drawer** (for mobile drawer cards) or **Quick Actions** (for the centre-button sheet). Every tile has its own label, hint, URL, and icon.

= How do I theme the favourites count? =

Add a filter that returns an integer:

`add_filter( 'dss_hl1_favorites_count', function () { return YourWishlist::count(); } );`

= Can I swap the built-in search for a third-party search plugin? =

Yes — paste the third-party shortcode into **Desktop → Custom search shortcode** (e.g. `[ivory_search id="123"]`).

= How do I put the flame icon on the Sale menu item? =

Go to **Appearance -> Menus**, open the "Sale" item, and type `sale-icon-fire` into the **CSS Classes (optional)** field. (If you don't see that field, open **Screen Options** at the top of the page and tick "CSS Classes".) Save the menu. That's it — the icon is attached everywhere that menu renders, on every theme, on desktop and mobile.

= Can I use my own icon, or add more icons? =

Yes. Register as many class -> icon pairs as you like:

`add_filter( 'dss_hl1_menu_icons', function ( $icons ) {
    $icons['new-icon-star'] = array(
        'url'      => 'https://example.com/star.svg',
        'width'    => 18,
        'height'   => 18,
        'ratio'    => '1 / 1',
        'position' => 'before',
        'size'     => '1.3em',
        'gap'      => '0.34em',
    );
    return $icons;
} );`

Other filters: `dss_hl1_menu_icons_enabled` (master switch), `dss_hl1_menu_icons_js` (the client-side safety net), `dss_hl1_menu_icons_observe` (keep watching the DOM for re-rendered menus), `dss_hl1_menu_icons_host_class` (the extra `dss-mi-host` class on the `<li>`).

= How do I resize the icon or change the spacing? =

Set the custom properties on the icon's own class from **Settings -> DSS Header -> Advanced -> Custom CSS**:

`.dss-mi-icon--sale-icon-fire { --dss-mi-size: 1.6em; --dss-mi-gap: 0.4em; --dss-mi-shift: -0.3em; }`

`--dss-mi-size` is the rendered height, `--dss-mi-gap` the space between icon and label, and `--dss-mi-shift` the baseline nudge.

= My WooCommerce cart count isn't updating live =

Make sure **Cart Sheet → Enable WooCommerce AJAX cart fragments** is checked. If you also have a caching plugin, exclude the cart fragments endpoint from caching.

== Changelog ==

= 1.4.0 =
* New: **Drawer brand logo.** Settings → DSS Header → Branding now has a "Drawer brand logo" media field. Upload an image and the mobile menu drawer renders that logo at the top instead of the "Discount / Safety Signs / tagline" text block. Leave it empty — the default — and the text block renders exactly as it always has.
* Deliberately standalone: this field does **not** inherit the desktop or mobile header logo. A fallback chain would have silently replaced the drawer's text brand on every existing install that already has a header logo, which is a behaviour change nobody asked for. An empty drawer-logo field always means "keep the text".
* New: "Drawer logo max height" (24–200px, default 56). Width scales automatically to preserve the aspect ratio. Emitted as a `--drawer-logo-max-h` custom property and re-clamped at render time as well as on save, so a value written straight into the option row by WP-CLI, a migration, or a filter can never produce a malformed length.
* Accessibility: with the text swapped out, the logo's alt text becomes the only accessible name for the drawer's homepage link, so it resolves through a guaranteed-non-empty chain — Site name (alt text) → brand name line 1 + line 2 → site title → "Home". The image also carries intrinsic width/height when the selection is a media-library attachment, preventing layout shift as the drawer paints. Those dimensions are only used when the resolved URL genuinely belongs to that attachment, so a stale attachment ID falling back to a pasted URL cannot borrow the wrong aspect ratio.
* Internal: the shared admin `input_text()` helper gained optional `min` / `max` / `step` support. Emitted only when explicitly passed, so the markup produced for all ~30 pre-existing fields is byte-identical.
* Upgrade safety: new keys are seeded through the existing `wp_parse_args()` merge in `DSS_HL1_Options::all()`, so a 1.3.x options row missing them entirely resolves to the defaults with no upgrade routine and no fatal.
* Verified against the real renderer under a WordPress function shim: default state renders the text block unchanged; logo renders from an attachment ID and from a pasted URL; a stale ID falls back correctly; header logos do not leak into the drawer; the full alt-text fallback chain; alt/src escaping against injection; height clamping at both boundaries and for empty, missing, negative, out-of-range and non-numeric values; a legacy options row; and all ten settings tabs render with zero PHP notices or warnings. A full render diff against 1.3.1 with default options shows only the intended CSS additions and a whitespace-only indentation shift in the drawer brand block — the emitted DOM is otherwise identical. The 1.3.1 bottom-nav suite was re-run and still passes in full.

= 1.3.1 =
* Fixed: the bottom-nav Cart tab could stay in its accent (yellow) state permanently — icon, label, top indicator and cell tint all stuck on — no matter which other tab was tapped afterwards. Home, Browse, Features and Account were unaffected in normal use, so Cart looked "always selected" next to four correctly-behaving siblings.
* Root cause (two independent defects, both in the bottom-sheet controller):
    1. The accent state is driven entirely by the CSS rule `.nav-item[aria-expanded="true"]`, but that attribute was written from individual click handlers. The bottom-nav sheets are built with an empty `openTriggers` array (they bind their own handlers so they can enforce mutual exclusion), so `createOverlay.close()` had no triggers to reset. Any dismissal that did not go through the tab itself — the sheet's close button, its backdrop, or the Escape key — closed the sheet and left the tab still claiming `aria-expanded="true"`. `closeOtherSheets()` then skipped it, because it only reset triggers whose sheet was *currently open*, so the stale state was never repaired.
    2. Cart is the only sheet with **two** triggers: the mobile header cart icon and the bottom-nav Cart tab both carry `data-sheet-target="sheet-cart"`. Each handler updated only the element it was bound to, so opening from one control and closing from the other left the unattended control stuck at `true`. This is why the fault presented on Cart alone.
* Fix: `aria-expanded` is no longer written by event handlers. Every trigger is now reconciled against the single authority that cannot drift — whether its sheet element actually carries `is-open` — through a new `onStateChange` hook on the overlay controller that fires on open and close from *every* path (tab tap, close button, backdrop, Escape, programmatic auto-open, and mutual-exclusion displacement), plus a baseline pass on init. The invariant now holds unconditionally: a tab is accent if and only if its sheet is on screen.
* Also fixed by the same change: a server-flagged auto-opened sheet (the WooCommerce login-error Account sheet) previously opened without lighting its tab; and the mobile header cart icon now correctly reports the expanded state of the panel it controls. The header icon has no `aria-expanded` styling of its own, so this is an accessibility correction with no visual change.
* Scope: JavaScript only. No CSS rule, no markup, no PHP logic, no setting, and no other feature was modified — Cart already shared the identical `.nav-item` styling with its siblings, so nothing needed to change there. Verified with a DOM-level behavioural suite (jsdom) driving the plugin's real inline script across 60+ assertions: the reported scenario, all four close paths, both cart triggers in every open/close permutation, per-tab parity, rapid-switch stress, mutual exclusion, preserved `preventDefault()` on both cart links, and the public `DSS.setCartCount()` badge API. All pass on 1.3.1; the same suite reproduces 45 failures on 1.3.0.

= 1.3.0 =
* Bottom nav: the centre action mark is now an inline nav item, matching the size, alignment and caption style of Home / Browse / Cart / Account. It previously rendered as a 72px round button raised above the bar, which read as off-centre because the bar reserves space on the right for the floating chat widget.
* New setting: "Bottom-nav action label" sets the caption under the centre mark. Defaults to "Features". Leave empty to show the mark with no caption.
* Accessibility: the centre item's accessible name now comes from its visible caption instead of a mismatched aria-label, satisfying WCAG 2.5.3 Label in Name.

= 1.2.10 =
* Fixed (regression introduced in 1.2.9): the notch mask was applied directly to `.bottom-nav`, which is also the ancestor of `.nav-action` — the centre action button that intentionally rises 26px above the bar's own top edge via negative `margin-top`, per the existing (pre-1.2.7) design. A `mask-image` constrains *all* rendered content of the element it's applied to, including overflowing descendants — so the rising button (the yellow action icon, e.g. a triangle/checkmark or a site's own uploaded mark via `action_mark_url`) was being flat-cut exactly at the bar's top edge instead of coming to its normal point. Fix: the background and mask now live on a dedicated `.bottom-nav::before` layer (`inset:0`, no children of its own — nothing can overflow it), while `.bottom-nav` itself carries no background or mask at all. `.bottom-nav__inner` and the rising `.nav-action` are unaffected by the mask entirely and render exactly as they did before 1.2.7. `mask-repeat:no-repeat` is now also set explicitly (previously relying on the browser default) to rule out any tiling artifact on top of this.
* Changed: the notch is deeper — `--bottom-nav-notch-radius` raised from `56px` to `84px` — and the reserved icon zone widened to match (`--bottom-nav-safe-right` `84px` → `114px`; the sub-360px breakpoint `72px` → `106px`), keeping the same ~28-30px safety margin between the two so the notch can visually reach further without ever geometrically reaching the actual nav icons.
* Verified: a to-scale pixel render of the corrected structure (masked background layer + separately-drawn, unclipped rising action button) confirms the action icon now renders with its full point intact, and the deeper notch reaches the requested position. `php -l` clean across the full plugin tree; diff against 1.2.9 confirms only these lines changed.

= 1.2.9 =
* Changed: the reserved right-hand safe zone from 1.2.7 was a flat rectangle when no third-party widget was covering it. It's now a genuine concave cutout in the bar itself: a `mask-image`/`-webkit-mask-image` radial-gradient centred exactly on the bar's own top-right corner, with a new `--bottom-nav-notch-radius` (`56px`) custom property controlling how deep it curves. This is a true mask, not a painted shape — the corner is actually empty/see-through, showing whatever's behind the bar, present whether or not a chat widget is sitting there.
* Superseded: an earlier same-day approach painted an opaque same-colour "raised tab" over that corner instead of cutting into it — visually it was a bump, not a notch, and didn't match the reference. That rule has been fully removed (along with its now-unused `--bottom-nav-corner-rise` / `--bottom-nav-corner-radius` properties) and replaced by the mask described above; nothing from that attempt remains in this build.
* Why a mask and not `border-radius`: both create a transparent cutout, so this was never a functional concern — `border-radius` on a corner only rounds a simple arc, while `mask-image` with a radial-gradient can be positioned and sized independently of the element's box (centred exactly on the corner point, radius tuned separately from the reserved width), which is what let the curve be matched to the reference precisely.
* Contained by construction: the notch radius (`56px`) is kept safely inside the `84px` reserved zone (`72px` on the sub-360px breakpoint) with margin to spare, so it can visually reach into the bar but can never geometrically reach far enough left to cut into the actual nav icons — verified with a to-scale pixel render of the exact mask math (not just the CSS, the actual resulting shape) before shipping, both with and without a simulated widget bubble in place.
* Scope: `class-dss-hl1-renderer.php` only. Verified with `php -l` across the full plugin tree and a full diff against 1.2.7 confirming every other line is byte-identical.

= 1.2.7 =
* Fixed: on the mobile bottom navigation, the Account icon (and part of the Cart icon) could sit directly underneath a third-party floating widget — most commonly a live-chat bubble injected by a separate plugin/script, which is not part of this plugin and outside its control — making those icons impossible to tap. The bottom-nav row now reserves a safe zone on its right edge via a new `--bottom-nav-safe-right` CSS custom property (default `84px`, reduced to `72px` under the existing 360px "tiny tuning" breakpoint) so all five items (Home / Browse / Action / Cart / Account) compress and shift left, leaving that zone clear. The bottom-nav bar itself is untouched — still full-width, same background, same fixed position — only the icons' own layout shifts left within it.
* Changed: `--bottom-nav-safe-right` is a plain CSS custom property (not an admin setting), consistent with how `--action-btn-size` and `--action-btn-rise` are already handled — a structural layout constant, not admin-configurable. If a specific site's chat/widget footprint needs a different reserved width, this single value can be adjusted directly (or overridden per-site via the existing "Custom CSS" field in Advanced settings, e.g. `.dss-hl1-wrap{--bottom-nav-safe-right:100px}`), with no other change required.
* Added: two small defensive rules so the compression itself can never regress into the same overlap problem — `min-width:0` on `.nav-item`/`.nav-action` (so the grid can actually shrink to its compressed track width instead of forcing an overflow on the narrowest screens) and a dedicated `.nav-item__label` rule that truncates with an ellipsis instead of visually spilling past its column if a label (including a longer translated string) doesn't fit. Both are no-ops at every normal screen width — they only engage as a safety net.
* Unchanged: markup, JS, PHP logic, and every other CSS rule are byte-for-byte identical to 1.2.6 — this is a CSS-only layout change confined to three declarations inside the existing mobile-shell stylesheet (`bottom-nav__inner`, its 360px override, and the new custom property). No option keys, hooks, filters, or JS selectors were added, removed, or renamed, so WooCommerce cart fragments, the bottom sheets, ripple effects, and the account auto-open behaviour all continue to work exactly as before.
* Scope: `class-dss-hl1-renderer.php` only (plus the version bump and this changelog entry). Verified with `php -l` across the full plugin tree, a full directory diff against 1.2.6 confirming every other file — and every other line of this file — is byte-identical, and a manual grid-track calculation across 320px/360px/375px/390px/430px viewport widths confirming every nav item stays within its compressed column with no overflow into the reserved zone.

= 1.2.6 =
* Changed: the "Show promo bar" and "Promo bar text" controls moved from **Desktop** to **Mobile → Promo bar**. The promo bar (the caution-tape strip reading e.g. "ISO 7010 • AS 1319 • FREE AU SHIPPING • 48H DISPATCH") only ever renders inside the mobile-first shell (`.dss-mobile-only`) — it has no desktop counterpart and never did — so having its controls on the Desktop tab was misleading. This is an admin-UI reorganisation only.
* Unchanged: the underlying option keys (`show_promo_bar`, `promo_text`), their defaults, sanitisation, and the front-end output are byte-for-byte identical to 1.2.5 — the two fields simply moved to a different tab in the settings screen. Existing saved values carry over automatically; no database migration, no re-saving required.
* Scope: `class-dss-hl1-settings.php` only (plus the readme/version bump). No other file was touched. Verified with `php -l` across the full plugin tree and a full directory diff against 1.2.5 confirming every other file is byte-identical.

= 1.2.5 =
* Changed: restyled the desktop cart hover popup (`.dss-hl1-cart-popup`) to match a reference design — larger card radius (14px → 20px) and padding (16px → 24px), a softer shadow, full pill-shaped buttons (`border-radius:999px`) in sentence case, and a thin 1px divider in place of the heavy 2px rule above the subtotal.
* Changed: `View cart` and `Checkout` button colours are swapped — `View cart` is now the filled accent-colour button (`var(--dss-color-accent)`, so it still tracks the admin's colour picker), `Checkout` is now solid black with a white label.
* Changed: the product thumbnail in this popup is hidden (`display:none`) so each line reads as title → variation details → quantity × unit price, stacked; the thumbnail markup itself is untouched, so it's a one-line CSS revert if a future update needs it back.
* Added: a close button (✕) on the popup, top-left. Since this popup opens on CSS `:hover`/`:focus-within` (no JS state), the button adds an `is-popup-closed` class to the cart wrapper on click, which a higher-specificity CSS rule uses to force the popup hidden even while still hovered; `mouseleave`/`focusout` clear the class again so the very next hover opens it normally. Vanilla JS, no dependencies, added as section 7 of the existing inline script.
* Added: on the popup only, each cart line now shows the product's variation attributes (e.g. "Colour: Black Text, Silver Background") via WooCommerce's own `wc_get_formatted_cart_item_data()` — nothing to configure, works automatically for any variable product. Simple products show nothing extra, unchanged.
* Added: `1 × $36.09`-style unit-price line on the popup (replacing the old "Qty 1" label), computed from `WC_Cart::get_product_price()` — this is deliberately the **unit** price, not the line subtotal, so it reads correctly for quantities > 1 (e.g. "2 × $40.00", not the $80 line total mislabelled as a unit price).
* Added: an optional small corner badge, off by default. One variation attribute can be pulled out of the meta list and shown as a pill badge instead (matching a reference design's "Exit Level B3" badge) by hooking `dss_hl1_cart_popup_badge_attribute`, e.g.:
  `add_filter( 'dss_hl1_cart_popup_badge_attribute', function () { return 'Exit Level'; } );`
  This targets a *variation attribute* by its label. If "Exit Level" on your site is a custom field rather than a variation attribute, this hook won't find it — that would need a small follow-up change once the field/meta key is confirmed.
* Fixed: `DSS_HL1_Cart::render_mini_cart()` now takes a `$context` parameter (`'sheet'` default, `'popup'` for the new desktop layout) so the mobile bottom-sheet cart is completely unaffected by any of the above — its markup, classes, and behaviour are byte-for-byte the same as 1.2.4.
* Fixed: a latent AJAX-fragments bug this change would otherwise have exposed — both the desktop popup and the mobile sheet wrap their content in the same `.dss-cart-content` class, so the old fragment key `.dss-cart-content` pushed identical HTML into *both* on every add-to-cart, even though they can now render different content. Fragment keys are now scoped separately (`.dss-hl1-cart-popup .dss-cart-content` and `#sheet-cart .dss-cart-content`), so AJAX updates match the context-appropriate initial render in both places.
* Scope: `class-dss-hl1-cart.php` and `class-dss-hl1-renderer.php` only. No other file, hook, or the mobile shell was touched. Verified with `php -l` on every file, a full directory diff against 1.2.4, and a rendered screenshot test of the actual generated markup/CSS at the plugin's real default colours and desktop breakpoint.

= 1.2.4 =
* Added: a CSS-only fallback for the `sale-icon-fire` flame. On themes that strip the admin's CSS class from the rendered menu, the icon now paints via a `::before` rule generated from the same URL the plugin already resolves for the tagged menu item — with zero JavaScript dependency, so it renders on the very first paint instead of waiting for a script to run, and still works if a visitor has JavaScript disabled.
* The rule is scoped to nav/list containers (so an unrelated link elsewhere on the page sharing the same URL is never decorated) and is self-suppressing via `:not(:has(...))`: if the class-based or JS layer successfully inserts the real icon first, the CSS pseudo-element does not render, so an item can still never end up with two icons.
* Requires `:has()` selector support (Chrome/Edge 105+, Safari 15.4+, Firefox 121+ — all released 2022 or earlier). Browsers without it simply do not receive this particular fallback layer; the existing class-based and JS href/label layers are unaffected either way.

= 1.2.3 =
* Fixed: the flame did not appear on themes whose primary menu is rendered by a custom nav walker or a manual `wp_get_nav_menu_items()` loop that hardcodes its own `<li>` class and discards the per-item "CSS Classes (optional)" value. Because that markup never contains `sale-icon-fire`, every earlier layer — the three nav-menu filters and the client-side class scan — had nothing to match, so no icon was injected (no error, no icon).
* Added: a theme-agnostic fallback. The plugin now reads which menu items actually carry each registered class straight from the menu data in the database (WordPress stores it there even when the theme does not render it), resolves their URL and label, and the client injects the icon by matching the rendered anchor's `href` (or label). This is scoped to list/nav containers, so a link to the same URL elsewhere on the page is never decorated. The map is cached in a transient and refreshed automatically whenever a menu is edited.
* Unchanged: the standard-theme path (class present in markup) still works exactly as before; the new layer is additive and idempotent, so an item can still never receive two icons.
* New filters: `dss_hl1_menu_icons_targets_enabled` (bool, master switch for the fallback) and `dss_hl1_menu_icon_targets` (filter the resolved URL/label map).

= 1.2.2 =
* Changed: the bundled `sale-icon-fire` flame is now a two-tone icon (red-orange outer flame with a yellow inner flame) that matches the intended design, replacing the earlier flat single-tone flame.
* Changed: the embedded default icon is now delivered as an SVG data URI instead of a PNG, so the flame stays razor-sharp at any menu font size and on any pixel density. It is still self-contained in the PHP, so it can never 404 or be stripped by a CDN/optimiser, and it is validated before use exactly as before.
* Added: `assets/images/sale-icon-fire.svg` (scalable source of truth). `assets/images/sale-icon-fire.png` is regenerated from it as a 200x252 RGBA raster for anyone who prefers to serve a cacheable file via the `dss_hl1_menu_icons` filter.
* Housekeeping: removed a stale nested `level1.zip` that had been left inside the plugin folder. No behaviour change; the injector, its filters, isolation hardening, and the `sale-icon-fire` class trigger are all unchanged.

= 1.2.1 =
* Fixed: if `assets/images/sale-icon-fire.png` was missing on the server, the icon registry resolved to nothing and the module silently did nothing at all — no icon, no stylesheet, no script, and no visible symptom. The flame is now embedded in the PHP as a data URI and is used automatically whenever a file cannot be resolved, so a partial upload can no longer make the icon disappear.
* Fixed: `esc_url()` strips `data:` URIs because `data` is not in `wp_allowed_protocols()`. Data URIs are now validated by pattern and escaped with `esc_attr()`.
* Changed: the stylesheet and safety-net script are now printed unconditionally on the front end. They used to be gated behind a `WP_Query` that looked for nav-menu items carrying a registered class; when that query missed — a non-WordPress menu, a theme that rebuilds the class list, a stale cached result — nothing was printed. The assets are inline and tiny, and this removes a database read per page.
* Changed: the client-side layer no longer requires an `<a>` inside the element carrying the class. It injects into the anchor when there is one and into the element itself otherwise, so the class now works on any element a theme or page builder renders, not only on WordPress menu items.

= 1.2.0 =
* New: nav-menu icon injector. Any WordPress menu item carrying the CSS class `sale-icon-fire` automatically renders the bundled flame icon immediately before its label.
* Injection is layered — `nav_menu_item_title`, then `walker_nav_menu_start_el`, then a client-side safety net — so it works with core menus, custom walkers, page builders, AJAX-loaded menus, and cloned sticky headers. Every layer is idempotent, so an item can never receive two icons.
* Styles are namespaced under `dss-mi-*`, printed late in `wp_head`, and hardened with `all:unset` plus `!important` at 3x class specificity. The same declarations are mirrored as inline styles, so the icon still renders correctly even if the stylesheet is stripped by an optimiser.
* Extensible through `dss_hl1_menu_icons` (register more class -> icon pairs) and tunable through the `--dss-mi-size`, `--dss-mi-gap` and `--dss-mi-shift` custom properties.
* Detection of icon-bearing menu items is cached in a transient and invalidated whenever a menu is saved, so no extra queries run on normal page loads.

= 1.1.0 =
* Added live (predictive) "search as you type" dropdown for the desktop and mobile header search boxes. Searches WooCommerce product titles and SKUs via AJAX, with thumbnail, SKU, category and price. Configurable under Settings → DSS Header → Desktop (enable/disable, min characters, max results, show SKU, show price).

= 1.0.0 =
* Initial release.
