diff --git a/banner.js b/banner.js
new file mode 100644
index 0000000..739f9fa
--- /dev/null
+++ b/banner.js
@@ -0,0 +1,120 @@
+(function () {
+ // =========== README ==========
+ // Update the message below
+ // incrementversion DISMISSED_KEY
+ const DISMISSED_KEY = 'jf-banner-dismissed-v1';
+ const BANNER_ID = 'custom-announcement-banner';
+
+ // Edit these to change banner content:
+ const BANNER_HTML = `
+
+ News:
+
+ Get the official app for your device
+
+ |
+
+ Submit requests
+
+
+
+ `;
+
+ const BANNER_CSS = `
+ #custom-announcement-banner {
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #1c3a5c;
+ color: #e0e0e0;
+ font-size: 0.9em;
+ padding: 8px 16px;
+ box-sizing: border-box;
+ width: 100%;
+ z-index: 9999;
+ border-bottom: 1px solid #2d5a8e;
+ }
+ #custom-announcement-banner a {
+ color: #7cb8f0;
+ }
+ #custom-banner-close {
+ background: none;
+ border: none;
+ color: #e0e0e0;
+ cursor: pointer;
+ font-size: 1.1em;
+ padding: 0 4px;
+ flex-shrink: 0;
+ }
+ `;
+
+ function applyOffset() {
+ const banner = document.getElementById(BANNER_ID);
+ const offset = banner ? (banner.offsetHeight + 'px') : '';
+ const header = document.querySelector('.headerTop');
+ const drawer = document.querySelector('.mainDrawer');
+ const sections = document.querySelector('.homeSectionsContainer');
+ if (header) header.style.marginTop = offset;
+ if (drawer) drawer.style.marginTop = offset;
+ if (sections) sections.style.marginTop = offset;
+ }
+
+ function isHomePage() {
+ const h = window.location.hash;
+ return h === '' || h === '#' || h === '#/home' || h === '#!'
+ || h === '#!/' || h.startsWith('#/home') || h.startsWith('#!/home');
+ }
+
+ function injectBanner() {
+ if (document.getElementById(BANNER_ID)) return;
+
+ const banner = document.createElement('div');
+ banner.id = BANNER_ID;
+ banner.innerHTML = BANNER_HTML;
+ document.body.insertBefore(banner, document.body.firstChild);
+
+ document.getElementById('custom-banner-close').addEventListener('click', function () {
+ sessionStorage.setItem(DISMISSED_KEY, '1');
+ document.getElementById(BANNER_ID).remove();
+ applyOffset();
+ });
+
+ applyOffset();
+ }
+
+ function removeBanner() {
+ const el = document.getElementById(BANNER_ID);
+ if (el) el.remove();
+ applyOffset();
+ }
+
+ function update() {
+ if (sessionStorage.getItem(DISMISSED_KEY)) { removeBanner(); return; }
+ if (isHomePage()) { injectBanner(); } else { removeBanner(); }
+ }
+
+ // Inject CSS once
+ const style = document.createElement('style');
+ style.textContent = BANNER_CSS;
+ document.head.appendChild(style);
+
+ // React to SPA navigation
+ window.addEventListener('hashchange', update);
+
+ // Watch for Jellyfin's deferred body rendering (SPA bootstraps after DOMContentLoaded)
+ // subtree: true catches .headerTop wherever it appears in the DOM
+ const observer = new MutationObserver(function () {
+ update();
+ applyOffset();
+ });
+ document.addEventListener('DOMContentLoaded', function () {
+ observer.observe(document.body, { childList: true, subtree: true });
+ update();
+ });
+
+ // Fallback: run immediately if DOMContentLoaded already fired
+ if (document.readyState !== 'loading') update();
+})();
diff --git a/inject-banner.sh b/inject-banner.sh
new file mode 100755
index 0000000..f24b25d
--- /dev/null
+++ b/inject-banner.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -euo pipefail
+
+CUSTOM_DIR="/srv/jellyfin/custom"
+WEB_DIR="/usr/share/jellyfin/web"
+SRC_JS="${CUSTOM_DIR}/banner.js"
+INDEX="${WEB_DIR}/index.html"
+
+if [[ ! -f "${SRC_JS}" ]]; then
+ echo "jellyfin-inject-banner: ${SRC_JS} not found, skipping." >&2
+ exit 0
+fi
+
+python3 - "${INDEX}" "${SRC_JS}" <<'EOF'
+import sys
+
+index_path, js_path = sys.argv[1], sys.argv[2]
+
+with open(index_path, 'r') as f:
+ html = f.read()
+
+with open(js_path, 'r') as f:
+ js = f.read()
+
+# Remove any previous injection
+import re
+html = re.sub(r'', '', html, flags=re.DOTALL)
+
+# Inject before