diff --git a/assets/static/js/main.ts b/assets/static/js/main.ts
index 551fbc4..4d1fc3f 100644
--- a/assets/static/js/main.ts
+++ b/assets/static/js/main.ts
@@ -1,6 +1,7 @@
// Side-effect import: installs the replaceChildren shim for the older-browser
-// degraded mode. Must stay first so the shim is in place before any render.
-import './polyfills'
+// degraded mode (shared across all apps). Must stay first so the shim is in
+// place before any render.
+import '@screenly-labs/signage-kit/polyfills'
import qrcode from 'qrcode-generator'
import { hostLabel, largestFit, relativeTime } from './render'
// The wire contract is defined once in the worker's parser. `import type` is
diff --git a/assets/static/js/polyfills.ts b/assets/static/js/polyfills.ts
deleted file mode 100644
index 4262c22..0000000
--- a/assets/static/js/polyfills.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Minimal runtime shim for the older-browser degraded mode. esbuild lowers modern
-// *syntax* (?., ??, spread) at build time, but it does not add missing runtime
-// APIs. The one DOM gap the apps can hit at the support floor is
-// Element.prototype.replaceChildren (Chrome 86 / Safari 14, 2020); it's included
-// as a defensive shim even in apps that don't call it directly. Everything else
-// the apps use (fetch, Intl, URLSearchParams) predates the floor, so no core-js.
-//
-// Imported only for its side effect (installing the shim) as the first line of
-// main.ts; it exports nothing.
-
-if (typeof Element !== 'undefined' && !Element.prototype.replaceChildren) {
- Element.prototype.replaceChildren = function replaceChildren(
- this: Element,
- ...nodes: (Node | string)[]
- ): void {
- // Build the new children in a fragment first so a bad node throws before we
- // touch the element, keeping the swap effectively atomic like the native API.
- const frag = document.createDocumentFragment()
- frag.append(...nodes)
- while (this.firstChild) this.removeChild(this.firstChild)
- this.appendChild(frag)
- }
-}
diff --git a/assets/static/styles/main.css b/assets/static/styles/main.css
index 8023580..aaa6d54 100644
--- a/assets/static/styles/main.css
+++ b/assets/static/styles/main.css
@@ -596,19 +596,11 @@ body {
}
/* =========================================================================
- Degraded mode — old/weak signage players (html.legacy set by the inline gate
- in Layout.tsx). Assume the device can't afford animation; drop it entirely.
- All motion here is gated behind prefers-reduced-motion: no-preference with no
- base opacity:0, so killing it leaves every element at its visible resting
- state -- no reveal mirror is needed.
+ Degraded mode — old/weak signage players (html.legacy set by the shared gate
+ in Layout.tsx). The generic animation/transition/will-change kill-switch is
+ prepended by @screenly-labs/signage-kit at build time (includeDegraded), so it
+ lives in the package, not here. This app has no app-specific html.legacy rules;
+ all motion is gated behind prefers-reduced-motion: no-preference with no base
+ opacity:0, so the shared kill-switch alone leaves every element at its visible
+ resting state.
========================================================================= */
-html.legacy *,
-html.legacy *::before,
-html.legacy *::after {
- /* biome-ignore lint/complexity/noImportantStyles: kill-switch must override the entrance animation rules */
- animation: none !important;
- /* biome-ignore lint/complexity/noImportantStyles: kill-switch must override any transition */
- transition: none !important;
- /* biome-ignore lint/complexity/noImportantStyles: release GPU layer hints on weak devices */
- will-change: auto !important;
-}
diff --git a/build.ts b/build.ts
index 2f4676f..547d5dc 100644
--- a/build.ts
+++ b/build.ts
@@ -17,60 +17,39 @@
// working-tree CSS stays unminified for editing).
import { Glob } from 'bun'
-import browserslist from 'browserslist'
-import { build as esbuild } from 'esbuild'
-import { browserslistToTargets, transform as lightningcss } from 'lightningcss'
+import { bundleJs, processCss } from '@screenly-labs/signage-kit/build'
import { run as syncFonts } from './sync-fonts'
const clientOnly = process.argv.includes('--client')
-// The `browserslist` field in package.json is the CSS support floor: Lightning
-// CSS down-levels the stylesheet to it. The JS is lowered separately by esbuild to
-// a fixed ES2017 syntax floor (kept at/below the browserslist minimum); esbuild
-// can't read browserslist, so keep the two in sync if you change the floor. See
-// the degraded-mode notes in Layout.tsx / main.css.
-const cssTargets = browserslistToTargets(browserslist())
-
// Vendor the Bun-managed webfonts into ./assets first.
await syncFonts()
// ---- Client JS bundle: main.ts -> main.js --------------------------------
-// esbuild bundles main.ts (inlining ./render, qrcode-generator, and the polyfills
-// shim), lowers modern syntax (?., ??, spread) to the ES2017 floor so old engines
-// can parse it, and emits an IIFE so the output stays a self-contained self-
-// executing classic script loadable from a plain
+
+ ${raw(GATE)}
${sentryScript(props.sentryId)}
${gaScript(props.gaId, props.feedId, props.feedTitle)}