<h2 style="margin:0 0 .5rem 0;">Services</h2>
+<div class="search" role="search">
+ <input id="filter" placeholder="Filter … (press / to focus)" autocomplete="off" aria-label="Filter services" />
+</div>
+
<main class="grid" id="grid">
{% for s in services %}
<a
{% endfor %}
</main>
+<script>
+(() => {
+ const input = document.getElementById('filter');
+ const cards = Array.from(document.querySelectorAll('.grid .card'));
+ const empty = document.createElement('p');
+ empty.textContent = 'No matches.';
+ empty.style.display = 'none';
+ empty.style.color = 'var(--muted)';
+ const grid = document.querySelector('.grid');
+ grid.parentNode.insertBefore(empty, grid.nextSibling);
+
+ // Read initial query from ?q=
+ const params = new URLSearchParams(location.search);
+ const initial = params.get('q') || '';
+ if (initial) input.value = initial;
+
+ const norm = s => (s || '').toLowerCase().trim();
+ const matches = (el, q) => {
+ if (!q) return true;
+ // search name, description, tag, and any data-* attributes
+ const name = el.querySelector('h3')?.textContent || '';
+ const desc = el.querySelector('p')?.textContent || '';
+ const tag = el.getAttribute('data-tag') || '';
+ const hay = `${name}\n${desc}\n${tag}\n${el.textContent}`;
+ return norm(hay).includes(q);
+ };
+
+ let t;
+ const apply = () => {
+ const q = norm(input.value);
+ let shown = 0;
+ cards.forEach(card => {
+ const ok = matches(card, q);
+ card.style.display = ok ? '' : 'none';
+ if (ok) shown++;
+ });
+ empty.style.display = shown ? 'none' : '';
+ // reflect in URL (so refresh/bookmark keeps query)
+ const url = new URL(location);
+ if (q) { url.searchParams.set('q', q); }
+ else { url.searchParams.delete('q'); }
+ history.replaceState(null, '', url);
+ };
+
+ input.addEventListener('input', () => {
+ clearTimeout(t);
+ t = setTimeout(apply, 80); // tiny debounce
+ });
+
+ // run once on load
+ apply();
+})();
+</script>
+
<script>
(function () {
const PING_URL = "https://lan.zndr.dk/ping"; // 200 only on LAN