diff --git a/browser/templates/_types/blog/_action_buttons.html b/browser/templates/_types/blog/_action_buttons.html
new file mode 100644
index 0000000..0ea7fa2
--- /dev/null
+++ b/browser/templates/_types/blog/_action_buttons.html
@@ -0,0 +1,51 @@
+{# New Post + Drafts toggle — shown in aside (desktop + mobile) #}
+
+ {% if has_access('blog.new_post') %}
+ {% set new_href = url_for('blog.new_post')|host %}
+
+ New Post
+
+ {% endif %}
+ {% if g.user and (draft_count or drafts) %}
+ {% if drafts %}
+ {% set drafts_off_href = (current_local_href ~ {'drafts': None}|qs)|host %}
+
+ Drafts
+ {{ draft_count }}
+
+ {% else %}
+ {% set drafts_on_href = (current_local_href ~ {'drafts': '1'}|qs)|host %}
+
+ Drafts
+ {{ draft_count }}
+
+ {% endif %}
+ {% endif %}
+
diff --git a/browser/templates/_types/blog/_main_panel.html b/browser/templates/_types/blog/_main_panel.html
new file mode 100644
index 0000000..350999d
--- /dev/null
+++ b/browser/templates/_types/blog/_main_panel.html
@@ -0,0 +1,48 @@
+
+ {# View toggle bar - desktop only #}
+
+
+
+ {# Calendar grid #}
+
+ {# Weekday header: only show on sm+ (desktop/tablet) #}
+
+ {% for wd in weekday_names %}
+
{{ wd }}
+ {% endfor %}
+
+
+ {# On mobile: 1 column; on sm+: 7 columns #}
+
+ {% for week in weeks %}
+ {% for day in week %}
+
+
+
+
+ {{ day.date.strftime('%a') }}
+
+
+ {# Clickable day number: goes to day detail view #}
+
+ {{ day.date.day }}
+
+
+
+ {# Entries for this day: merged, chronological #}
+
+ {# Build a list of entries for this specific day.
+ month_entries is already sorted by start_at in Python. #}
+ {% for e in month_entries %}
+ {% if e.start_at.date() == day.date %}
+ {# Decide colour: highlight "mine" differently if you want #}
+ {% set is_mine = (g.user and e.user_id == g.user.id)
+ or (not g.user and e.session_id == qsession.get('calendar_sid')) %}
+
+
+ {{ e.name }}
+
+
+ {{ (e.state or 'pending')|replace('_', ' ') }}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {% endfor %}
+ {% endfor %}
+
+
diff --git a/browser/templates/_types/calendar/admin/_description.html b/browser/templates/_types/calendar/admin/_description.html
new file mode 100644
index 0000000..7fc96f0
--- /dev/null
+++ b/browser/templates/_types/calendar/admin/_description.html
@@ -0,0 +1,33 @@
+
+ {% if calendar.description %}
+
+ {{ calendar.description }}
+
+ {% else %}
+
+ No description yet.
+
+ {% endif %}
+
+
+
+
+
+
+{% if oob %}
+
+ {% from '_types/calendar/_description.html' import description %}
+ {{description(calendar, oob=True)}}
+{% endif %}
+
+
diff --git a/browser/templates/_types/calendar/admin/_description_edit.html b/browser/templates/_types/calendar/admin/_description_edit.html
new file mode 100644
index 0000000..61c5ff0
--- /dev/null
+++ b/browser/templates/_types/calendar/admin/_description_edit.html
@@ -0,0 +1,43 @@
+
diff --git a/browser/templates/_types/calendar/admin/_main_panel.html b/browser/templates/_types/calendar/admin/_main_panel.html
new file mode 100644
index 0000000..9696f47
--- /dev/null
+++ b/browser/templates/_types/calendar/admin/_main_panel.html
@@ -0,0 +1,46 @@
+
+
+
+
+
Calendar configuration
+
+
+
+ Description
+
+ {% include '_types/calendar/admin/_description.html' %}
+
+
+
+
+
+
Description
+
{{calendar.description or ''}}
+
{{ (calendar.description or '') }}
+
+
+
+ Save
+
+
+
+
+
+
+
diff --git a/browser/templates/_types/calendar/admin/header/_header.html b/browser/templates/_types/calendar/admin/header/_header.html
new file mode 100644
index 0000000..d383373
--- /dev/null
+++ b/browser/templates/_types/calendar/admin/header/_header.html
@@ -0,0 +1,14 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='calendar-admin-row', oob=oob) %}
+ {% call links.link(
+ url_for('calendars.calendar.admin.admin', slug=post.slug, calendar_slug=calendar.slug),
+ hx_select_search
+ ) %}
+ {{ links.admin() }}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/calendar/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/browser/templates/_types/calendar/header/_header.html b/browser/templates/_types/calendar/header/_header.html
new file mode 100644
index 0000000..73fe115
--- /dev/null
+++ b/browser/templates/_types/calendar/header/_header.html
@@ -0,0 +1,23 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='calendar-row', oob=oob) %}
+
+
+
+
+
+ {{ calendar.name }}
+
+
+ {% from '_types/calendar/_description.html' import description %}
+ {{description(calendar)}}
+
+
+ {% call links.desktop_nav() %}
+ {% include '_types/calendar/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
+
+
+
diff --git a/browser/templates/_types/calendar/index.html b/browser/templates/_types/calendar/index.html
new file mode 100644
index 0000000..802c45c
--- /dev/null
+++ b/browser/templates/_types/calendar/index.html
@@ -0,0 +1,20 @@
+{% extends '_types/post/index.html' %}
+
+{% block post_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('calendar-header-child', '_types/calendar/header/_header.html') %}
+ {% block calendar_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+
+{% block _main_mobile_menu %}
+ {% include '_types/calendar/_nav.html' %}
+{% endblock %}
+
+
+
+{% block content %}
+ {% include '_types/calendar/_main_panel.html' %}
+{% endblock %}
diff --git a/browser/templates/_types/day/_add.html b/browser/templates/_types/day/_add.html
new file mode 100644
index 0000000..df02331
--- /dev/null
+++ b/browser/templates/_types/day/_add.html
@@ -0,0 +1,301 @@
+
+
+
+
+
+ {# 1) Entry name #}
+
+
+ {# 2) Slot picker for this weekday (required) #}
+ {% if day_slots %}
+
+ {% for slot in day_slots %}
+
+ {{ slot.name }}
+ ({{ slot.time_start.strftime('%H:%M') }}
+ {% if slot.time_end %}–{{ slot.time_end.strftime('%H:%M') }}{% else %}–open-ended{% endif %})
+ {% if slot | getattr('flexible', False) %}[flexible]{% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
+ No slots defined for this day.
+
+ {% endif %}
+
+ {# 3) Time entry + cost display #}
+
+ {# Time inputs — hidden until a flexible slot is selected #}
+
+
+ From
+
+
+
+ To
+
+
+
+
+
+ {# Cost display — shown when a slot is selected #}
+
+ Estimated Cost: £0.00
+
+
+ {# Summary of fixed times — shown for non-flexible slots #}
+
+
+
+ {# Ticket Configuration #}
+
+
Ticket Configuration (Optional)
+
+
+
+
+
+ Cancel
+
+
+
+
+ Save entry
+
+
+
+
+{# --- Behaviour: lock / unlock times based on slot.flexible --- #}
+
\ No newline at end of file
diff --git a/browser/templates/_types/day/_add_button.html b/browser/templates/_types/day/_add_button.html
new file mode 100644
index 0000000..46726e5
--- /dev/null
+++ b/browser/templates/_types/day/_add_button.html
@@ -0,0 +1,17 @@
+
+
+ + Add entry
+
diff --git a/browser/templates/_types/day/_nav.html b/browser/templates/_types/day/_nav.html
new file mode 100644
index 0000000..cfb8aca
--- /dev/null
+++ b/browser/templates/_types/day/_nav.html
@@ -0,0 +1,50 @@
+{% import 'macros/links.html' as links %}
+
+{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #}
+
+
+{# Container nav widgets (market links, etc.) #}
+{% if container_nav_widgets %}
+ {% for wdata in container_nav_widgets %}
+ {% with ctx=wdata.ctx %}
+ {% include wdata.widget.template with context %}
+ {% endwith %}
+ {% endfor %}
+{% endif %}
+
+{# Admin link #}
+{% if g.rights.admin %}
+ {% from 'macros/admin_nav.html' import admin_nav_item %}
+ {{admin_nav_item(
+ url_for(
+ 'calendars.calendar.day.admin.admin',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ year=day_date.year,
+ month=day_date.month,
+ day=day_date.day
+ )
+ )}}
+{% endif %}
\ No newline at end of file
diff --git a/browser/templates/_types/day/_row.html b/browser/templates/_types/day/_row.html
new file mode 100644
index 0000000..87aead7
--- /dev/null
+++ b/browser/templates/_types/day/_row.html
@@ -0,0 +1,76 @@
+{% import 'macros/links.html' as links %}
+
+
+
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.day.calendar_entries.calendar_entry.get',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ day=day,
+ month=month,
+ year=year,
+ entry_id=entry.id
+ ),
+ hx_select_search,
+ aclass=styles.pill
+ ) %}
+ {{ entry.name }}
+ {% endcall %}
+
+
+
+ {% if entry.slot %}
+
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.slots.slot.get',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ slot_id=entry.slot.id
+ ),
+ hx_select_search,
+ aclass=styles.pill
+ ) %}
+ {{ entry.slot.name }}
+ {% endcall %}
+
+ ({{ entry.slot.time_start.strftime('%H:%M') }}{% if entry.slot.time_end %} → {{ entry.slot.time_end.strftime('%H:%M') }}{% endif %})
+
+
+ {% else %}
+
+ {% include '_types/entry/_times.html' %}
+
+ {% endif %}
+
+
+
+ {% include '_types/entry/_state.html' %}
+
+
+
+
+ £{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}
+
+
+
+ {% if entry.ticket_price is not none %}
+
+
£{{ ('%.2f'|format(entry.ticket_price)) }}
+
+ {% if entry.ticket_count is not none %}
+ {{ entry.ticket_count }} tickets
+ {% else %}
+ Unlimited
+ {% endif %}
+
+
+ {% else %}
+ No tickets
+ {% endif %}
+
+
+ {% include '_types/entry/_options.html' %}
+
+
\ No newline at end of file
diff --git a/browser/templates/_types/day/admin/_nav_entries_oob.html b/browser/templates/_types/day/admin/_nav_entries_oob.html
new file mode 100644
index 0000000..d72fc90
--- /dev/null
+++ b/browser/templates/_types/day/admin/_nav_entries_oob.html
@@ -0,0 +1,34 @@
+{# OOB swap for day confirmed entries nav when entries are edited #}
+{% import 'macros/links.html' as links %}
+
+{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #}
+{% if confirmed_entries %}
+
+{% else %}
+ {# Empty placeholder to remove nav entries when none are confirmed #}
+
+{% endif %}
diff --git a/browser/templates/_types/day/admin/header/_header.html b/browser/templates/_types/day/admin/header/_header.html
new file mode 100644
index 0000000..b5f583c
--- /dev/null
+++ b/browser/templates/_types/day/admin/header/_header.html
@@ -0,0 +1,21 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='day-admin-row', oob=oob) %}
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.day.admin.admin',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ year=day_date.year,
+ month=day_date.month,
+ day=day_date.day
+ ),
+ hx_select_search
+ ) %}
+ {{ links.admin() }}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/day/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/browser/templates/_types/day/header/_header.html b/browser/templates/_types/day/header/_header.html
new file mode 100644
index 0000000..e53a815
--- /dev/null
+++ b/browser/templates/_types/day/header/_header.html
@@ -0,0 +1,27 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='day-row', oob=oob) %}
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.day.show_day',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ year=day_date.year,
+ month=day_date.month,
+ day=day_date.day
+ ),
+ hx_select_search,
+ ) %}
+
+
+ {{ day_date.strftime('%A %d %B %Y') }}
+
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/day/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
+
+
+
diff --git a/browser/templates/_types/day/index.html b/browser/templates/_types/day/index.html
new file mode 100644
index 0000000..655ee55
--- /dev/null
+++ b/browser/templates/_types/day/index.html
@@ -0,0 +1,18 @@
+{% extends '_types/calendar/index.html' %}
+
+{% block calendar_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('day-header-child', '_types/day/header/_header.html') %}
+ {% block day_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+{% block _main_mobile_menu %}
+ {% include '_types/day/_nav.html' %}
+{% endblock %}
+
+
+{% block content %}
+ {% include '_types/day/_main_panel.html' %}
+{% endblock %}
diff --git a/browser/templates/_types/entry/_edit.html b/browser/templates/_types/entry/_edit.html
new file mode 100644
index 0000000..628f239
--- /dev/null
+++ b/browser/templates/_types/entry/_edit.html
@@ -0,0 +1,334 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+ Slot
+
+ {% if day_slots %}
+
+ {% for slot in day_slots %}
+
+ {{ slot.name }}
+ ({{ slot.time_start.strftime('%H:%M') }}
+ {% if slot.time_end %}–{{ slot.time_end.strftime('%H:%M') }}{% else %}–open-ended{% endif %})
+ {% if slot.flexible %}[flexible]{% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
+ No slots defined for this day.
+
+ {% endif %}
+
+
+
+
+
+
+ From
+
+
+
+
+
+
+ To
+
+
+
+
+
+
+
+
+
+
+
+
+ Estimated Cost: £{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}
+
+
+
+
+
Ticket Configuration
+
+
+
+
+ Ticket Price (£)
+
+
+
Leave empty if no tickets needed
+
+
+
+
+ Total Tickets
+
+
+
Leave empty for unlimited
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+ Save entry
+
+
+
+
+
+
+
+{# --- Behaviour: lock / unlock times based on slot.flexible --- #}
+
\ No newline at end of file
diff --git a/browser/templates/_types/entry/_main_panel.html b/browser/templates/_types/entry/_main_panel.html
new file mode 100644
index 0000000..403605b
--- /dev/null
+++ b/browser/templates/_types/entry/_main_panel.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+ Name
+
+
+ {{ entry.name }}
+
+
+
+
+
+
+ Slot
+
+
+ {% if entry.slot %}
+
+ {{ entry.slot.name }}
+
+ {% if entry.slot.flexible %}
+ (flexible)
+ {% else %}
+ (fixed)
+ {% endif %}
+ {% else %}
+ No slot assigned
+ {% endif %}
+
+
+
+
+
+
+ Time Period
+
+
+ {{ entry.start_at.strftime('%H:%M') }}
+ {% if entry.end_at %}
+ – {{ entry.end_at.strftime('%H:%M') }}
+ {% else %}
+ – open-ended
+ {% endif %}
+
+
+
+
+
+
+ State
+
+
+
+ {% include '_types/entry/_state.html' %}
+
+
+
+
+
+
+
+ Cost
+
+
+
+ £{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}
+
+
+
+
+
+
+
+ Tickets
+
+
+ {% include '_types/entry/_tickets.html' %}
+
+
+
+
+
+
+ Date
+
+
+ {{ entry.start_at.strftime('%A, %B %d, %Y') }}
+
+
+
+
+
+
+ Associated Posts
+
+
+ {% include '_types/entry/_posts.html' %}
+
+
+
+
+
+ {% include '_types/entry/_options.html' %}
+
+
+ Edit
+
+
+
+
\ No newline at end of file
diff --git a/browser/templates/_types/entry/_nav.html b/browser/templates/_types/entry/_nav.html
new file mode 100644
index 0000000..388fc2e
--- /dev/null
+++ b/browser/templates/_types/entry/_nav.html
@@ -0,0 +1,48 @@
+{% import 'macros/links.html' as links %}
+
+{# Associated Posts - vertical on mobile, horizontal with arrows on desktop #}
+
+ {% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
+ {% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
+
+ {% if entry_post.feature_image %}
+
+ {% else %}
+
+ {% endif %}
+
+
{{ entry_post.title }}
+
+
+ {% endcall %}
+
+
+{% if container_nav_widgets %}
+ {% for wdata in container_nav_widgets %}
+ {% with ctx=wdata.ctx %}
+ {% include wdata.widget.template with context %}
+ {% endwith %}
+ {% endfor %}
+{% endif %}
+
+{# Admin link #}
+{% if g.rights.admin %}
+
+ {% from 'macros/admin_nav.html' import admin_nav_item %}
+ {{admin_nav_item(
+ url_for(
+ 'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ day=day,
+ month=month,
+ year=year,
+ entry_id=entry.id
+ )
+ )}}
+{% endif %}
diff --git a/browser/templates/_types/entry/_options.html b/browser/templates/_types/entry/_options.html
new file mode 100644
index 0000000..6c42952
--- /dev/null
+++ b/browser/templates/_types/entry/_options.html
@@ -0,0 +1,98 @@
+
+ {% if entry.state == 'provisional' %}
+
+
+
+
+ confirm
+
+
+
+
+
+
+ decline
+
+
+ {% endif %}
+ {% if entry.state == 'confirmed' %}
+
+
+
+
+
+ provisional
+
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/browser/templates/_types/entry/_posts.html b/browser/templates/_types/entry/_posts.html
new file mode 100644
index 0000000..9a88936
--- /dev/null
+++ b/browser/templates/_types/entry/_posts.html
@@ -0,0 +1,74 @@
+
+
+ {% if entry_posts %}
+
+ {% for entry_post in entry_posts %}
+
+ {% if entry_post.feature_image %}
+
+ {% else %}
+
+ {% endif %}
+
{{ entry_post.title }}
+
+ Remove
+
+
+ {% endfor %}
+
+ {% else %}
+
No posts associated
+ {% endif %}
+
+
+
+
diff --git a/browser/templates/_types/entry/_tickets.html b/browser/templates/_types/entry/_tickets.html
new file mode 100644
index 0000000..ee5dc38
--- /dev/null
+++ b/browser/templates/_types/entry/_tickets.html
@@ -0,0 +1,105 @@
+{% if entry.ticket_price is not none %}
+ {# Tickets are configured #}
+
+
+ Price:
+
+ £{{ ('%.2f'|format(entry.ticket_price)) }}
+
+
+
+ Available:
+
+ {% if entry.ticket_count is not none %}
+ {{ entry.ticket_count }} tickets
+ {% else %}
+ Unlimited
+ {% endif %}
+
+
+
+ Edit ticket config
+
+
+{% else %}
+ {# No tickets configured #}
+
+ No tickets configured
+
+ Configure tickets
+
+
+{% endif %}
+
+{# Ticket configuration form (hidden by default) #}
+
+
+
+
+ Ticket Price (£)
+
+
+
+
+
+
+ Total Tickets
+
+
+
+
+
+
+ Save
+
+
+ Cancel
+
+
+
diff --git a/browser/templates/_types/entry/admin/_nav.html b/browser/templates/_types/entry/admin/_nav.html
new file mode 100644
index 0000000..ff658d3
--- /dev/null
+++ b/browser/templates/_types/entry/admin/_nav.html
@@ -0,0 +1,18 @@
+{% import 'macros/links.html' as links %}
+{% call links.link(
+ url_for(
+ 'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.get',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ entry_id=entry.id,
+ year=year,
+ month=month,
+ day=day
+ ),
+ hx_select_search,
+ select_colours,
+ True,
+ aclass=styles.nav_button,
+)%}
+ ticket_types
+{% endcall %}
diff --git a/browser/templates/_types/entry/admin/header/_header.html b/browser/templates/_types/entry/admin/header/_header.html
new file mode 100644
index 0000000..ea06833
--- /dev/null
+++ b/browser/templates/_types/entry/admin/header/_header.html
@@ -0,0 +1,22 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='entry-admin-row', oob=oob) %}
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ day=day,
+ month=month,
+ year=year,
+ entry_id=entry.id
+ ),
+ hx_select_search
+ ) %}
+ {{ links.admin() }}
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/entry/admin/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/browser/templates/_types/entry/header/_header.html b/browser/templates/_types/entry/header/_header.html
new file mode 100644
index 0000000..6d0680c
--- /dev/null
+++ b/browser/templates/_types/entry/header/_header.html
@@ -0,0 +1,28 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='entry-row', oob=oob) %}
+ {% call links.link(
+ url_for(
+ 'calendars.calendar.day.calendar_entries.calendar_entry.get',
+ slug=post.slug,
+ calendar_slug=calendar.slug,
+ day=day,
+ month=month,
+ year=year,
+ entry_id=entry.id
+ ),
+ hx_select_search,
+ ) %}
+
+ {% include '_types/entry/_title.html' %}
+ {% include '_types/entry/_times.html' %}
+
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/entry/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
+
+
+
diff --git a/browser/templates/_types/entry/index.html b/browser/templates/_types/entry/index.html
new file mode 100644
index 0000000..a980f46
--- /dev/null
+++ b/browser/templates/_types/entry/index.html
@@ -0,0 +1,20 @@
+{% extends '_types/day/index.html' %}
+
+{% block day_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('entry-header-child', '_types/entry/header/_header.html') %}
+ {% block entry_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+
+{% block _main_mobile_menu %}
+ {% include '_types/entry/_nav.html' %}
+{% endblock %}
+
+
+
+{% block content %}
+{% include '_types/entry/_main_panel.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/browser/templates/_types/market/_oob_elements.html b/browser/templates/_types/market/_oob_elements.html
new file mode 100644
index 0000000..b37eea0
--- /dev/null
+++ b/browser/templates/_types/market/_oob_elements.html
@@ -0,0 +1,30 @@
+{% 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 %}
+
+{# 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('root-header-child', 'market-header-child', '_types/market/header/_header.html')}}
+
+ {% from '_types/root/header/_header.html' import header_row with context %}
+ {{ header_row(oob=True) }}
+{% endblock %}
+
+
+{% block mobile_menu %}
+ {% include '_types/market/mobile/_nav_panel.html' %}
+{% endblock %}
+
+
+{% block content %}
+ {% include "_types/market/_main_panel.html" %}
+{% endblock %}
+
+
diff --git a/browser/templates/_types/market/index.html b/browser/templates/_types/market/index.html
new file mode 100644
index 0000000..df1ec4c
--- /dev/null
+++ b/browser/templates/_types/market/index.html
@@ -0,0 +1,25 @@
+{% extends '_types/root/_index.html' %}
+
+
+{% block root_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('market-header-child', '_types/market/header/_header.html') %}
+ {% block market_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+
+{% block _main_mobile_menu %}
+ {% include '_types/market/mobile/_nav_panel.html' %}
+{% endblock %}
+
+
+
+{% block aside %}
+{# No aside on landing page #}
+{% endblock %}
+
+{% block content %}
+ {% include "_types/market/_main_panel.html" %}
+{% endblock %}
diff --git a/browser/templates/_types/post/_meta.html b/browser/templates/_types/post/_meta.html
new file mode 100644
index 0000000..fc752d4
--- /dev/null
+++ b/browser/templates/_types/post/_meta.html
@@ -0,0 +1,128 @@
+{# --- social/meta_post.html --- #}
+{# Context expected:
+ site, post, request
+#}
+{% if post is not defined %}
+ {% include 'social/meta_base.html' %}
+{% else %}
+
+{# Visibility → robots #}
+{% set is_public = (post.visibility == 'public') %}
+{% set is_published = (post.status == 'published') %}
+{% set robots_here = 'index,follow' if (is_public and is_published and not post.email_only) else 'noindex,nofollow' %}
+
+{# Compute canonical early so both this file and base can use it #}
+{% set _site_url = site().url.rstrip('/') if site and site().url else '' %}
+{% set _post_path = request.path if request else ('/posts/' ~ (post.slug or post.uuid)) %}
+{% set canonical = post.canonical_url or (_site_url ~ _post_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 og_title = post.og_title or base_title %}
+{% set tw_title = post.twitter_title or base_title %}
+
+{# Description best-effort, trimmed #}
+{% set desc_source = post.meta_description
+ or post.og_description
+ or post.twitter_description
+ or post.custom_excerpt
+ or post.excerpt
+ or (post.plaintext if post.plaintext else (post.html|striptags if post.html else '')) %}
+{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %}
+
+{# Image priority #}
+{% set image_url = post.og_image
+ or post.twitter_image
+ or post.feature_image
+ or (site().default_image if site and site().default_image else None) %}
+
+{# Dates #}
+{% set published_iso = post.published_at.isoformat() if post.published_at else None %}
+{% set updated_iso = post.updated_at.isoformat() if post.updated_at
+ else (post.created_at.isoformat() if post.created_at else None) %}
+
+{# Authors / tags #}
+{% set primary_author = post.primary_author %}
+{% set authors = post.authors or ([primary_author] if primary_author else []) %}
+{% set tag_names = (post.tags or []) | map(attribute='name') | list %}
+{% set is_article = not post.is_page %}
+
+{{ base_title }}
+
+{% if canonical %} {% endif %}
+
+{# ---- Open Graph ---- #}
+
+
+
+
+{% if canonical %} {% endif %}
+{% if image_url %} {% endif %}
+{% if is_article and published_iso %} {% endif %}
+{% if is_article and updated_iso %}
+
+
+{% endif %}
+{% if is_article and post.primary_tag and post.primary_tag.name %}
+
+{% endif %}
+{% if is_article %}
+ {% for t in tag_names %}
+
+ {% endfor %}
+{% endif %}
+
+{# ---- Twitter ---- #}
+
+{% if site and site().twitter_site %} {% endif %}
+{% if primary_author and primary_author.twitter %}
+
+{% endif %}
+
+
+{% if image_url %} {% endif %}
+
+{# ---- JSON-LD author value (no list comprehensions) ---- #}
+{% if authors and authors|length == 1 %}
+ {% set author_value = {"@type": "Person", "name": authors[0].name} %}
+{% elif authors %}
+ {% set ns = namespace(arr=[]) %}
+ {% for a in authors %}
+ {% set _ = ns.arr.append({"@type": "Person", "name": a.name}) %}
+ {% endfor %}
+ {% set author_value = ns.arr %}
+{% else %}
+ {% set author_value = none %}
+{% endif %}
+
+{# ---- JSON-LD using combine for optionals ---- #}
+{% set jsonld = {
+ "@context": "https://schema.org",
+ "@type": "BlogPosting" if is_article else "WebPage",
+ "mainEntityOfPage": canonical,
+ "headline": base_title,
+ "description": description,
+ "image": image_url,
+ "datePublished": published_iso,
+ "author": author_value,
+ "publisher": {
+ "@type": "Organization",
+ "name": site().title if site and site().title else "",
+ "logo": {"@type": "ImageObject", "url": site().logo if site and site().logo else image_url}
+ }
+} %}
+
+{% if updated_iso %}
+ {% set jsonld = jsonld | combine({"dateModified": updated_iso}) %}
+{% endif %}
+{% if tag_names %}
+ {% set jsonld = jsonld | combine({"keywords": tag_names | join(", ")}) %}
+{% endif %}
+
+
+{% endif %}
diff --git a/browser/templates/_types/post/_nav.html b/browser/templates/_types/post/_nav.html
new file mode 100644
index 0000000..db8cdc4
--- /dev/null
+++ b/browser/templates/_types/post/_nav.html
@@ -0,0 +1,8 @@
+{% 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 %}
diff --git a/browser/templates/_types/post/admin/_nav.html b/browser/templates/_types/post/admin/_nav.html
new file mode 100644
index 0000000..7296d15
--- /dev/null
+++ b/browser/templates/_types/post/admin/_nav.html
@@ -0,0 +1,18 @@
+{% import 'macros/links.html' as links %}
+
+{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
+ entries
+{% endcall %}
+{% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
+ data
+{% endcall %}
+{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
+ edit
+{% endcall %}
+{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
+ settings
+{% endcall %}
\ No newline at end of file
diff --git a/browser/templates/_types/post/admin/_oob_elements.html b/browser/templates/_types/post/admin/_oob_elements.html
new file mode 100644
index 0000000..4bd3b74
--- /dev/null
+++ b/browser/templates/_types/post/admin/_oob_elements.html
@@ -0,0 +1,22 @@
+{% extends "oob_elements.html" %}
+{# OOB elements for post admin page #}
+
+{# 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('post-header-child', 'post-admin-header-child', '_types/post/admin/header/_header.html')}}
+
+ {% from '_types/post/header/_header.html' import header_row with context %}
+ {{ header_row(oob=True) }}
+{% endblock %}
+
+{% block mobile_menu %}
+ {% include '_types/post/admin/_nav.html' %}
+{% endblock %}
+
+{% block content %}
+nowt
+{% endblock %}
\ No newline at end of file
diff --git a/browser/templates/_types/post/admin/index.html b/browser/templates/_types/post/admin/index.html
new file mode 100644
index 0000000..fb1de5f
--- /dev/null
+++ b/browser/templates/_types/post/admin/index.html
@@ -0,0 +1,18 @@
+{% extends '_types/post/index.html' %}
+{% import 'macros/layout.html' as layout %}
+
+{% block post_header_child %}
+ {% from '_types/root/_n/macros.html' import index_row with context %}
+ {% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
+ {% block post_admin_header_child %}
+ {% endblock %}
+ {% endcall %}
+{% endblock %}
+
+{% block _main_mobile_menu %}
+ {% include '_types/post/admin/_nav.html' %}
+{% endblock %}
+
+{% block content %}
+nowt
+{% endblock %}
diff --git a/browser/templates/_types/post/header/_header.html b/browser/templates/_types/post/header/_header.html
new file mode 100644
index 0000000..a75eda3
--- /dev/null
+++ b/browser/templates/_types/post/header/_header.html
@@ -0,0 +1,19 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='post-row', oob=oob) %}
+
+ {% if post.feature_image %}
+
+ {% endif %}
+
+ {{ post.title | truncate(160, True, '…') }}
+
+
+ {% call links.desktop_nav() %}
+ {% include '_types/post/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
\ No newline at end of file
diff --git a/browser/templates/_types/post_entries/_main_panel.html b/browser/templates/_types/post_entries/_main_panel.html
new file mode 100644
index 0000000..342041e
--- /dev/null
+++ b/browser/templates/_types/post_entries/_main_panel.html
@@ -0,0 +1,48 @@
+
+
+ {# Associated Entries List #}
+ {% include '_types/post/admin/_associated_entries.html' %}
+
+ {# Calendars Browser #}
+
+
Browse Calendars
+ {% for calendar in all_calendars %}
+
+
+ {% if calendar.post.feature_image %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+ {{ calendar.name }}
+
+
+ {{ calendar.post.title }}
+
+
+
+
+
+ {% else %}
+
No calendars found.
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/browser/templates/_types/post_entries/header/_header.html b/browser/templates/_types/post_entries/header/_header.html
new file mode 100644
index 0000000..019c000
--- /dev/null
+++ b/browser/templates/_types/post_entries/header/_header.html
@@ -0,0 +1,17 @@
+{% import 'macros/links.html' as links %}
+{% macro header_row(oob=False) %}
+ {% call links.menu_row(id='post_entries-row', oob=oob) %}
+ {% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search) %}
+
+
+ entries
+
+ {% endcall %}
+ {% call links.desktop_nav() %}
+ {% include '_types/post_entries/_nav.html' %}
+ {% endcall %}
+ {% endcall %}
+{% endmacro %}
+
+
+
diff --git a/browser/templates/_types/product/_cart.html b/browser/templates/_types/product/_cart.html
new file mode 100644
index 0000000..4947d86
--- /dev/null
+++ b/browser/templates/_types/product/_cart.html
@@ -0,0 +1,278 @@
+{% macro add(slug, cart, oob='false') %}
+{% set quantity = cart
+ | selectattr('product.slug', 'equalto', slug)
+ | sum(attribute='quantity') %}
+
+
+
+ {% if not quantity %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% else %}
+
+ {% endif %}
+
+{% endmacro %}
+
+
+
+{% macro cart_item(oob=False) %}
+
+{% set p = item.product %}
+{% set unit_price = p.special_price or p.regular_price %}
+
+
+ {% if p.image %}
+
+ {% else %}
+
+ No image
+
'market', 'product', p.slug
+ {% endif %}
+
+
+ {# Details #}
+
+
+
+
+ {% set href=market_product_url(p.slug, market_place=item.market_place) %}
+
+ {{ p.title }}
+
+
+
+ {% if p.brand %}
+
+ {{ p.brand }}
+
+ {% endif %}
+
+ {% if item.is_deleted %}
+
+
+ This item is no longer available or price has changed
+
+ {% endif %}
+
+
+ {# Unit price #}
+
+ {% if unit_price %}
+ {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
+
+ {{ symbol }}{{ "%.2f"|format(unit_price) }}
+
+ {% if p.special_price and p.special_price != p.regular_price %}
+
+ {{ symbol }}{{ "%.2f"|format(p.regular_price) }}
+
+ {% endif %}
+ {% else %}
+
No price
+ {% endif %}
+
+
+
+
+
+ Quantity
+ {% set qty_url = cart_quantity_url(item.product_id) if cart_quantity_url is defined else market_product_url(p.slug, 'cart', item.market_place) %}
+
+
+
+
+ -
+
+
+
+ {{ item.quantity }}
+
+
+
+
+
+ +
+
+
+
+ {% if cart_delete_url is defined %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+ {% if unit_price %}
+ {% set line_total = unit_price * item.quantity %}
+ {% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
+
+ Line total:
+ {{ symbol }}{{ "%.2f"|format(line_total) }}
+
+ {% endif %}
+
+
+
+
+
+{% endmacro %}