diff --git a/shared b/shared
index 65c4989..322ae48 160000
--- a/shared
+++ b/shared
@@ -1 +1 @@
-Subproject commit 65c4989d08b85a821a0932e4376f2ab088f50d0c
+Subproject commit 322ae481eeb47418245e669c3585f6d2195656da
diff --git a/templates/_types/browse/_admin.html b/templates/_types/browse/_admin.html
new file mode 100644
index 0000000..e3cf3a2
--- /dev/null
+++ b/templates/_types/browse/_admin.html
@@ -0,0 +1,7 @@
+{% import "macros/links.html" as links %}
+{% if g.rights.admin %}
+ {% from 'macros/admin_nav.html' import admin_nav_item %}
+ {{admin_nav_item(
+ url_for('market.browse.product.admin', product_slug=slug)
+ )}}
+{% endif %}
\ No newline at end of file
diff --git a/templates/_types/browse/_main_panel.html b/templates/_types/browse/_main_panel.html
new file mode 100644
index 0000000..8640ce8
--- /dev/null
+++ b/templates/_types/browse/_main_panel.html
@@ -0,0 +1,5 @@
+
+
+ {% include "_types/browse/_product_cards.html" %}
+
+
diff --git a/templates/_types/browse/_product_card.html b/templates/_types/browse/_product_card.html
new file mode 100644
index 0000000..b923bc5
--- /dev/null
+++ b/templates/_types/browse/_product_card.html
@@ -0,0 +1,104 @@
+{% import 'macros/stickers.html' as stick %}
+{% import '_types/product/prices.html' as prices %}
+{% set prices_ns = namespace() %}
+{{ prices.set_prices(p, prices_ns) }}
+{% set item_href = url_for('market.browse.product.product_detail', product_slug=p.slug)|host %}
+
\ No newline at end of file
diff --git a/templates/_types/browse/_product_cards.html b/templates/_types/browse/_product_cards.html
new file mode 100644
index 0000000..cc8edb3
--- /dev/null
+++ b/templates/_types/browse/_product_cards.html
@@ -0,0 +1,107 @@
+{% for p in products %}
+ {% include "_types/browse/_product_card.html" %}
+{% endfor %}
+{% if page < total_pages|int %}
+
+
+
+ {% include "sentinel/mobile_content.html" %}
+
+
+
+ {% include "sentinel/desktop_content.html" %}
+
+{% else %}
+ End of results
+{% endif %}
+
diff --git a/templates/_types/browse/desktop/_category_selector.html b/templates/_types/browse/desktop/_category_selector.html
new file mode 100644
index 0000000..b3c68b6
--- /dev/null
+++ b/templates/_types/browse/desktop/_category_selector.html
@@ -0,0 +1,40 @@
+{# Categories #}
+
+
+ {% set top_active = (sub_slug is not defined or sub_slug is none or sub_slug == '') %}
+ {% set href = (url_for('market.browse.browse_top', top_slug=top_slug) ~ qs)|host %}
+
+
+ All products
+
+
+
+ {% for sub in subs_local %}
+ {% set active = (sub.slug == sub_slug) %}
+ {% set href = (url_for('market.browse.browse_sub', top_slug=top_slug, sub_slug=sub.slug) ~ qs)|host %}
+
+
+ {{ (sub.html_label or sub.name) | safe }}
+
+
+ {% endfor %}
+
+
diff --git a/templates/_types/browse/desktop/_filter/brand.html b/templates/_types/browse/desktop/_filter/brand.html
new file mode 100644
index 0000000..616e36e
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/brand.html
@@ -0,0 +1,40 @@
+{# Brand filter (desktop, single-select) #}
+
+{# Brands #}
+
+ Brands
+
+
diff --git a/templates/_types/browse/desktop/_filter/labels.html b/templates/_types/browse/desktop/_filter/labels.html
new file mode 100644
index 0000000..7a4a41e
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/labels.html
@@ -0,0 +1,44 @@
+
+
+
+{% import 'macros/stickers.html' as stick %}
+
+
diff --git a/templates/_types/browse/desktop/_filter/like.html b/templates/_types/browse/desktop/_filter/like.html
new file mode 100644
index 0000000..c830f98
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/like.html
@@ -0,0 +1,38 @@
+{% import 'macros/stickers.html' as stick %}
+ {% set qs = {"liked": None if liked else True, "page": None}|qs %}
+ {% set href = (current_local_href ~ qs)|host %}
+
+ {% if liked %}
+
+ {% else %}
+
+ {% endif %}
+
+ {{ liked_count }}
+
+
diff --git a/templates/_types/browse/desktop/_filter/search.html b/templates/_types/browse/desktop/_filter/search.html
new file mode 100644
index 0000000..2e0ea8e
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/search.html
@@ -0,0 +1,44 @@
+
+{% macro search(current_local_href,search, search_count, hx_select) -%}
+
+
+
+
+
+
+ {% if search %}
+ {{search_count}}
+ {% endif %}
+ {{zap_filter}}
+
+
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/browse/desktop/_filter/sort.html b/templates/_types/browse/desktop/_filter/sort.html
new file mode 100644
index 0000000..a4b5404
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/sort.html
@@ -0,0 +1,34 @@
+
+
+
+
+{% import 'macros/stickers.html' as stick %}
+{% set sort_val = sort|default('az', true) %}
+
+
+ {% for key,label,icon in sort_options %}
+ {% set is_on = (sort_val == key) %}
+ {% set qs = {"sort": None, "page": None}|qs if is_on
+ else {"sort": key, "page": None}|qs %}
+ {% set href = (current_local_href ~ qs)|host %}
+
+
+
+ {{ stick.sticker(asset_url(icon), label, is_on) }}
+
+
+ {% endfor %}
+
diff --git a/templates/_types/browse/desktop/_filter/stickers.html b/templates/_types/browse/desktop/_filter/stickers.html
new file mode 100644
index 0000000..46fd22b
--- /dev/null
+++ b/templates/_types/browse/desktop/_filter/stickers.html
@@ -0,0 +1,46 @@
+
+
+
+
+{% import 'macros/stickers.html' as stick %}
+
+
diff --git a/templates/_types/browse/desktop/menu.html b/templates/_types/browse/desktop/menu.html
new file mode 100644
index 0000000..893cf2d
--- /dev/null
+++ b/templates/_types/browse/desktop/menu.html
@@ -0,0 +1,37 @@
+ {% import '_types/browse/desktop/_filter/search.html' as s %}
+ {{ s.search(current_local_href, search, search_count, hx_select) }}
+
+
+
+ {% include "_types/browse/desktop/_filter/sort.html" %}
+
+ {% include "_types/browse/desktop/_filter/like.html" %}
+ {% if labels %}
+ {% include "_types/browse/desktop/_filter/labels.html" %}
+ {% endif %}
+
+
+ {% if stickers %}
+ {% include "_types/browse/desktop/_filter/stickers.html" %}
+ {% endif %}
+
+
+ {% if subs_local and top_local_href %}
+ {% include "_types/browse/desktop/_category_selector.html" %}
+ {% endif %}
+
+
+
+
+
+ {% include "_types/browse/desktop/_filter/brand.html" %}
+
+
diff --git a/templates/_types/browse/index.html b/templates/_types/browse/index.html
new file mode 100644
index 0000000..015e6b3
--- /dev/null
+++ b/templates/_types/browse/index.html
@@ -0,0 +1,13 @@
+{% extends '_types/market/index.html' %}
+
+{% block filter %}
+ {% include "_types/browse/mobile/_filter/summary.html" %}
+{% endblock %}
+
+{% block aside %}
+ {% include "_types/browse/desktop/menu.html" %}
+{% endblock %}
+
+{% block content %}
+ {% include "_types/browse/_main_panel.html" %}
+{% endblock %}
diff --git a/templates/_types/browse/like/button.html b/templates/_types/browse/like/button.html
new file mode 100644
index 0000000..426bdc1
--- /dev/null
+++ b/templates/_types/browse/like/button.html
@@ -0,0 +1,20 @@
+
+ {% if liked %}
+
+ {% else %}
+
+ {% endif %}
+
diff --git a/templates/_types/browse/mobile/_filter/brand_ul.html b/templates/_types/browse/mobile/_filter/brand_ul.html
new file mode 100644
index 0000000..ac15400
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/brand_ul.html
@@ -0,0 +1,40 @@
+
+ {% if brands|length %}
+ Brands
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/templates/_types/browse/mobile/_filter/index.html b/templates/_types/browse/mobile/_filter/index.html
new file mode 100644
index 0000000..7c2a615
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/index.html
@@ -0,0 +1,30 @@
+
+ {% include "_types/browse/mobile/_filter/sort_ul.html" %}
+ {% if search or selected_labels|length or selected_stickers|length or selected_brands|length %}
+ {% set href = (current_local_href ~ {"clear_filters": True}|qs)|host %}
+
+ {% endif %}
+
+ {% include "_types/browse/mobile/_filter/like.html" %}
+ {% include "_types/browse/mobile/_filter/labels.html" %}
+
+ {% include "_types/browse/mobile/_filter/stickers.html" %}
+ {% include "_types/browse/mobile/_filter/brand_ul.html" %}
+
diff --git a/templates/_types/browse/mobile/_filter/labels.html b/templates/_types/browse/mobile/_filter/labels.html
new file mode 100644
index 0000000..3868d42
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/labels.html
@@ -0,0 +1,47 @@
+{% import 'macros/stickers.html' as stick %}
+
+ {# One row only; center when not overflowing; horizontal scroll when needed #}
+
+
+
+{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #}
+
diff --git a/templates/_types/browse/mobile/_filter/like.html b/templates/_types/browse/mobile/_filter/like.html
new file mode 100644
index 0000000..509ea92
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/like.html
@@ -0,0 +1,40 @@
+{% import 'macros/stickers.html' as stick %}
+
+ {% set qs = {"liked": None if liked else True, "page": None}|qs%}
+ {% set href = (current_local_href ~ qs)|host %}
+
+ {% if liked %}
+
+ {% else %}
+
+ {% endif %}
+
+ {{ liked_count }}
+
+
+
\ No newline at end of file
diff --git a/templates/_types/browse/mobile/_filter/search.html b/templates/_types/browse/mobile/_filter/search.html
new file mode 100644
index 0000000..0f39178
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/search.html
@@ -0,0 +1,40 @@
+{% macro search(current_local_href, search, search_count, hx_select) -%}
+
+
+
+
+
+ {% if search %}
+ {{search_count}}
+ {% endif %}
+
+
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/browse/mobile/_filter/sort_ul.html b/templates/_types/browse/mobile/_filter/sort_ul.html
new file mode 100644
index 0000000..c02de19
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/sort_ul.html
@@ -0,0 +1,33 @@
+
+
+
+{% import 'macros/stickers.html' as stick %}
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/_types/browse/mobile/_filter/stickers.html b/templates/_types/browse/mobile/_filter/stickers.html
new file mode 100644
index 0000000..fed0927
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/stickers.html
@@ -0,0 +1,50 @@
+{% import 'macros/stickers.html' as stick %}
+
+
+ {# One row only; center when not overflowing; horizontal scroll when needed #}
+
+
+
+{# Optional: hide horizontal scrollbar on mobile while keeping scrollable #}
+
diff --git a/templates/_types/browse/mobile/_filter/summary.html b/templates/_types/browse/mobile/_filter/summary.html
new file mode 100644
index 0000000..07a86a1
--- /dev/null
+++ b/templates/_types/browse/mobile/_filter/summary.html
@@ -0,0 +1,120 @@
+{% import 'macros/stickers.html' as stick %}
+{% import 'macros/layout.html' as layout %}
+
+
+
+
+{% call layout.details('/filter', 'md:hidden') %}
+ {% call layout.filter_summary("filter-summary-mobile", current_local_href, search, search_count, hx_select) %}
+
+
+
+
+ {% if sort %}
+
+
+ {% for k,l,i in sort_options %}
+ {% if k == sort %}
+ {% set key = k %}
+ {% set label = l %}
+ {% set icon = i %}
+
+ {{ stick.sticker(asset_url(icon), label, True)}}
+
+ {% endif %}
+ {% endfor %}
+
+ {% endif %}
+ {% if liked %}
+
+
+ {% if liked_count is not none %}
+
+ {{ liked_count }}
+
+ {% endif %}
+
+ {% endif %}
+ {% if selected_labels and selected_labels|length %}
+
+ {% for st in selected_labels %}
+ {% for s in labels %}
+ {% if st == s.name %}
+
+ {{ stick.sticker(asset_url('nav-labels/' + s.name + '.svg'), s.name, True)}}
+ {% if s.count is not none %}
+
+ {{ s.count }}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+ {% endif %}
+ {% if selected_stickers and selected_stickers|length %}
+
+ {% for st in selected_stickers %}
+ {% for s in stickers %}
+ {% if st == s.name %}
+
+
+ {{ stick.sticker(asset_url('stickers/' + s.name + '.svg'), s.name, True)}}
+ {% if s.count is not none %}
+
+ {{ s.count }}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% if selected_brands and selected_brands|length %}
+
+ {% for b in selected_brands %}
+
+ {% set ns = namespace(count=0) %}
+ {% for brand in brands %}
+ {% if brand.name == b %}
+ {% set ns.count = brand.count %}
+ {% endif %}
+ {% endfor %}
+ {% if ns.count %}
+ {{ b }}
+ {{ ns.count }}
+ {% else %}
+ {{ b }}
+ 0
+ {% endif %}
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {% endcall %}
+
+ {% include "_types/browse/mobile/_filter/index.html" %}
+
+{% endcall %}
diff --git a/templates/_types/cart/_mini.html b/templates/_types/cart/_mini.html
new file mode 100644
index 0000000..a8255e4
--- /dev/null
+++ b/templates/_types/cart/_mini.html
@@ -0,0 +1,45 @@
+{% macro mini(oob=False, count=None) %}
+
+ {# cart_count is set by the context processor in all apps.
+ Cart app computes it from g.cart + calendar_cart_entries;
+ other apps get it from the cart internal API.
+ count param allows explicit override when macro is imported without context. #}
+ {% if count is not none %}
+ {% set _count = count %}
+ {% elif cart_count is defined and cart_count is not none %}
+ {% set _count = cart_count %}
+ {% elif cart is defined and cart is not none %}
+ {% set _count = (cart | sum(attribute="quantity")) + ((calendar_cart_entries | length) if calendar_cart_entries else 0) %}
+ {% else %}
+ {% set _count = 0 %}
+ {% endif %}
+
+ {% if _count == 0 %}
+
+ {% else %}
+
+
+
+
+
+ {{ _count }}
+
+
+ {% endif %}
+
+{% endmacro %}
diff --git a/templates/_types/market/_admin.html b/templates/_types/market/_admin.html
new file mode 100644
index 0000000..0b09927
--- /dev/null
+++ b/templates/_types/market/_admin.html
@@ -0,0 +1,7 @@
+{% import "macros/links.html" as links %}
+{% if g.rights.admin %}
+ {% from 'macros/admin_nav.html' import admin_nav_item %}
+ {{admin_nav_item(
+ url_for('market.admin.admin')
+ )}}
+{% endif %}
\ No newline at end of file
diff --git a/templates/_types/market/_main_panel.html b/templates/_types/market/_main_panel.html
new file mode 100644
index 0000000..87bb965
--- /dev/null
+++ b/templates/_types/market/_main_panel.html
@@ -0,0 +1,23 @@
+{# Main panel fragment for HTMX navigation - market landing page #}
+
+ {% if post.custom_excerpt %}
+
+ {{post.custom_excerpt|safe}}
+
+ {% endif %}
+ {% if post.feature_image %}
+
+
+
+ {% endif %}
+
+ {% if post.html %}
+ {{post.html|safe}}
+ {% endif %}
+
+
+
diff --git a/templates/_types/market/_title.html b/templates/_types/market/_title.html
new file mode 100644
index 0000000..6e8024b
--- /dev/null
+++ b/templates/_types/market/_title.html
@@ -0,0 +1,17 @@
+
+
+
+ {{ market_title }}
+
+
+
+ {{top_slug or ''}}
+
+ {% if sub_slug %}
+
+ {{sub_slug}}
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/templates/_types/market/admin/_main_panel.html b/templates/_types/market/admin/_main_panel.html
new file mode 100644
index 0000000..a354325
--- /dev/null
+++ b/templates/_types/market/admin/_main_panel.html
@@ -0,0 +1 @@
+market admin
\ No newline at end of file
diff --git a/templates/_types/market/admin/_nav.html b/templates/_types/market/admin/_nav.html
new file mode 100644
index 0000000..f5c504d
--- /dev/null
+++ b/templates/_types/market/admin/_nav.html
@@ -0,0 +1,2 @@
+{% from 'macros/admin_nav.html' import placeholder_nav %}
+{{ placeholder_nav() }}
diff --git a/templates/_types/market/admin/_oob_elements.html b/templates/_types/market/admin/_oob_elements.html
new file mode 100644
index 0000000..9b306fd
--- /dev/null
+++ b/templates/_types/market/admin/_oob_elements.html
@@ -0,0 +1,29 @@
+{% extends 'oob_elements.html' %}
+
+{# OOB elements for HTMX navigation - all elements that need updating #}
+
+{# Import shared OOB macros #}
+{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
+
+{# Header with app title - includes cart-mini, navigation, and market-specific header #}
+
+{% block oobs %}
+
+ {% from '_types/root/_n/macros.html' import oob_header with context %}
+ {{oob_header('market-header-child', 'market-admin-header-child', '_types/market/admin/header/_header.html')}}
+
+ {% from '_types/market/header/_header.html' import header_row with context %}
+ {{ header_row(oob=True) }}
+{% endblock %}
+
+
+{% block mobile_menu %}
+ {% include '_types/market/admin/_nav.html' %}
+{% endblock %}
+
+
+{% block content %}
+ {% include "_types/market/admin/_main_panel.html" %}
+{% endblock %}
+
+
diff --git a/templates/_types/market/admin/header/_header.html b/templates/_types/market/admin/header/_header.html
new file mode 100644
index 0000000..950eefc
--- /dev/null
+++ b/templates/_types/market/admin/header/_header.html
@@ -0,0 +1,11 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='market-admin-row', oob=oob) %}
+ {% call links.link(url_for('market.admin.admin'), hx_select_search) %}
+ {{ links.admin() }}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/market/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/market/admin/index.html b/templates/_types/market/admin/index.html
new file mode 100644
index 0000000..4798c46
--- /dev/null
+++ b/templates/_types/market/admin/index.html
@@ -0,0 +1,19 @@
+{% extends '_types/market/index.html' %}
+
+
+{% block market_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('market-admin-header-child', '_types/market/admin/header/_header.html') %}
+ {% block market_admin_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+{% block _main_mobile_menu %}
+ {% include '_types/market/admin/_nav.html' %}
+{% endblock %}
+
+
+{% block content %}
+ {% include '_types/market/admin/_main_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/market/desktop/_nav.html b/templates/_types/market/desktop/_nav.html
new file mode 100644
index 0000000..d4de6e6
--- /dev/null
+++ b/templates/_types/market/desktop/_nav.html
@@ -0,0 +1,38 @@
+
+
+ {% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %}
+ {% set all_active = (category_label == 'All Products') %}
+
+
+ {% for cat, data in categories.items() %}
+ {% set cat_href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%}
+ {% set cat_active = (cat == category_label) %}
+
+ {% endfor %}
+ {% include '_types/market/_admin.html' %}
+
diff --git a/templates/_types/market/header/_header.html b/templates/_types/market/header/_header.html
new file mode 100644
index 0000000..2d92286
--- /dev/null
+++ b/templates/_types/market/header/_header.html
@@ -0,0 +1,11 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='market-row', oob=oob) %}
+ {% call links.link(url_for('market.browse.home'), hx_select_search ) %}
+ {% include '_types/market/_title.html' %}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/market/desktop/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/market/mobile/_nav_panel.html b/templates/_types/market/mobile/_nav_panel.html
new file mode 100644
index 0000000..65a9685
--- /dev/null
+++ b/templates/_types/market/mobile/_nav_panel.html
@@ -0,0 +1,110 @@
+{% from 'macros/glyphs.html' import opener %}
+
+
+ {% set all_href = (url_for('market.browse.browse_all') ~ qs)|host %}
+ {% set all_active = (category_label == 'All Products') %}
+
+
+ All
+
+
+ {% for cat, data in categories.items() %}
+
+
+
+ {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host %}
+
+
+ {{ cat }}
+ {{ data.count }}
+
+ {{ opener('cat')}}
+
+
+
+
+ {% if data.subs %}
+
+
+
+
+ {% for sub in data.subs %}
+ {% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~qs)|host%}
+ {% if top_slug==(data.slug | lower) and sub_slug == sub.slug %}
+
+ {{ sub.html_label or sub.name }}
+ {{ sub.count }}
+
+ {% endif %}
+ {% endfor %}
+ {% for sub in data.subs %}
+ {% if not (top_slug==(data.slug | lower) and sub_slug == sub.slug) %}
+ {% set href = (url_for('market.browse.browse_sub', top_slug=data.slug, sub_slug=sub.slug) ~ qs)|host%}
+
+ {{ sub.name }}
+ {{ sub.count }}
+
+ {% endif %}
+ {% endfor %}
+
+
+ {% else %}
+ {% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%}
+
View all
+ {% endif %}
+
+
+ {% endfor %}
+ {% include '_types/market/_admin.html' %}
+
+
diff --git a/templates/_types/market/mobile/menu.html b/templates/_types/market/mobile/menu.html
new file mode 100644
index 0000000..145b551
--- /dev/null
+++ b/templates/_types/market/mobile/menu.html
@@ -0,0 +1,6 @@
+{% extends 'mobile/menu.html' %}
+{% block menu %}
+ {% block mobile_menu %}
+ {% endblock %}
+ {% include '_types/market/mobile/_nav_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/post/_nav.html b/templates/_types/post/_nav.html
new file mode 100644
index 0000000..037bdcd
--- /dev/null
+++ b/templates/_types/post/_nav.html
@@ -0,0 +1,15 @@
+{% import 'macros/links.html' as links %}
+ {# Widget-driven container nav — entries, calendars, markets #}
+ {% if container_nav_widgets %}
+
+ {% include '_types/post/admin/_nav_entries.html' %}
+
+ {% endif %}
+
+ {# Admin link #}
+ {% if post and has_access('blog.post.admin.admin') %}
+ {% call links.link(url_for('blog.post.admin.admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
+
+ {% endcall %}
+ {% endif %}
diff --git a/templates/_types/post/admin/_nav_entries.html b/templates/_types/post/admin/_nav_entries.html
new file mode 100644
index 0000000..47290d4
--- /dev/null
+++ b/templates/_types/post/admin/_nav_entries.html
@@ -0,0 +1,50 @@
+
+ {# Left scroll arrow - desktop only #}
+
+
+
+
+ {# Widget-driven nav items container #}
+
+
+
+
+ {# Right scroll arrow - desktop only #}
+
+
+
diff --git a/templates/_types/post/header/_header.html b/templates/_types/post/header/_header.html
new file mode 100644
index 0000000..143e79d
--- /dev/null
+++ b/templates/_types/post/header/_header.html
@@ -0,0 +1,28 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='post-row', oob=oob) %}
+ {% call links.link(url_for('blog.post.post_detail', slug=post.slug), hx_select_search ) %}
+ {% if post.feature_image %}
+
+ {% endif %}
+
+ {{ post.title | truncate(160, True, '…') }}
+
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% if page_cart_count is defined and page_cart_count > 0 %}
+
+
+ {{ page_cart_count }}
+
+ {% endif %}
+ {% include '_types/post/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/product/_main_panel.html b/templates/_types/product/_main_panel.html
new file mode 100644
index 0000000..cf8df31
--- /dev/null
+++ b/templates/_types/product/_main_panel.html
@@ -0,0 +1,131 @@
+{# Main panel fragment for HTMX navigation - product detail content #}
+{% import 'macros/stickers.html' as stick %}
+{% import '_types/product/prices.html' as prices %}
+{% set prices_ns = namespace() %}
+{{ prices.set_prices(d, prices_ns)}}
+
+ {# Product detail grid from content block #}
+
+
+ {% if d.images and d.images|length > 0 %}
+
+ {# --- like button overlay in top-right --- #}
+ {% if g.user %}
+
+ {% set slug = d.slug %}
+ {% set liked = liked_by_current_user %}
+ {% include "_types/browse/like/button.html" %}
+
+ {% endif %}
+
+
+
+
+
+ {% for l in d.labels %}
+
+ {% endfor %}
+
+
+ {{ d.brand }}
+
+
+
+ {% if d.images|length > 1 %}
+
‹
+
›
+ {% endif %}
+
+
+
+ {% else %}
+
+ {# Even if no image, still render the like button in the corner for consistency #}
+ {% if g.user %}
+
+ {% set slug = d.slug %}
+ {% set liked = liked_by_current_user %}
+ {% include "_types/browse/like/button.html" %}
+
+ {% endif %}
+
+ No image
+
+ {% endif %}
+
+
+ {% for s in d.stickers %}
+ {{ stick.sticker(asset_url('stickers/' + s + '.svg'), s, True, size=40) }}
+ {% endfor %}
+
+
+
+
+ {# Optional extras shown quietly #}
+
+ {% if d.price_per_unit or d.price_per_unit_raw %}
+
Unit price: {{ prices.price_str(d.price_per_unit, d.price_per_unit_raw, d.price_per_unit_currency) }}
+ {% endif %}
+ {% if d.case_size_raw %}
+
Case size: {{ d.case_size_raw }}
+ {% endif %}
+
+
+
+ {% if d.description_short or d.description_html %}
+
+ {% if d.description_short %}
+
{{ d.description_short }}
+ {% endif %}
+ {% if d.description_html %}
+
+ {{ d.description_html | safe }}
+
+ {% endif %}
+
+ {% endif %}
+
+ {% if d.sections and d.sections|length %}
+
+ {% for sec in d.sections %}
+
+
+ {{ sec.title }}
+ ⌄
+
+
+ {{ sec.html | safe }}
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
diff --git a/templates/_types/product/_meta.html b/templates/_types/product/_meta.html
new file mode 100644
index 0000000..aebb684
--- /dev/null
+++ b/templates/_types/product/_meta.html
@@ -0,0 +1,106 @@
+{# --- social/meta_product.html --- #}
+{# Context expected:
+ site, d (Product), request
+#}
+
+{# Visibility → robots: index unless soft-deleted #}
+{% set robots_here = 'noindex,nofollow' if d.deleted_at else 'index,follow' %}
+
+{# Compute canonical #}
+{% set _site_url = site().url.rstrip('/') if site and site().url else '' %}
+{% set _product_path = request.path if request else ('/products/' ~ (d.slug or '')) %}
+{% set canonical = _site_url ~ _product_path if _site_url else (request.url if request else None) %}
+
+{# Include common base (charset, viewport, robots default, RSS, Org/WebSite JSON-LD) #}
+{% set robots_override = robots_here %}
+{% include 'social/meta_base.html' %}
+
+{# ---- Titles / descriptions ---- #}
+{% set base_product_title = d.title or base_title %}
+{% set og_title = base_product_title %}
+{% set tw_title = base_product_title %}
+
+{# Description: prefer short, then HTML stripped #}
+{% set desc_source = d.description_short
+ or (d.description_html|striptags if d.description_html else '') %}
+{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %}
+
+{# ---- Image priority: product image, then first gallery image, then site default ---- #}
+{% set image_url = d.image
+ or ((d.images|first).url if d.images and (d.images|first).url else None)
+ or (site().default_image if site and site().default_image else None) %}
+
+{# ---- Price / offer helpers ---- #}
+{% set price = d.special_price or d.regular_price or d.rrp %}
+{% set price_currency = d.special_price_currency or d.regular_price_currency or d.rrp_currency %}
+
+{# ---- Basic meta ---- #}
+{{ base_product_title }}
+
+{% if canonical %} {% endif %}
+
+{# ---- Open Graph ---- #}
+
+
+
+
+{% if canonical %} {% endif %}
+{% if image_url %} {% endif %}
+
+{# Optional product OG price tags #}
+{% if price and price_currency %}
+
+
+{% endif %}
+{% if d.brand %}
+
+{% endif %}
+{% if d.sku %}
+
+{% endif %}
+
+{# ---- Twitter ---- #}
+
+{% if site and site().twitter_site %} {% endif %}
+
+
+{% if image_url %} {% endif %}
+
+{# ---- JSON-LD Product ---- #}
+{% set jsonld = {
+ "@context": "https://schema.org",
+ "@type": "Product",
+ "name": d.title,
+ "image": image_url,
+ "description": description,
+ "sku": d.sku,
+ "brand": d.brand,
+ "url": canonical
+} %}
+
+{# Brand as proper object if present #}
+{% if d.brand %}
+ {% set jsonld = jsonld | combine({
+ "brand": {
+ "@type": "Brand",
+ "name": d.brand
+ }
+ }) %}
+{% endif %}
+
+{# Offers if price available #}
+{% if price and price_currency %}
+ {% set jsonld = jsonld | combine({
+ "offers": {
+ "@type": "Offer",
+ "price": price,
+ "priceCurrency": price_currency,
+ "url": canonical,
+ "availability": "https://schema.org/InStock"
+ }
+ }) %}
+{% endif %}
+
+
diff --git a/templates/_types/product/_oob_elements.html b/templates/_types/product/_oob_elements.html
new file mode 100644
index 0000000..589d369
--- /dev/null
+++ b/templates/_types/product/_oob_elements.html
@@ -0,0 +1,49 @@
+{% extends 'oob_elements.html' %}
+{# OOB elements for HTMX navigation - product extends browse so use similar structure #}
+{% import 'macros/layout.html' as layout %}
+{% import 'macros/stickers.html' as stick %}
+{% import '_types/product/prices.html' as prices %}
+{% set prices_ns = namespace() %}
+{{ prices.set_prices(d, prices_ns)}}
+
+{# Import shared OOB macros #}
+{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
+{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
+
+
+
+{% block oobs %}
+ {% from '_types/market/header/_header.html' import header_row with context %}
+ {{ header_row(oob=True) }}
+
+ {% from '_types/root/_n/macros.html' import oob_header with context %}
+ {{oob_header('market-header-child', 'product-header-child', '_types/product/header/_header.html')}}
+
+{% endblock %}
+
+
+
+{% block mobile_menu %}
+ {% include '_types/market/mobile/_nav_panel.html' %}
+ {% include '_types/browse/_admin.html' %}
+{% endblock %}
+
+{% block filter %}
+ {% call layout.details() %}
+ {% call layout.summary('blog-child-header') %}
+ {% endcall %}
+ {% call layout.menu('blog-child-menu') %}
+ {% endcall %}
+ {% endcall %}
+
+ {% call layout.details() %}
+ {% call layout.summary('product-child-header') %}
+ {% endcall %}
+ {% call layout.menu('item-child-menu') %}
+ {% endcall %}
+ {% endcall %}
+{% endblock %}
+
+{% block content %}
+ {% include '_types/product/_main_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/product/_prices.html b/templates/_types/product/_prices.html
new file mode 100644
index 0000000..e56339f
--- /dev/null
+++ b/templates/_types/product/_prices.html
@@ -0,0 +1,33 @@
+{% import '_types/product/_cart.html' as _cart %}
+ {# ---- Price block ---- #}
+ {% import '_types/product/prices.html' as prices %}
+ {% set prices_ns = namespace() %}
+ {{ prices.set_prices(d, prices_ns)}}
+
+
+ {{ _cart.add(d.slug, cart)}}
+
+ {% if prices_ns.sp_val %}
+
+ Special price
+
+
+ {{ prices.price_str(prices_ns.sp_val, prices_ns.sp_raw, prices_ns.sp_cur) }}
+
+ {% if prices_ns.sp_val and prices_ns.rp_val %}
+
+ {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }}
+
+ {% endif %}
+ {% elif prices_ns.rp_val %}
+
+ Our price
+
+
+ {{ prices.price_str(prices_ns.rp_val, prices_ns.rp_raw, prices_ns.rp_cur) }}
+
+ {% endif %}
+ {{ prices.rrp(prices_ns) }}
+
+
+
diff --git a/templates/_types/product/_title.html b/templates/_types/product/_title.html
new file mode 100644
index 0000000..0b3be43
--- /dev/null
+++ b/templates/_types/product/_title.html
@@ -0,0 +1,2 @@
+
+{{ d.title }}
diff --git a/templates/_types/product/admin/_nav.html b/templates/_types/product/admin/_nav.html
new file mode 100644
index 0000000..f5c504d
--- /dev/null
+++ b/templates/_types/product/admin/_nav.html
@@ -0,0 +1,2 @@
+{% from 'macros/admin_nav.html' import placeholder_nav %}
+{{ placeholder_nav() }}
diff --git a/templates/_types/product/admin/_oob_elements.html b/templates/_types/product/admin/_oob_elements.html
new file mode 100644
index 0000000..84acac6
--- /dev/null
+++ b/templates/_types/product/admin/_oob_elements.html
@@ -0,0 +1,40 @@
+{% extends 'oob_elements.html' %}
+
+
+{# OOB elements for HTMX navigation - all elements that need updating #}
+{# Import shared OOB macros #}
+{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
+{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
+
+
+
+{% block oobs %}
+
+ {% from '_types/root/_n/macros.html' import oob_header with context %}
+ {{oob_header('product-header-child', 'product-admin-header-child', '_types/product/admin/header/_header.html')}}
+
+ {% from '_types/product/header/_header.html' import header_row with context %}
+ {{ header_row(oob=True) }}
+{% endblock %}
+
+
+{% from '_types/root/_n/macros.html' import header with context %}
+{% call header(id='product-header-child', oob=True) %}
+ {% call header() %}
+ {% from '_types/product/admin/header/_header.html' import header_row with context %}
+ {{header_row()}}
+
+ {% endcall %}
+{% endcall %}
+
+
+{% block mobile_menu %}
+ {% include '_types/product/admin/_nav.html' %}
+{% endblock %}
+
+
+{% block content %}
+ {% include '_types/product/_main_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/product/admin/header/_header.html b/templates/_types/product/admin/header/_header.html
new file mode 100644
index 0000000..eacdf7d
--- /dev/null
+++ b/templates/_types/product/admin/header/_header.html
@@ -0,0 +1,11 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='product-admin-row', oob=oob) %}
+ {% call links.link(url_for('market.browse.product.admin', product_slug=d.slug), hx_select_search ) %}
+ admin!!
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/product/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/templates/_types/product/admin/index.html b/templates/_types/product/admin/index.html
new file mode 100644
index 0000000..d1cb714
--- /dev/null
+++ b/templates/_types/product/admin/index.html
@@ -0,0 +1,39 @@
+{% extends '_types/product/index.html' %}
+
+{% import 'macros/layout.html' as layout %}
+
+{% block product_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('market-header-child', '_types/product/admin/header/_header.html') %}
+ {% block product_admin_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+
+
+{% block ___app_title %}
+ {% import 'macros/links.html' as links %}
+ {% call links.menu_row() %}
+ {% call links.link(url_for('market.browse.product.admin', product_slug=slug), hx_select_search) %}
+ {{ links.admin() }}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/product/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endblock %}
+
+
+
+{% block _main_mobile_menu %}
+ {% include '_types/product/admin/_nav.html' %}
+{% endblock %}
+
+{% block aside %}
+{% endblock %}
+
+
+{% block content %}
+{% include '_types/product/_main_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/product/header/_header.html b/templates/_types/product/header/_header.html
new file mode 100644
index 0000000..6608fce
--- /dev/null
+++ b/templates/_types/product/header/_header.html
@@ -0,0 +1,15 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='product-row', oob=oob) %}
+ {% call links.link(url_for('market.browse.product.product_detail', product_slug=d.slug), hx_select_search ) %}
+ {% include '_types/product/_title.html' %}
+ {% endcall %}
+ {% include '_types/product/_prices.html' %}
+ {% call links.desktop_nav() %}
+ {% include '_types/browse/_admin.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
+
+
+
diff --git a/templates/_types/product/index.html b/templates/_types/product/index.html
new file mode 100644
index 0000000..31ccd88
--- /dev/null
+++ b/templates/_types/product/index.html
@@ -0,0 +1,61 @@
+{% extends '_types/browse/index.html' %}
+
+{% block meta %}
+ {% include '_types/product/_meta.html' %}
+{% endblock %}
+
+
+{% import 'macros/stickers.html' as stick %}
+{% import '_types/product/prices.html' as prices %}
+{% set prices_ns = namespace() %}
+{{ prices.set_prices(d, prices_ns)}}
+
+
+
+{% block market_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('market-header-child', '_types/product/header/_header.html') %}
+ {% block product_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+
+{% block _main_mobile_menu %}
+ {% include '_types/browse/_admin.html' %}
+{% endblock %}
+
+
+
+{% block filter %}
+
+{% call layout.details() %}
+ {% call layout.summary('blog-child-header') %}
+ {% block blog_child_summary %}
+ {% endblock %}
+ {% endcall %}
+ {% call layout.menu('blog-child-menu') %}
+ {% block post_child_menu %}
+ {% endblock %}
+ {% endcall %}
+ {% endcall %}
+
+ {% call layout.details() %}
+ {% call layout.summary('product-child-header') %}
+ {% block item_child_summary %}
+ {% endblock %}
+ {% endcall %}
+ {% call layout.menu('item-child-menu') %}
+ {% block item_child_menu %}
+ {% endblock %}
+ {% endcall %}
+ {% endcall %}
+
+{% endblock %}
+
+{% block aside %}
+{% endblock %}
+
+{% block content %}
+ {% include '_types/product/_main_panel.html' %}
+{% endblock %}
diff --git a/templates/_types/product/prices.html b/templates/_types/product/prices.html
new file mode 100644
index 0000000..be9cc4c
--- /dev/null
+++ b/templates/_types/product/prices.html
@@ -0,0 +1,66 @@
+{# ---- Price formatting helpers ---- #}
+{% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %}
+{% macro price_str(val, raw, cur) -%}
+ {%- if raw -%}
+ {{ raw }}
+ {%- elif val is number -%}
+ {{ (_sym.get(cur) or '') ~ ('%.2f'|format(val)) }}
+ {%- else -%}
+ {{ val or '' }}
+ {%- endif -%}
+{%- endmacro %}
+
+
+{% macro set_prices(item, ns) -%}
+
+{% set ns.sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %}
+{% set ns.sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %}
+{% set ns.sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %}
+
+{% set ns.rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %}
+{% set ns.rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %}
+{% set ns.rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %}
+
+{% set ns.case_size_count = (item.case_size_count or 1) %}
+{% set ns.rrp = item.rrp_raw[0] ~ "%.2f"|format(item.rrp * (ns.case_size_count)) %}
+{% set ns.rrp_raw = item.rrp_raw %}
+
+{%- endmacro %}
+
+
+{% macro rrp(ns) -%}
+ {% if ns.rrp %}
+
+ rrp:
+
+ {{ ns.rrp }}
+
+
+ {% endif %}
+{%- endmacro %}
+
+
+{% macro card_price(item) %}
+
+
+{# price block unchanged #}
+ {% set _sym = {'GBP':'£','EUR':'€','USD':'$'} %}
+ {% set sp_val = item.special_price or (item.oe_list_price and item.oe_list_price.special) %}
+ {% set sp_raw = item.special_price_raw or (item.oe_list_price and item.oe_list_price.special_raw) %}
+ {% set sp_cur = item.special_price_currency or (item.oe_list_price and item.oe_list_price.special_currency) %}
+ {% set rp_val = item.regular_price or item.rrp or (item.oe_list_price and item.oe_list_price.rrp) %}
+ {% set rp_raw = item.regular_price_raw or item.rrp_raw or (item.oe_list_price and item.oe_list_price.rrp_raw) %}
+ {% set rp_cur = item.regular_price_currency or item.rrp_currency or (item.oe_list_price and item.oe_list_price.rrp_currency) %}
+ {% set sp_str = sp_raw if sp_raw else ( (_sym.get(sp_cur, '') ~ ('%.2f'|format(sp_val))) if sp_val is number else (sp_val or '')) %}
+ {% set rp_str = rp_raw if rp_raw else ( (_sym.get(rp_cur, '') ~ ('%.2f'|format(rp_val))) if rp_val is number else (rp_val or '')) %}
+
+ {% if sp_val %}
+
{{ sp_str }}
+ {% if rp_val %}
+
{{ rp_str }}
+ {% endif %}
+ {% elif rp_val %}
+
{{ rp_str }}
+ {% endif %}
+
+{% endmacro %}
diff --git a/templates/aside_clear.html b/templates/aside_clear.html
new file mode 100644
index 0000000..e091ac2
--- /dev/null
+++ b/templates/aside_clear.html
@@ -0,0 +1,7 @@
+
+
diff --git a/templates/filter_clear.html b/templates/filter_clear.html
new file mode 100644
index 0000000..fc3901e
--- /dev/null
+++ b/templates/filter_clear.html
@@ -0,0 +1,5 @@
+
+
diff --git a/templates/macros/filters.html b/templates/macros/filters.html
new file mode 100644
index 0000000..8d13887
--- /dev/null
+++ b/templates/macros/filters.html
@@ -0,0 +1,117 @@
+{#
+ Unified filter macros for browse/shop pages
+ Consolidates duplicate mobile/desktop filter components
+#}
+
+{% macro filter_item(href, is_on, title, icon_html, count=none, variant='desktop') %}
+ {#
+ Generic filter item (works for labels, stickers, etc.)
+ variant: 'desktop' or 'mobile'
+ #}
+ {% set base_class = "flex flex-col items-center justify-center" %}
+ {% if variant == 'mobile' %}
+ {% set item_class = base_class ~ " p-1 rounded hover:bg-stone-50" %}
+ {% set count_class = "text-[10px] text-stone-500 mt-1 leading-none tabular-nums" if count != 0 else "text-md text-red-500 font-bold mt-1 leading-none tabular-nums" %}
+ {% else %}
+ {% set item_class = base_class ~ " py-2 w-full h-full" %}
+ {% set count_class = "text-xs text-stone-500 leading-none justify-self-end tabular-nums" if count != 0 else "text-md text-red-500 font-bold leading-none justify-self-end tabular-nums" %}
+ {% endif %}
+
+
+ {{ icon_html | safe }}
+ {% if count is not none %}
+ {{ count }}
+ {% endif %}
+
+{% endmacro %}
+
+
+{% macro labels_list(labels, selected_labels, current_local_href, variant='desktop') %}
+ {#
+ Unified labels filter list
+ variant: 'desktop' or 'mobile'
+ #}
+ {% import 'macros/stickers.html' as stick %}
+
+ {% if variant == 'mobile' %}
+
+
+ {% endif %}
+{% endmacro %}
+
+
+{% macro stickers_list(stickers, selected_stickers, current_local_href, variant='desktop') %}
+ {#
+ Unified stickers filter list
+ variant: 'desktop' or 'mobile'
+ #}
+ {% import 'macros/stickers.html' as stick %}
+
+ {% if variant == 'mobile' %}
+
+
+
+ {% endif %}
+{% endmacro %}
+
+