diff --git a/docs/working/plans/2026-06-23-template-refactor.md b/docs/working/plans/2026-06-23-template-refactor.md
new file mode 100644
index 0000000..2bed4c7
--- /dev/null
+++ b/docs/working/plans/2026-06-23-template-refactor.md
@@ -0,0 +1,993 @@
+# Template Refactor Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Extract stats computation and panel HTML into Twig macros, extract date range formatting into a macro, and fix two latent bugs in inactive templates — with zero visual change.
+
+**Architecture:** Three Twig macros live in `templates/macros/`. `trip.html.twig` imports and calls the stats/cycling macros, shrinking from 386 to ~260 lines. `story.html.twig` and `stories.html.twig` both use the date-range macro. `feed-map.html.twig` and `map.html.twig` get DOMContentLoaded wrappers and correct asset registration timing.
+
+**Tech Stack:** Grav 2.0, Twig 3, Playwright (test runner), `make test-ui` → `npx playwright test`.
+
+## Global Constraints
+
+- Working directory for all file edits: `user/themes/intotheeast/templates/`
+- Dev server URL: `http://localhost:8081`
+- Demo trip used for testing: `/trips/italy-2026-demo`
+- Zero visual change — no HTML structure, CSS class, or JS logic changes
+- Twig macros are imported with `{% import 'macros/file.html.twig' as alias %}` inside `{% block content %}`
+- Macros live in `templates/macros/` (new directory — create it)
+- All macro arguments are positional (Twig macros use named params with defaults as of Twig 1.12, but positional is fine here)
+
+---
+
+## File Map
+
+| Action | Path | Responsibility |
+|---|---|---|
+| Create | `templates/macros/stats.html.twig` | Stats computation + stats panel HTML |
+| Create | `templates/macros/cycling.html.twig` | Cycling panel HTML (all JS placeholders) |
+| Create | `templates/macros/date-range.html.twig` | Smart condensed date range string |
+| Modify | `templates/trip.html.twig` | Import + call stats/cycling macros; remove 130 lines |
+| Modify | `templates/story.html.twig` | Replace 15-line date logic with macro call |
+| Modify | `templates/stories.html.twig` | Add `{% block map_assets %}`; replace date logic with macro call |
+| Modify | `templates/partials/feed-map.html.twig` | Remove asset calls; wrap map init in DOMContentLoaded |
+| Modify | `templates/map.html.twig` | Move `{% block map_assets %}` to top level; add DOMContentLoaded |
+| Modify | `templates/dailies.html.twig` | Add `{% block map_assets %}` override |
+
+---
+
+### Task 1: Stats + cycling macros; update trip.html.twig
+
+**Files:**
+- Create: `templates/macros/stats.html.twig`
+- Create: `templates/macros/cycling.html.twig`
+- Modify: `templates/trip.html.twig`
+
+**Interfaces:**
+- Produces: `stats_panel(journal_entries, page, journal_count, has_gpx)` — renders `
`
+- Produces: `cycling_panel()` — renders `
`
+- Both macros are imported at the top of `{% block content %}` in trip.html.twig
+
+- [ ] **Step 1: Create the macros directory**
+
+```bash
+mkdir -p user/themes/intotheeast/templates/macros
+```
+
+- [ ] **Step 2: Create `templates/macros/stats.html.twig`**
+
+This macro receives the entry collection, computes all server-side stats internally, and renders the complete stats panel. The `id="stat-distance"` placeholder is left empty for JS to fill after page load.
+
+```twig
+{% macro stats_panel(journal_entries, page, journal_count, has_gpx) %}
+{% set days_on_road = 0 %}
+{% if page.header.date_end is not empty %}
+ {% set start_ts = page.header.date_start|date('U') %}
+ {% set end_ts = page.header.date_end|date('U') %}
+ {% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
+{% else %}
+ {% set first_ts = null %}
+ {% for entry in journal_entries %}
+ {% set ts = entry.date|date('U') %}
+ {% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
+ {% endfor %}
+ {% if first_ts is not null %}
+ {% set diff_seconds = "now"|date('U') - first_ts %}
+ {% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
+ {% set days_on_road = days_raw < 1 ? 1 : days_raw %}
+ {% endif %}
+{% endif %}
+
+{% set seen_lower = [] %}
+{% set country_display = [] %}
+{% for entry in journal_entries %}
+ {% if entry.header.location_country is not empty %}
+ {% set lower = entry.header.location_country|trim|lower %}
+ {% if lower not in seen_lower %}
+ {% set seen_lower = seen_lower|merge([lower]) %}
+ {% set country_display = country_display|merge([entry.header.location_country|trim]) %}
+ {% endif %}
+ {% endif %}
+{% endfor %}
+
+{% set seen_city_lower = [] %}
+{% set city_display = [] %}
+{% for entry in journal_entries %}
+ {% if entry.header.location_city is not empty %}
+ {% set lower = entry.header.location_city|trim|lower %}
+ {% if lower not in seen_city_lower %}
+ {% set seen_city_lower = seen_city_lower|merge([lower]) %}
+ {% set city_display = city_display|merge([entry.header.location_city|trim]) %}
+ {% endif %}
+ {% endif %}
+{% endfor %}
+
+{% set temp_min = null %}
+{% set temp_max = null %}
+{% for entry in journal_entries %}
+ {% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
+ {% set t = entry.header.weather_temp_c %}
+ {% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
+ {% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
+ {% endif %}
+{% endfor %}
+
+
+
+
+
+ {{ days_on_road }}
+ {{ days_on_road == 1 ? 'day' : 'days' }} on the road
+