<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ app_display }}{% endblock title %}</title>
<link rel="stylesheet" href="/static/css/lily.css">
<script src="/static/js/htmx.min.js" defer></script>
<script src="/static/js/alpine.min.js" defer></script>
<script>
(function () {
'use strict';
var rtl = { ar: 1, fa: 1, ur: 1 };
try {
var t = localStorage.getItem('lily-theme');
if (t === 'dark' || t === 'high-contrast') {
document.documentElement.setAttribute('data-theme', t);
}
var loc = localStorage.getItem('lily-locale');
if (loc && /^[a-z]{2}$/.test(loc)) {
document.documentElement.setAttribute('lang', loc);
document.documentElement.setAttribute('dir', rtl[loc] ? 'rtl' : 'ltr');
}
var acc = localStorage.getItem('lily-accent');
if (acc && /^#[0-9a-fA-F]{6}$/.test(acc)) {
document.documentElement.style.setProperty('--nhs-blue', acc);
}
} catch (e) { }
})();
</script>
</head>
<body>
<a class="skip-link" aria-label="Skip to main content" href="#main-content">Skip to main content</a>
<div class="super-banner"
role="alert"
aria-live="assertive"
aria-label="System announcement"
data-type="warning"
x-data='{
id: "maintenance-2026-05-29",
dismissed: false,
init() {
try {
var key = "lily-super-banner-dismissed-" + this.id;
this.dismissed = localStorage.getItem(key) === "1";
} catch (e) { /* ignore */ }
},
dismiss() {
this.dismissed = true;
try {
localStorage.setItem("lily-super-banner-dismissed-" + this.id, "1");
} catch (e) { /* ignore */ }
if (window.lily && window.lily.toast) {
window.lily.toast("Banner dismissed", "info");
}
}
}'
x-init="init()"
x-show="!dismissed">
<div class="banner-box">
<span>
<strong>Scheduled maintenance</strong> on 2026-05-29 02:00–04:00 UTC.
Read-only access during the window. See
<a href="/health" aria-label="Open system health page for status">system status</a>.
</span>
<button type="button"
aria-label="Dismiss announcement"
@click="dismiss()">Dismiss</button>
</div>
</div>
<header class="header" aria-label="Site header">
<h1>{{ app_display }}</h1>
<nav class="navigation-menu" aria-label="Primary navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/{{ entity_plural }}">{{ entity_plural | capitalize }}</a></li>
<li><a href="/{{ entity_plural }}/search">Search</a></li>
<li><a href="/{{ entity_plural }}/review-queue">Review queue</a></li>
<li><a href="/audit">Audit</a></li>
<li><a href="/notifications" aria-label="Notification center">Notifications</a></li>
<li><a href="/health">Health</a></li>
<li><a href="/metrics">Metrics</a></li>
<li><a href="/tour">Tour</a></li>
<li><a href="/docs">API docs</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</nav>
<div class="theme-picker" aria-label="Theme picker">
<label class="label" for="theme-select">Theme</label>
<select id="theme-select"
class="theme-select"
aria-label="Choose a visual theme">
<option class="theme-select-option" value="">NHS UK (default)</option>
<option class="theme-select-option" value="dark">Dark</option>
<option class="theme-select-option" value="high-contrast">High contrast</option>
</select>
</div>
<div class="locale-picker" aria-label="Locale picker">
<label class="label" for="locale-select">Language</label>
<select id="locale-select"
class="select"
aria-label="Choose a display language">
<option value="">English (default)</option>
<option value="ar">العربية · Arabic</option>
<option value="bg">Български · Bulgarian</option>
<option value="bh">भोजपुरी · Bhojpuri</option>
<option value="bn">বাংলা · Bengali</option>
<option value="cs">Čeština · Czech</option>
<option value="cy">Cymraeg · Welsh</option>
<option value="da">Dansk · Danish</option>
<option value="de">Deutsch · German</option>
<option value="el">Ελληνικά · Greek</option>
<option value="en">English</option>
<option value="es">Español · Spanish</option>
<option value="et">Eesti · Estonian</option>
<option value="eu">Euskara · Basque</option>
<option value="fa">فارسی · Persian</option>
<option value="fi">Suomi · Finnish</option>
<option value="fr">Français · French</option>
<option value="ga">Gaeilge · Irish</option>
<option value="gu">ગુજરાતી · Gujarati</option>
<option value="ha">Harshen Hausa · Hausa</option>
<option value="hi">हिन्दी · Hindi</option>
<option value="hr">Hrvatski · Croatian</option>
<option value="hu">Magyar · Hungarian</option>
<option value="id">Bahasa Indonesia · Indonesian</option>
<option value="it">Italiano · Italian</option>
<option value="ja">日本語 · Japanese</option>
<option value="jv">Basa Jawa · Javanese</option>
<option value="ko">한국어 · Korean</option>
<option value="lt">Lietuvių · Lithuanian</option>
<option value="lv">Latviešu · Latvian</option>
<option value="mr">मराठी · Marathi</option>
<option value="mt">Malti · Maltese</option>
<option value="nl">Nederlands · Dutch</option>
<option value="pa">ਪੰਜਾਬੀ · Punjabi</option>
<option value="pl">Polski · Polish</option>
<option value="pt">Português · Portuguese</option>
<option value="ro">Română · Romanian</option>
<option value="ru">Русский · Russian</option>
<option value="sk">Slovenčina · Slovak</option>
<option value="sl">Slovenščina · Slovenian</option>
<option value="sv">Svenska · Swedish</option>
<option value="ta">தமிழ் · Tamil</option>
<option value="te">తెలుగు · Telugu</option>
<option value="tr">Türkçe · Turkish</option>
<option value="ur">اردو · Urdu</option>
<option value="vi">Tiếng Việt · Vietnamese</option>
<option value="zh">普通话 · Mandarin</option>
</select>
</div>
</header>
<main id="main-content" class="page-wrapper">
{% block content %}{% endblock content %}
</main>
<footer class="footer" aria-label="Site footer">
<p>© 2026 {{ app_display }} — styled with the
<a href="https://lilydesignsystem.github.io">Lily Design System</a>,
powered by <a href="https://loco.rs">Loco</a>,
<a href="https://keats.github.io/tera/">Tera</a>,
<a href="https://htmx.org">HTMX</a>, and
<a href="https://alpinejs.dev">Alpine</a>.</p>
</footer>
<dialog id="command-palette"
class="dialog"
role="dialog"
aria-modal="true"
aria-label="Command palette"
x-data='{
query: "",
activeIndex: 0,
commands: [
{ label: "Go to Home", hint: "Top-level landing page", kb: ["g","h"], href: "/" },
{ label: "Go to {{ entity_plural | capitalize }}", hint: "{{ entity_plural | capitalize }} index", kb: ["g","i"], href: "/{{ entity_plural }}" },
{ label: "Search", hint: "Full-text + filters", kb: ["g","s"], href: "/{{ entity_plural }}/search" },
{ label: "Review queue", hint: "Pending duplicates", kb: ["g","r"], href: "/{{ entity_plural }}/review-queue" },
{ label: "Compare two records", hint: "Side-by-side match", href: "/{{ entity_plural }}/compare?a=aaaa-1&b=aaaa-2" },
{ label: "Calendar", hint: "Records by creation date", href: "/{{ entity_plural }}/calendar" },
{ label: "Map", hint: "Geo pins", href: "/{{ entity_plural }}/map" },
{ label: "Import CSV", hint: "Bulk-import wizard", href: "/{{ entity_plural }}/import" },
{ label: "System audit", hint: "Recent activity", kb: ["g","a"], href: "/audit" },
{ label: "System health", hint: "RAG + subsystem status", kb: ["g","e"], href: "/health" },
{ label: "Performance metrics", hint: "Sparklines per endpoint", href: "/metrics" },
{ label: "Guided tour", hint: "8-step walkthrough", href: "/tour" },
{ label: "API documentation", hint: "REST + FHIR endpoint reference", href: "/docs" },
{ label: "Color palette", hint: "Full NHS token customizer", href: "/palette" },
{ label: "Settings", hint: "Per-browser preferences", href: "/settings" },
{ label: "Show keyboard shortcuts", hint: "Open the shortcuts dialog", kb: ["?"], action: "shortcuts" },
{ label: "Switch theme: NHS UK (default)", hint: "Light theme", action: "theme:" },
{ label: "Switch theme: Dark", hint: "Inverted neutrals", action: "theme:dark" },
{ label: "Switch theme: High contrast", hint: "Pure black on white", action: "theme:high-contrast" },
{ label: "Reset palette", hint: "Clear all token overrides", action: "palette:reset" }
],
filtered() {
var q = this.query.trim().toLowerCase();
if (!q) return this.commands;
return this.commands.filter(function (c) {
return c.label.toLowerCase().indexOf(q) !== -1
|| (c.hint || "").toLowerCase().indexOf(q) !== -1;
});
},
invoke(c) {
if (c.href) {
window.location.href = c.href;
} else if (c.action === "shortcuts") {
document.getElementById("command-palette").close();
document.getElementById("shortcuts-dialog").showModal();
} else if (c.action && c.action.indexOf("theme:") === 0) {
var t = c.action.slice(6);
if (t) {
document.documentElement.setAttribute("data-theme", t);
try { localStorage.setItem("lily-theme", t); } catch (e) {}
} else {
document.documentElement.removeAttribute("data-theme");
try { localStorage.removeItem("lily-theme"); } catch (e) {}
}
if (window.lily && window.lily.toast) {
window.lily.toast("Theme switched", "info");
}
document.getElementById("command-palette").close();
} else if (c.action === "palette:reset") {
try {
var stored = JSON.parse(localStorage.getItem("lily-palette") || "{}");
Object.keys(stored).forEach(function (k) {
document.documentElement.style.removeProperty("--" + k);
});
localStorage.removeItem("lily-palette");
} catch (e) {}
if (window.lily && window.lily.toast) {
window.lily.toast("Palette reset", "warning");
}
document.getElementById("command-palette").close();
}
},
step(delta) {
var n = this.filtered().length;
if (n === 0) return;
this.activeIndex = (this.activeIndex + delta + n) % n;
},
choose() {
var f = this.filtered();
var pick = f[this.activeIndex] || f[0];
if (pick) this.invoke(pick);
}
}'>
<h2>Command palette</h2>
<input class="text-input-with-search"
type="search"
role="searchbox"
aria-label="Filter commands"
placeholder="Type to filter commands…"
autocomplete="off"
autofocus
x-model="query"
@input="activeIndex = 0"
@keydown.arrow-down.prevent="step(1)"
@keydown.arrow-up.prevent="step(-1)"
@keydown.enter.prevent="choose()">
<ol class="command" aria-label="Matching commands" role="listbox">
<template x-for="(c, i) in filtered()" :key="c.label">
<li role="option"
x-bind:aria-selected="i === activeIndex ? 'true' : 'false'"
x-bind:data-active="i === activeIndex ? 'true' : 'false'"
tabindex="-1"
@click="activeIndex = i; choose()"
@mouseover="activeIndex = i">
<strong x-text="c.label"></strong>
<template x-if="c.kb">
<span>
<template x-for="k in c.kb" :key="k">
<kbd class="kbd" x-text="k"></kbd>
</template>
</span>
</template>
<br>
<span class="hint" x-text="c.hint"></span>
</li>
</template>
<li role="option" x-show="filtered().length === 0" aria-disabled="true">
<span class="hint" aria-label="Empty filter">No commands match.</span>
</li>
</ol>
<form method="dialog" class="form" aria-label="Close command palette">
<button class="button" type="submit" aria-label="Close">Close</button>
</form>
</dialog>
<dialog id="shortcuts-dialog"
class="dialog"
role="dialog"
aria-modal="true"
aria-labelledby="shortcuts-title">
<h2 id="shortcuts-title">Keyboard shortcuts</h2>
<ol class="summary-list" aria-label="Available shortcuts">
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">?</kbd></dt>
<dd>Show / hide this dialog</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">Ctrl</kbd> <kbd class="kbd">K</kbd> · <kbd class="kbd">⌘</kbd> <kbd class="kbd">K</kbd></dt>
<dd>Open the command palette (works inside inputs too)</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">/</kbd></dt>
<dd>Focus the search field on this page</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">h</kbd></dt>
<dd>Go to Home</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">i</kbd></dt>
<dd>Go to the {{ entity_plural }} index</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">s</kbd></dt>
<dd>Go to Search</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">r</kbd></dt>
<dd>Go to the Review queue</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">a</kbd></dt>
<dd>Go to the system Audit log</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">g</kbd> <kbd class="kbd">e</kbd></dt>
<dd>Go to system Health</dd>
</dl>
</li>
<li class="summary-list-item">
<dl>
<dt><kbd class="kbd">Esc</kbd></dt>
<dd>Close any open dialog</dd>
</dl>
</li>
</ol>
<form method="dialog" class="form" aria-label="Close shortcuts">
<button class="button" type="submit" aria-label="Close">Close</button>
</form>
</dialog>
<div id="htmx-indicator" aria-live="polite">
<div class="progress-spinner"
role="progressbar"
aria-label="Loading"
aria-busy="true"></div>
<span>Loading…</span>
</div>
<div id="toast-region"
class="sonner"
role="status"
aria-label="Notifications"
aria-live="polite"></div>
<script>
(function () {
'use strict';
var region = document.getElementById('toast-region');
function spawn(message, type) {
if (!region || !message) return;
var t = document.createElement('div');
t.className = 'toast';
t.setAttribute('role', 'status');
t.setAttribute('aria-live', 'polite');
if (type) t.setAttribute('data-type', String(type));
t.setAttribute('aria-label', String(message));
t.textContent = String(message);
region.appendChild(t);
setTimeout(function () { t.remove(); }, 5000);
}
window.lily = window.lily || {};
window.lily.toast = spawn;
document.body.addEventListener('showToast', function (evt) {
var d = evt.detail || {};
spawn(d.message || d.value || '', d.type || 'info');
});
})();
(function () {
'use strict';
var indicator = document.getElementById('htmx-indicator');
if (!indicator) return;
var inflight = 0;
function update() {
if (inflight > 0) {
indicator.setAttribute('data-active', 'true');
} else {
indicator.removeAttribute('data-active');
}
}
document.body.addEventListener('htmx:beforeRequest', function () {
inflight += 1;
update();
});
document.body.addEventListener('htmx:afterRequest', function () {
inflight = Math.max(0, inflight - 1);
update();
});
document.body.addEventListener('htmx:responseError', function () {
inflight = Math.max(0, inflight - 1);
update();
if (window.lily && window.lily.toast) {
window.lily.toast('Request failed', 'error');
}
});
document.body.addEventListener('htmx:sendError', function () {
inflight = Math.max(0, inflight - 1);
update();
if (window.lily && window.lily.toast) {
window.lily.toast('Network error', 'error');
}
});
})();
(function () {
'use strict';
function findCard(trigger) {
var id = trigger.getAttribute('aria-describedby');
return id ? document.getElementById(id) : null;
}
function open(card) { if (card) card.setAttribute('data-open', 'true'); }
function close(card) { if (card) card.removeAttribute('data-open'); }
document.addEventListener('mouseover', function (evt) {
var t = evt.target.closest('.hover-card-trigger');
if (t) open(findCard(t));
});
document.addEventListener('mouseout', function (evt) {
var t = evt.target.closest('.hover-card-trigger');
if (t) close(findCard(t));
});
document.addEventListener('focusin', function (evt) {
var t = evt.target.closest('.hover-card-trigger');
if (t) open(findCard(t));
});
document.addEventListener('focusout', function (evt) {
var t = evt.target.closest('.hover-card-trigger');
if (t) close(findCard(t));
});
document.addEventListener('keydown', function (evt) {
if (evt.key === 'Escape') {
document.querySelectorAll('.hover-card[data-open="true"]').forEach(close);
}
});
})();
(function () {
'use strict';
var entityPlural = "{{ entity_plural }}";
var nav = {
h: '/',
i: '/' + entityPlural,
s: '/' + entityPlural + '/search',
r: '/' + entityPlural + '/review-queue',
a: '/audit',
e: '/health'
};
var dlg = document.getElementById('shortcuts-dialog');
var pendingGoto = 0;
function isTyping(target) {
if (!target) return false;
var tag = target.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;
}
document.addEventListener('keydown', function (evt) {
if ((evt.metaKey || evt.ctrlKey) && evt.key === 'k') {
evt.preventDefault();
var pal = document.getElementById('command-palette');
if (pal) {
if (pal.open) {
pal.close();
} else {
pal.showModal();
}
}
return;
}
if (evt.metaKey || evt.ctrlKey || evt.altKey) return;
if (isTyping(evt.target)) return;
if (evt.key === '?') {
evt.preventDefault();
if (dlg) {
if (dlg.open) {
dlg.close();
} else {
dlg.showModal();
}
}
return;
}
if (evt.key === '/') {
var input = document.querySelector('input[type="search"], input[type="text"]');
if (input) {
evt.preventDefault();
input.focus();
}
return;
}
if (evt.key === 'g') {
pendingGoto = Date.now();
return;
}
if (pendingGoto && Date.now() - pendingGoto < 1500) {
var dest = nav[evt.key];
if (dest) {
evt.preventDefault();
pendingGoto = 0;
window.location.href = dest;
}
}
});
})();
(function () {
'use strict';
var picker = document.getElementById('theme-select');
if (!picker) return;
var current = document.documentElement.getAttribute('data-theme') || '';
picker.value = current;
picker.addEventListener('change', function () {
var t = picker.value;
if (t === 'dark' || t === 'high-contrast') {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem('lily-theme', t); } catch (e) { }
} else {
document.documentElement.removeAttribute('data-theme');
try { localStorage.removeItem('lily-theme'); } catch (e) { }
}
if (window.lily && window.lily.toast) {
var label = picker.options[picker.selectedIndex] && picker.options[picker.selectedIndex].text;
window.lily.toast('Theme: ' + (label || 'default'), 'info');
}
});
})();
(function () {
'use strict';
var picker = document.getElementById('locale-select');
if (!picker) return;
var RTL = { ar: 1, fa: 1, ur: 1 };
var current = document.documentElement.getAttribute('lang') || '';
picker.value = (current === 'en') ? '' : current;
picker.addEventListener('change', function () {
var loc = picker.value;
if (loc && /^[a-z]{2}$/.test(loc)) {
document.documentElement.setAttribute('lang', loc);
document.documentElement.setAttribute('dir', RTL[loc] ? 'rtl' : 'ltr');
try { localStorage.setItem('lily-locale', loc); } catch (e) { }
} else {
document.documentElement.setAttribute('lang', 'en');
document.documentElement.setAttribute('dir', 'ltr');
try { localStorage.removeItem('lily-locale'); } catch (e) { }
}
if (window.lily && window.lily.toast) {
var label = picker.options[picker.selectedIndex] && picker.options[picker.selectedIndex].text;
window.lily.toast('Language: ' + (label || 'default'), 'info');
}
});
})();
(function () {
'use strict';
document.addEventListener('click', function (evt) {
var el = evt.target.closest('.clipboard-copy-button');
if (!el) return;
var text = el.getAttribute('data-clipboard-text') || el.textContent || '';
if (!navigator.clipboard) {
if (window.lily && window.lily.toast) {
window.lily.toast('Clipboard not supported by this browser', 'error');
}
return;
}
navigator.clipboard.writeText(text).then(function () {
el.setAttribute('data-copied', 'true');
setTimeout(function () { el.removeAttribute('data-copied'); }, 2000);
if (window.lily && window.lily.toast) {
window.lily.toast(el.getAttribute('data-copied-message') || 'Copied to clipboard', 'success');
}
});
});
})();
</script>
</body>
</html>