diff --git a/css/style.css b/css/style.css
index 0809380..b0cc4f4 100644
--- a/css/style.css
+++ b/css/style.css
@@ -115,9 +115,32 @@ html, body {
#layers {
display: flex;
flex-direction: column;
- gap: 2px;
+ gap: 1px;
overflow-y: auto;
}
+
+/* ---- collapsible category sections ---- */
+.cat:has(.cat-body:empty) { display: none; } /* hide unused categories */
+.cat-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 4px 3px;
+ margin-top: 4px;
+ cursor: pointer;
+ user-select: none;
+ color: var(--dim);
+ font-size: 9.5px;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+ border-top: 1px solid var(--border);
+}
+.cat-header:hover { color: var(--text); }
+.cat-chevron { font-size: 8px; width: 8px; transition: transform 0.15s; }
+.cat.collapsed .cat-chevron { transform: rotate(-90deg); }
+.cat.collapsed .cat-body { display: none; }
+.cat-body { display: flex; flex-direction: column; gap: 1px; }
+
.layer-row {
padding: 5px 4px;
border-radius: 6px;
diff --git a/js/ui.js b/js/ui.js
index 731fabc..0bf14f8 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1,9 +1,60 @@
-// HUD controller: layer registry, per-feed status lines, LIVE/SCRUBBED chip.
+// HUD controller: categorized collapsible layer list, per-feed status lines,
+// LIVE/SCRUBBED chip.
const rows = new Map();
+// Category scheme is owned here so HUD order is stable regardless of layer
+// registration order. Categories collapsed-by-default are marked below.
+const CATEGORY_ORDER = ['Space', 'Air', 'Sea', 'Earth & hazards', 'Context', 'Regional — Australia'];
+const COLLAPSED_BY_DEFAULT = new Set(['Regional — Australia']);
+const CATEGORY_BY_ID = {
+ satellites: 'Space', 'sat-paths': 'Space', launches: 'Space',
+ aircraft: 'Air', military: 'Air', 'aircraft-mil': 'Air',
+ ships: 'Sea',
+ quakes: 'Earth & hazards', fires: 'Earth & hazards', gdacs: 'Earth & hazards', nws: 'Earth & hazards',
+ infra: 'Context', events: 'Context',
+ rfs: 'Regional — Australia',
+};
+
+const sections = new Map(); // category → { body }
+
+// Lazily create a category section, inserted in CATEGORY_ORDER position.
+function sectionFor(category) {
+ if (sections.has(category)) return sections.get(category);
+ const wrap = document.createElement('div');
+ wrap.className = 'cat';
+ if (COLLAPSED_BY_DEFAULT.has(category)) wrap.classList.add('collapsed');
+ const header = document.createElement('div');
+ header.className = 'cat-header';
+ header.innerHTML = `▾`;
+ header.querySelector('.cat-name').textContent = category;
+ header.addEventListener('click', () => wrap.classList.toggle('collapsed'));
+ const body = document.createElement('div');
+ body.className = 'cat-body';
+ wrap.appendChild(header);
+ wrap.appendChild(body);
+
+ // Insert before the existing section that is this one's immediate successor in
+ // CATEGORY_ORDER (smallest index greater than ours), so DOM order stays stable
+ // regardless of registration order. Unknown categories sort last.
+ const container = document.getElementById('layers');
+ const ord = (c) => { const i = CATEGORY_ORDER.indexOf(c); return i < 0 ? Infinity : i; };
+ const myOrder = ord(category);
+ let before = null, bestOrder = Infinity;
+ for (const [cat, s] of sections) {
+ const idx = ord(cat);
+ if (idx > myOrder && idx < bestOrder) { bestOrder = idx; before = s.wrap; }
+ }
+ container.insertBefore(wrap, before);
+ const rec = { wrap, body };
+ sections.set(category, rec);
+ return rec;
+}
+
// Register a layer row. Returns a handle so callers can flip the checkbox.
-export function addLayer(id, name, defaultOn, onToggle) {
+// `category` is optional; a known id maps to a fixed category regardless.
+export function addLayer(id, name, defaultOn, onToggle, category) {
+ const cat = CATEGORY_BY_ID[id] || category || 'Other';
const row = document.createElement('div');
row.className = 'layer-row';
row.innerHTML = `
@@ -16,7 +67,7 @@ export function addLayer(id, name, defaultOn, onToggle) {
row.querySelector('.lname').textContent = name;
const cb = row.querySelector('input');
cb.addEventListener('change', () => onToggle(cb.checked));
- document.getElementById('layers').appendChild(row);
+ sectionFor(cat).body.appendChild(row);
rows.set(id, row);
return { setChecked: (v) => { cb.checked = v; } };
}