diff --git a/archive/build.sh b/archive/build.sh
index 266d19f..4c0cf1a 100755
--- a/archive/build.sh
+++ b/archive/build.sh
@@ -23,6 +23,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"css/base.css" \
"css/layout.css" \
@@ -64,6 +65,7 @@ concat_files \
"js/app.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
> "$js_raw"
diff --git a/archive/template.html b/archive/template.html
index 5711b47..19cf886 100644
--- a/archive/template.html
+++ b/archive/template.html
@@ -36,12 +36,6 @@
diff --git a/browse/build.sh b/browse/build.sh
index 41a3536..2a99797 100755
--- a/browse/build.sh
+++ b/browse/build.sh
@@ -29,6 +29,7 @@ concat_files \
"../shared/vendor/codemirror-yaml.min.css" \
"../shared/context-menu.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"css/base.css" \
"css/tree.css" \
"css/preview-yaml.css" \
@@ -59,6 +60,7 @@ concat_files \
"../shared/preview-lib.js" \
"../shared/context-menu.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
"../shared/icons.js" \
"../shared/zddc-source.js" \
diff --git a/browse/template.html b/browse/template.html
index 5abb1e7..1a13b14 100644
--- a/browse/template.html
+++ b/browse/template.html
@@ -28,12 +28,6 @@
diff --git a/classifier/build.sh b/classifier/build.sh
index 14b41c1..27bf0aa 100755
--- a/classifier/build.sh
+++ b/classifier/build.sh
@@ -23,6 +23,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"css/base.css" \
"css/layout.css" \
@@ -62,6 +63,7 @@ concat_files \
"js/excel.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
> "$js_raw"
diff --git a/classifier/template.html b/classifier/template.html
index ab15a9c..5991e0b 100644
--- a/classifier/template.html
+++ b/classifier/template.html
@@ -32,12 +32,6 @@
diff --git a/form/build.sh b/form/build.sh
index 4dcab94..b373844 100755
--- a/form/build.sh
+++ b/form/build.sh
@@ -22,6 +22,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"css/form.css" \
> "$css_temp"
@@ -32,6 +33,7 @@ concat_files \
"../shared/logo.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
"js/app.js" \
"js/context.js" \
diff --git a/form/template.html b/form/template.html
index 5dda43b..e5b167f 100644
--- a/form/template.html
+++ b/form/template.html
@@ -26,12 +26,6 @@
diff --git a/landing/build.sh b/landing/build.sh
index 72d32f7..d29803a 100755
--- a/landing/build.sh
+++ b/landing/build.sh
@@ -22,6 +22,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"css/landing.css" \
> "$css_temp"
@@ -34,6 +35,7 @@ concat_files \
"../shared/logo.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
"js/landing.js" \
> "$js_raw"
diff --git a/landing/template.html b/landing/template.html
index 818e2b3..2e9618d 100644
--- a/landing/template.html
+++ b/landing/template.html
@@ -26,12 +26,6 @@
diff --git a/shared/elevation.css b/shared/elevation.css
index 0ef67ec..2f57126 100644
--- a/shared/elevation.css
+++ b/shared/elevation.css
@@ -1,56 +1,7 @@
-/* shared/elevation.css — on-page admin-elevation toggle.
- elevation.js appends this control to ONLY for users the
- server says can_elevate (sudo-style opt-in). It's a fixed bottom-
- right switch so it works in any tool without a header slot and
- stays clear of the top "admin mode is on" banner. Arming is
- per-page: the cookie is session-scoped and cleared on pagehide. */
-
-.elevation-toggle {
- position: fixed;
- right: 0.9rem;
- bottom: 0.9rem;
- z-index: 9300; /* above the is-elevated frame (9200) so it stays clickable */
- display: inline-flex;
- align-items: center;
- gap: 0.3rem;
- font-size: 0.78rem;
- color: var(--text-muted);
- user-select: none;
- cursor: pointer;
- padding: 0.2rem 0.5rem;
- border: 1px solid var(--border);
- border-radius: var(--radius);
- background: var(--bg);
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.14);
- transition: background 0.12s, border-color 0.12s, color 0.12s;
-}
-
-.elevation-toggle:hover {
- background: var(--bg-hover);
- border-color: var(--border-dark);
-}
-
-.elevation-toggle input[type="checkbox"] {
- margin: 0;
- cursor: pointer;
- accent-color: var(--danger);
-}
-
-.elevation-toggle__label {
- cursor: pointer;
- letter-spacing: 0.02em;
-}
-
-/* Active state — when elevation is ON, the toggle reads as "armed"
- so the user can't miss that admin powers are currently live.
- :has(:checked) lets us style the wrapper based on the inner
- checkbox without JS. */
-.elevation-toggle:has(input:checked) {
- background: rgba(220, 53, 69, 0.12);
- border-color: var(--danger);
- color: var(--danger);
- font-weight: 600;
-}
+/* shared/elevation.css — page-wide armed chrome for admin mode.
+ The elevate CONTROL is the "Admin mode" item in the shared profile menu
+ (shared/profile-menu.{js,css}); this file only styles the unmistakable
+ "you are elevated" cues: the red viewport frame + the sticky banner. */
/* Page-wide chrome when admin mode is active. The toggle alone is
easy to miss; these add an inescapable visual cue:
diff --git a/shared/elevation.js b/shared/elevation.js
index ff6a442..a29f35a 100644
--- a/shared/elevation.js
+++ b/shared/elevation.js
@@ -1,17 +1,18 @@
-// shared/elevation.js — admin elevation via an on-page toggle.
+// shared/elevation.js — admin elevation state machine.
//
// Sudo-style model: admins behave as normal users by default; elevating
-// the session turns on admin escape hatches (WORM bypass, .zddc edit
-// authority, profile admin scaffolds). State is carried in a
-// `zddc-elevate=1` cookie that the server reads via handler.ACLMiddleware
-// → zddc.Principal{Elevated}.
+// the session turns on admin escape hatches (WORM bypass, recursive
+// delete, rearranging records, profile admin scaffolds — NOT config-edit,
+// which is standing). State is carried in a `zddc-elevate=1` cookie that
+// the server reads via handler.ACLMiddleware → zddc.Principal{Elevated}.
//
-// Two ways to arm, both gated on /.profile/access `can_elevate` so only
-// real admins can flip it (a non-admin's attempt is a silent no-op):
-// 1. The on-page toggle (renderToggle) — a small fixed control that we
-// render ONLY for users who can_elevate. This is the primary path.
-// 2. `?admin=true|false` typed into any URL — still honoured (handy for
-// deep links / scripting), just normalised into the same state.
+// This module owns the STATE (cookie, armed chrome/banner, ephemeral
+// lifecycle, the change event) + exposes setOn/setOff/isElevated. The
+// on-page elevate CONTROL lives in the shared profile menu
+// (shared/profile-menu.js) — an "Admin mode" item shown only to
+// can_elevate users — which calls setOn/setOff. `?admin=true|false` typed
+// into any URL is also honoured (gated on can_elevate), for deep links /
+// scripting.
//
// Admin mode is EPHEMERAL — scoped to the page you turned it on:
// * the cookie is a SESSION cookie (no Max-Age), and
@@ -63,19 +64,19 @@
} catch (_e) { /* CustomEvent unsupported — non-fatal */ }
}
- // setOn / setOff are the single funnel for every arm/drop path (toggle,
- // URL param, banner button). Each flips the cookie, re-paints the armed
- // chrome, syncs the toggle checkbox, and emits the change — no reload.
+ // setOn / setOff are the single funnel for every arm/drop path (the
+ // profile menu's Admin mode item, the ?admin= URL param, the banner's
+ // Drop button). Each flips the cookie, re-paints the armed chrome, and
+ // emits the change — no reload. The profile menu listens for the change
+ // event to keep its checkbox + armed indicator in sync.
function setOn() {
setElevated(true);
applyArmedChrome(true);
- syncToggle();
emitChange();
}
function setOff() {
setElevated(false);
applyArmedChrome(false);
- syncToggle();
emitChange();
}
@@ -181,75 +182,30 @@
}
}
- // renderToggle mounts the on-page admin switch — but ONLY for users the
- // server says can_elevate (i.e. who'd actually gain edit authority by
- // arming). Everyone else never sees it. It's a fixed control so it works
- // in any tool without that tool rendering a header slot for it.
- function renderToggle() {
- // Reuse the header placeholder the tool templates ship (an empty
- // ``) when present —
- // dropping its `hidden` class and repopulating it; otherwise create
- // one. Either way the fixed-position CSS floats it bottom-right, so
- // it doesn't matter where in the DOM it lives.
- var el = document.getElementById('elevation-toggle');
- var created = false;
- if (!el) {
- el = document.createElement('span');
- el.id = 'elevation-toggle';
- created = true;
- }
- el.className = 'elevation-toggle'; // drops any stale `hidden`
- el.title = 'Arm admin mode for this page. Drops automatically when you leave.';
- el.innerHTML = '';
- var input = document.createElement('input');
- input.type = 'checkbox';
- input.id = 'elevation-toggle-input';
- input.className = 'elevation-toggle__input';
- input.checked = isElevated();
- input.addEventListener('change', function () {
- if (input.checked) setOn(); else setOff();
- });
- var txt = document.createElement('span');
- txt.className = 'elevation-toggle__label';
- txt.textContent = 'Admin mode';
- txt.addEventListener('click', function () { input.click(); });
- el.appendChild(input);
- el.appendChild(txt);
- if (created) document.body.appendChild(el);
- }
-
- // syncToggle keeps the checkbox honest when state is flipped by some
- // other path (URL param, banner "Drop admin", bfcache restore).
- function syncToggle() {
- var input = document.getElementById('elevation-toggle-input');
- if (input) input.checked = isElevated();
- }
-
async function init() {
- // file:// (offline FS-Access mode) has no server to elevate against
- // — skip the access probe and the toggle entirely.
+ // file:// (offline FS-Access mode) has no server to elevate against.
if (window.location.protocol === 'file:') return;
// Reflect the cookie's armed chrome on every load (a leftover from a
// not-yet-fired pagehide, or an arrived-with ?admin link).
applyArmedChrome(isElevated());
- // One /.profile/access probe drives both decisions: whether to show
- // the toggle (can_elevate) and whether a ?admin=true may arm.
- var access = await fetchAccess();
- if (access && access.can_elevate) renderToggle();
- await handleAdminParam(access);
+ // Honour ?admin=true|false typed into any URL — handleAdminParam
+ // fetches /.profile/access itself to gate arming on can_elevate. The
+ // on-page elevate control lives in the shared profile menu
+ // (shared/profile-menu.js), which calls setOn/setOff and listens for
+ // zddc:elevationchange to keep its checkbox + armed ring in sync.
+ await handleAdminParam();
- // Admin mode is per-page: clear the cookie when the page goes away
- // so it never persists past a navigation. (No UI work here — the
- // page is unloading; the next page re-derives state from scratch.)
+ // Admin mode is per-page: clear the cookie when the page goes away so
+ // it never persists past a navigation.
window.addEventListener('pagehide', function () {
if (isElevated()) setElevated(false);
});
// bfcache can restore a page whose pagehide already cleared the
- // cookie — re-sync the visible state so chrome ≠ cookie can't happen.
+ // cookie — re-sync the armed chrome so chrome ≠ cookie can't happen.
window.addEventListener('pageshow', function (e) {
- if (e.persisted) { applyArmedChrome(isElevated()); syncToggle(); }
+ if (e.persisted) applyArmedChrome(isElevated());
});
}
diff --git a/shared/profile-menu.css b/shared/profile-menu.css
new file mode 100644
index 0000000..ce443b5
--- /dev/null
+++ b/shared/profile-menu.css
@@ -0,0 +1,111 @@
+/* shared/profile-menu.css — header account menu (upper-right).
+ shared/profile-menu.js mounts a button into `.header-right` and toggles
+ a dropdown with the signed-in email, Admin mode, Profile, Access tokens,
+ and Sign out. Server mode only. */
+
+.profile-menu {
+ position: relative;
+ display: inline-flex;
+}
+
+/* The button: a small circular avatar showing the email initial. */
+.profile-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ width: 1.9rem;
+ height: 1.9rem;
+ border-radius: 50%;
+ line-height: 1;
+}
+.profile-btn__avatar {
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ text-transform: uppercase;
+}
+/* Armed (admin mode on): a red ring so the elevated state reads from the
+ button even when the menu is closed — pairs with the page banner/frame. */
+.profile-btn--armed {
+ box-shadow: 0 0 0 2px var(--danger, #dc3545);
+ border-color: var(--danger, #dc3545);
+ color: var(--danger, #dc3545);
+}
+
+.profile-menu__panel {
+ display: none;
+ /* Fixed + JS-positioned from the button rect: an absolute panel gets
+ trapped below the content layer by the app's stacking contexts, so
+ anchor it to the viewport instead (profile-menu.js sets top/right). */
+ position: fixed;
+ min-width: 15rem;
+ z-index: 9400; /* above the is-elevated frame (9200) + banner (9100) */
+ background: var(--bg, #fff);
+ border: 1px solid var(--border, #ddd);
+ border-radius: var(--radius, 6px);
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.16);
+ padding: 0.3rem;
+ font-size: 0.85rem;
+}
+.profile-menu__panel.open { display: block; }
+
+.profile-menu__id {
+ padding: 0.35rem 0.55rem 0.45rem;
+}
+.profile-menu__email {
+ font-weight: 600;
+ color: var(--text, #222);
+ word-break: break-all;
+}
+.profile-menu__role {
+ margin-top: 0.1rem;
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--danger, #dc3545);
+}
+
+.profile-menu__sep {
+ height: 1px;
+ margin: 0.25rem 0;
+ background: var(--border, #eee);
+}
+
+.profile-menu__item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0.4rem 0.55rem;
+ border-radius: var(--radius, 4px);
+ color: var(--text, #222);
+ text-decoration: none;
+ cursor: pointer;
+ background: none;
+ border: none;
+ text-align: left;
+ font: inherit;
+}
+.profile-menu__item:hover {
+ background: var(--bg-hover, rgba(0, 0, 0, 0.05));
+}
+
+.profile-menu__toggle { cursor: pointer; }
+.profile-menu__check {
+ margin: 0;
+ cursor: pointer;
+ accent-color: var(--danger, #dc3545);
+ flex-shrink: 0;
+}
+.profile-menu__toggle-label {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.25;
+}
+.profile-menu__hint {
+ font-size: 0.72rem;
+ color: var(--text-muted, #888);
+}
diff --git a/shared/profile-menu.js b/shared/profile-menu.js
new file mode 100644
index 0000000..f7af5f4
--- /dev/null
+++ b/shared/profile-menu.js
@@ -0,0 +1,165 @@
+// shared/profile-menu.js — account menu in the header's upper-right.
+//
+// Replaces the old floating elevation toggle. Admin mode is now one item in
+// this dropdown, alongside the signed-in email, Profile, and Access tokens.
+// Mounts into the tool header's `.header-right` cluster (every tool ships one)
+// and drives elevation via window.zddc.elevation, so the cookie / armed-chrome
+// / ephemeral state machine stays in shared/elevation.js.
+//
+// No logout: authentication is the upstream proxy's concern (oauth2-proxy /
+// Authelia) — ZDDC owns no session, so it doesn't touch sign-out.
+//
+// Server mode only: it reads /.profile/access for the email + can_elevate.
+// On file:// (offline FS-Access mode) there's no server account, so nothing
+// renders.
+(function () {
+ 'use strict';
+
+ if (!window.zddc) window.zddc = {};
+ if (window.zddc.profileMenu) return;
+
+ function el(tag, cls, text) {
+ var e = document.createElement(tag);
+ if (cls) e.className = cls;
+ if (text != null) e.textContent = text;
+ return e;
+ }
+
+ async function fetchAccess() {
+ try {
+ var r = await fetch('/.profile/access', {
+ headers: { Accept: 'application/json' },
+ credentials: 'same-origin',
+ cache: 'no-cache'
+ });
+ if (!r.ok) return null;
+ return await r.json();
+ } catch (_e) { return null; }
+ }
+
+ var elevation = null;
+ var panelEl = null, btnEl = null, adminInput = null;
+
+ function isElevated() {
+ return !!(elevation && elevation.isElevated && elevation.isElevated());
+ }
+
+ // Keep the button's armed ring + the menu checkbox in lockstep with the
+ // elevation cookie (flipped here, by ?admin=, or by the banner's Drop).
+ function syncArmed() {
+ if (btnEl) btnEl.classList.toggle('profile-btn--armed', isElevated());
+ if (adminInput) adminInput.checked = isElevated();
+ }
+
+ function closeMenu() {
+ if (panelEl) panelEl.classList.remove('open');
+ if (btnEl) btnEl.setAttribute('aria-expanded', 'false');
+ }
+ // The panel is position:fixed (to escape the app's stacking contexts), so
+ // anchor it to the button rect — top just below it, right-aligned.
+ function positionPanel() {
+ var r = btnEl.getBoundingClientRect();
+ panelEl.style.top = (r.bottom + 4) + 'px';
+ panelEl.style.right = Math.max(4, window.innerWidth - r.right) + 'px';
+ panelEl.style.left = 'auto';
+ }
+ function toggleMenu() {
+ if (!panelEl) return;
+ var open = panelEl.classList.toggle('open');
+ if (open) positionPanel();
+ btnEl.setAttribute('aria-expanded', open ? 'true' : 'false');
+ }
+
+ function linkItem(text, href) {
+ var a = el('a', 'profile-menu__item', text);
+ a.href = href;
+ a.setAttribute('role', 'menuitem');
+ return a;
+ }
+
+ function build(access) {
+ var wrap = el('div', 'profile-menu');
+
+ btnEl = el('button', 'btn btn-secondary profile-btn');
+ btnEl.type = 'button';
+ btnEl.id = 'profile-btn';
+ btnEl.title = 'Account: ' + (access.email || 'signed in');
+ btnEl.setAttribute('aria-haspopup', 'menu');
+ btnEl.setAttribute('aria-expanded', 'false');
+ var initial = ((access.email || '?').trim().charAt(0) || '?').toUpperCase();
+ btnEl.appendChild(el('span', 'profile-btn__avatar', initial));
+ btnEl.addEventListener('click', function (e) { e.stopPropagation(); toggleMenu(); });
+ wrap.appendChild(btnEl);
+
+ panelEl = el('div', 'profile-menu__panel');
+ panelEl.setAttribute('role', 'menu');
+
+ var id = el('div', 'profile-menu__id');
+ id.appendChild(el('div', 'profile-menu__email', access.email || '(anonymous)'));
+ if (access.is_super_admin) id.appendChild(el('div', 'profile-menu__role', 'super admin'));
+ else if (access.has_any_admin_scope) id.appendChild(el('div', 'profile-menu__role', 'admin'));
+ panelEl.appendChild(id);
+ panelEl.appendChild(el('div', 'profile-menu__sep'));
+
+ // Admin mode — only offered to principals who actually have admin
+ // authority somewhere (can_elevate). Drops automatically on leave.
+ if (access.can_elevate && elevation) {
+ var row = el('label', 'profile-menu__item profile-menu__toggle');
+ adminInput = document.createElement('input');
+ adminInput.type = 'checkbox';
+ adminInput.className = 'profile-menu__check';
+ adminInput.checked = isElevated();
+ adminInput.addEventListener('change', function () {
+ if (adminInput.checked) elevation.setOn(); else elevation.setOff();
+ });
+ row.appendChild(adminInput);
+ var txt = el('span', 'profile-menu__toggle-label');
+ txt.appendChild(el('span', null, 'Admin mode'));
+ txt.appendChild(el('span', 'profile-menu__hint', 'bypass WORM/ACL — drops when you leave the page'));
+ row.appendChild(txt);
+ panelEl.appendChild(row);
+ panelEl.appendChild(el('div', 'profile-menu__sep'));
+ }
+
+ panelEl.appendChild(linkItem('Profile', '/.profile'));
+ panelEl.appendChild(linkItem('Access tokens', '/.tokens'));
+ // No "Sign out": authentication is the upstream proxy's concern
+ // (oauth2-proxy / Authelia). ZDDC doesn't own a session, so it
+ // doesn't render a logout affordance.
+
+ // Portal the panel to , not inside the header: the app's
+ // layout creates stacking contexts that trap even a fixed+high
+ // z-index panel below the content. As a direct body child it sits in
+ // the root stacking context and reliably overlays everything.
+ // position:fixed + positionPanel() keep it anchored to the button.
+ document.body.appendChild(panelEl);
+ return wrap;
+ }
+
+ async function init() {
+ if (window.location.protocol === 'file:') return;
+ elevation = window.zddc.elevation || null;
+ var access = await fetchAccess();
+ if (!access || !access.email) return; // unauthenticated / non-zddc backend
+ var host = document.querySelector('.header-right');
+ if (!host) return;
+
+ host.appendChild(build(access));
+ syncArmed();
+
+ document.addEventListener('click', function (e) {
+ if (panelEl && panelEl.classList.contains('open')
+ && !panelEl.contains(e.target) && !btnEl.contains(e.target)) closeMenu();
+ });
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); });
+ window.addEventListener('zddc:elevationchange', syncArmed);
+
+ window.zddc.profileMenu = { close: closeMenu };
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/tables/build.sh b/tables/build.sh
index d0dbfba..5fd6b07 100755
--- a/tables/build.sh
+++ b/tables/build.sh
@@ -22,6 +22,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"../shared/context-menu.css" \
"css/table.css" \
@@ -42,6 +43,7 @@ concat_files \
"../shared/logo.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
"../shared/context-menu.js" \
"js/mode.js" \
diff --git a/tables/template.html b/tables/template.html
index 65e484e..7d8a8d5 100644
--- a/tables/template.html
+++ b/tables/template.html
@@ -26,12 +26,6 @@
diff --git a/transmittal/build.sh b/transmittal/build.sh
index 9a3da19..cc63f84 100755
--- a/transmittal/build.sh
+++ b/transmittal/build.sh
@@ -26,6 +26,7 @@ concat_files \
"../shared/base.css" \
"../shared/toast.css" \
"../shared/elevation.css" \
+ "../shared/profile-menu.css" \
"../shared/logo.css" \
"css/base.css" \
"css/layout.css" \
@@ -87,6 +88,7 @@ concat_files \
"js/focus.js" \
"../shared/help.js" \
"../shared/elevation.js" \
+ "../shared/profile-menu.js" \
"../shared/cap.js" \
"js/main.js" \
> "$js_raw"
diff --git a/transmittal/template.html b/transmittal/template.html
index 3a006c8..2643527 100644
--- a/transmittal/template.html
+++ b/transmittal/template.html
@@ -51,12 +51,6 @@ conventions at https://codeberg.org/VARASYS/ZDDC#file-naming-convention.