, which is why the UI half lives in panel.ts at the "page" stage.
A11Y_CSS is effects.css, passed in as a string argument by the integration.
========================================================================== */
try {
var W = window;
var D = document;
var H = D.documentElement;
/* Bail. /admin and /login are operator-only SSR surfaces that run their own
dark-mode system (src/layouts/Layout.astro), and data-a11y-disable is the
documented client escape hatch. On a bail we return WITHOUT defining
window.a11y — that absence is the single signal panel.ts keys on, so it
also covers this file having thrown. */
if (/^\/(admin|login)(\/|$)/.test(W.location.pathname)) return;
if (H.hasAttribute("data-a11y-disable")) return;
var KEY = "a11y:prefs";
var VERSION = 1;
/* Every value is a string enum, never a boolean: costs nothing today and
lets a future third state land without a storage migration.
`always:1` means the attribute is present whatever the value, so clients
test one thing (data-a11y-motion) rather than presence-or-value. */
var DEFS = [
{ k: "motion", attr: "data-a11y-motion", on: "reduce", off: "allow", os: "(prefers-reduced-motion: reduce)", always: 1 },
{ k: "links", attr: "data-a11y-links", on: "underline", off: null },
{ k: "spacing", attr: "data-a11y-spacing", on: "loose", off: null },
{ k: "font", attr: "data-a11y-font", on: "readable", off: null },
{ k: "focus", attr: "data-a11y-focus", on: "strong", off: null },
{ k: "contrast", attr: "data-a11y-contrast", enums: ["standard", "high"], off: null, os: "(prefers-contrast: more)", osValue: "high" },
{ k: "scheme", attr: "data-a11y-scheme", enums: ["light", "dark"], off: null, os: "(prefers-color-scheme: dark)", osValue: "dark" }
];
/* side + min are host-only: stored here, applied by panel.ts. They get no
attribute on because nothing in client CSS should key on them. */
var HOST = { side: ["start", "end"], min: ["yes"] };
/* ---- storage -----------------------------------------------------------
localStorage throws outright in some privacy modes, so every access is
wrapped and falls back to an in-memory object: the panel still works for
the session, it just does not persist. */
var mem = null;
var memOn = false;
function rawRead() {
if (memOn) return mem;
var s;
try { s = W.localStorage.getItem(KEY); } catch (e) { memOn = true; return mem; }
if (s == null) return null;
var o;
try { o = JSON.parse(s); } catch (e) { return null; }
if (!o || typeof o !== "object") return null;
if (Object.prototype.toString.call(o) === "[object Array]") return null;
if (typeof o.v !== "number") return null;
return o;
}
function rawWrite(o) {
if (!memOn) {
try { W.localStorage.setItem(KEY, JSON.stringify(o)); return; } catch (e) { memOn = true; }
}
mem = o;
}
/* Effective preferences. A future version is IGNORED but never deleted, so
rolling the build back loses nothing. A past version would migrate here. */
function prefs() {
var o = rawRead();
if (!o) return {};
if (o.v > VERSION) return {};
return o;
}
/* ---- resolution --------------------------------------------------------
explicit stored value > OS media query > built-in default
Never write a value just because the OS said so. Persisting a detected
value makes "unset" indistinguishable from "chosen" and the site stops
tracking the OS forever — the bug that silently kills OS-following. */
function mq(q) {
try { return W.matchMedia(q); } catch (e) { return null; }
}
function mqOn(q) {
var m = mq(q);
return !!(m && m.matches);
}
function defOf(k) {
for (var i = 0; i < DEFS.length; i++) { if (DEFS[i].k === k) return DEFS[i]; }
return null;
}
function allowed(d, v) {
var list = d.enums;
if (list) {
for (var i = 0; i < list.length; i++) { if (list[i] === v) return true; }
return false;
}
return v === d.on || v === d.off;
}
function hostAllowed(k, v) {
var list = HOST[k];
if (!list) return false;
for (var i = 0; i < list.length; i++) { if (list[i] === v) return true; }
return false;
}
function resolved(k) {
var p = prefs();
var v = p[k];
var d = defOf(k);
if (!d) return hostAllowed(k, v) ? v : null;
if (v != null && allowed(d, v)) return v;
if (d.os && mqOn(d.os)) return d.osValue || d.on;
return d.off;
}
function apply() {
for (var i = 0; i < DEFS.length; i++) {
var d = DEFS[i];
var v = resolved(d.k);
if (v == null) H.removeAttribute(d.attr);
else H.setAttribute(d.attr, v);
}
}
/* ---- effects sheet ----------------------------------------------------
Inserted unconditionally: predictability beats micro-optimisation, and
every selector is gated on a root attribute that fails immediately when
absent, so the universal-selector motion rules cost nothing when motion
resolves to "allow". */
function injectCss() {
if (D.getElementById("a11y-effects")) return;
var s = D.createElement("style");
s.id = "a11y-effects";
s.appendChild(D.createTextNode(A11Y_CSS));
(D.head || H).appendChild(s);
}
function fire(k) {
try {
D.dispatchEvent(new W.CustomEvent("a11y:change", {
detail: {
key: k,
value: k == null ? null : prefs()[k],
resolved: k == null ? null : resolved(k)
}
}));
} catch (e) {}
}
function set(k, v) {
if (v != null) {
var d = defOf(k);
if (d ? !allowed(d, v) : !hostAllowed(k, v)) return;
}
var stored = rawRead();
/* A stored version ahead of ours cannot be safely merged, so the first
explicit change replaces it wholesale. Acceptable: the user is actively
choosing at that moment. Otherwise read-modify-write preserves unknown
keys so a newer build's settings survive a rollback. */
var future = !!(stored && stored.v > VERSION);
var o = future || !stored ? {} : stored;
o.v = VERSION;
if (v == null) delete o[k];
else o[k] = v;
rawWrite(o);
apply();
fire(k);
}
function reset() {
if (!memOn) {
try { W.localStorage.removeItem(KEY); } catch (e) { memOn = true; }
}
mem = null;
apply();
fire(null);
}
apply();
injectCss();
var api = {
version: VERSION,
get: function (k) { return prefs()[k]; },
resolved: resolved,
set: set,
reset: reset,
motionReduced: function () { return resolved("motion") === "reduce"; }
};
/* caps is a LIVE getter and is never read pre-paint. In a production build
the client CSS is a before this script and a classic blocking
script waits on pending stylesheets, so getComputedStyle would resolve —
but under `astro dev` the CSS arrives through Vite's module graph and a
pre-paint read can return empty. That failure would be silent and
byte-identical to a client that never opted in. Nothing pre-paint needs
capabilities: the gated fieldsets live in the lazily-imported panel body. */
try {
Object.defineProperty(api, "caps", {
enumerable: true,
get: function () {
var raw = "";
try {
raw = getComputedStyle(H).getPropertyValue("--a11y-caps") || "";
} catch (e) {}
/* getPropertyValue returns the token stream verbatim, leading space and
literal quotes included, so a naive split yields ["", "\"contrast",
"dark\""] and every capability check silently fails. */
return raw.replace(/["']/g, " ").replace(/^\s+|\s+$/g, "").split(/\s+/)
.filter(function (t) { return !!t; });
}
});
} catch (e) {
api.caps = [];
}
W.a11y = api;
/* While a key is absent the OS query is authoritative and LIVE. Once the
user has chosen, an OS change must not move the site under them. */
for (var i = 0; i < DEFS.length; i++) {
(function (d) {
if (!d.os) return;
var m = mq(d.os);
if (!m) return;
var onChange = function () {
if (prefs()[d.k] != null) return;
apply();
fire(d.k);
};
if (m.addEventListener) m.addEventListener("change", onChange);
else if (m.addListener) m.addListener(onChange);
})(DEFS[i]);
}
try {
W.addEventListener("storage", function (e) {
if (e && e.key && e.key !== KEY) return;
mem = null;
memOn = false;
apply();
fire(null);
});
} catch (e) {}
} catch (e) {}
})("/* ============================================================================\n a11y effects sheet.\n\n Injected pre-paint by boot.js as the LAST node in
.\n ------------------------------------------------------------------------- */\n:root[data-a11y-spacing=\"loose\"]\n :is(main, article, footer, [role=\"main\"], [role=\"contentinfo\"])\n :is(p, li, dd, dt, blockquote, figcaption, summary, td, th):not(nav *) {\n line-height: 1.5 !important;\n letter-spacing: 0.12em !important;\n word-spacing: 0.16em !important;\n overflow-wrap: break-word !important;\n min-width: 0;\n}\n:root[data-a11y-spacing=\"loose\"]\n :is(main, article, footer, [role=\"main\"], [role=\"contentinfo\"])\n :is(p, blockquote, figcaption):not(nav *) { margin-block-end: 2em !important; }\n\n/* ---------- Readable font (prose only) -------------------------------------\n Honest scope: this switches PROSE type to the system stack. The strongest\n real win is a client's condensed display face — on 4bkstorage that is\n --f-display, a \"Google Sans Flex Cond\" family pinned to a 75% width axis,\n used for every heading inside main/footer.\n\n It deliberately does NOT touch letter-spacing (the spacing setting owns\n that) and does NOT touch text-transform: killing uppercase would render the\n brand as \"bk storage\" — misrepresentation, not accessibility. Nav micro-\n labels (.brand-l2 :397, .sheet-title :516) are out of scope by the header\n rule above; that is the price of deleting the header regression, and it is\n the right trade.\n\n `font-stretch: normal` and `font-variation-settings: normal` are NOT set,\n and must not be: a client may pin a variable font's width axis with a\n `font-stretch` DESCRIPTOR inside @font-face rather than a declaration on the\n element (4bkstorage does exactly this to get a condensed display family and a\n normal body family out of one file). A descriptor belongs to the @font-face,\n so replacing font-family drops the pinned width along with it. Resetting\n those properties here would instead fight any client that legitimately sets\n them on elements, for no gain.\n\n CONTRACT NOTE for clients: if you set `font-variation-settings` on ELEMENTS\n rather than as an @font-face descriptor, this override cannot reset it and\n the system stack may inherit meaningless axis values. Pin axes in\n @font-face.\n\n Ligatures: `none` includes `no-contextual`, which breaks Arabic and several\n Indic scripts. Narrowed to the three Latin-disambiguation families only.\n ------------------------------------------------------------------------- */\n:root[data-a11y-font=\"readable\"]\n :is(main, article, footer, [role=\"main\"], [role=\"contentinfo\"]),\n:root[data-a11y-font=\"readable\"]\n :is(main, article, footer, [role=\"main\"], [role=\"contentinfo\"])\n *:not(code, pre, kbd, samp, svg, svg *, iframe, nav, nav *, a11y-prefs, a11y-prefs *) {\n font-family: var(--a11y-readable-stack,\n system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif) !important;\n font-variant-ligatures: no-common-ligatures no-discretionary-ligatures\n no-historical-ligatures !important;\n}\n\n/* ---------- Strong focus ---------------------------------------------------\n White halo hugged by a black outline. Verified on 4bkstorage's palette:\n #fff is 13.69:1 on --navy #0c2e54 and 11.43:1 on --deep #0f3a6b; #000 is\n 21:1 on white. So one edge always clears SC 1.4.11's 3:1 whatever it lands\n on. GOV.UK yellow alone would not (#ffdd00 is 1.35:1 on white).\n\n !important is mandatory: .field input:focus{outline:none} (bkstorage.css\n :1489-1495) is (0,2,1) and beats :root[data-a11y-focus] :focus-visible at\n (0,2,0) — it would kill the ring on exactly the form fields where it matters\n most.\n\n NO border-radius declaration. At (0,3,0) it would outrank .btn's 11px\n (:265) and .btn-lg's var(--radius) (:275) and snap every focused button to a\n near-square on focus and back on blur. `outline` follows the element's own\n border shape in every current engine, which is the desired result for free.\n\n offset 2px + a 2px halo (5px total beyond the border box, was 7px) plus\n scroll-margin, because the ring is drawn inside the client's own clipping\n ancestors: .hero (:592), .type (:947), .cta (:1296), and body{overflow-x:\n hidden} (:134).\n ------------------------------------------------------------------------- */\n:root[data-a11y-focus=\"strong\"] :focus-visible {\n outline: 3px solid #000 !important;\n outline-offset: 2px !important;\n box-shadow: 0 0 0 2px #fff !important;\n scroll-margin: 12px;\n}\n@media (forced-colors: active) {\n :root[data-a11y-focus=\"strong\"] :focus-visible {\n outline-color: Highlight !important;\n box-shadow: none !important;\n }\n}\n\n/* ---------- Panel plumbing ------------------------------------------------\n No scroll lock. `:root{overflow:hidden}` was deleted: 4bkstorage's\n body{overflow-x:hidden} (:134) makes viewport-overflow propagation\n engine-dependent, and iOS Safari — the only platform where the bottom-sheet\n layout exists — ignores overflow:hidden for touch scrolling. It was\n unverifiable behaviour that changed the page under the user and contradicted\n the non-modal ruling. overscroll-behavior:contain on the sheet plus\n focusout-closes at <=40rem replaces it. Never write body.style.overflow:\n both clients' Nav.tsx reset it to \"\" unconditionally (4bkstorage :29-32,\n rockin2ind :40-43) and whichever closed last would destroy the other's lock.\n ------------------------------------------------------------------------- */\n@media print { a11y-prefs { display: none !important; } }\n");
Skip to content
Keep every product, variant, and SKU in one place. Set low-stock thresholds that trigger automatic reorder alerts, track quantities across multiple locations, and get real-time visibility into what's moving and what's sitting. Pairs seamlessly with the Storefront and Invoicing modules to keep numbers in sync without double entry.
Launch a full online store without a separate platform. Publish products with rich media, manage variants and pricing, and give customers a smooth cart-to-checkout experience. Every sale automatically updates Inventory, triggers Delivery workflows, and generates an Invoice.
Manage any rental fleet, from equipment and vehicles to spaces and anything in between. Build combo packages that bundle multiple items at a discounted rate, attach add-on features customers can select at checkout, and define custom options like sizes, durations, or insurance tiers. Assign rich metadata to every listing, set discount rules based on quantity, duration, or season, and let the system enforce availability constraints automatically. Late returns? Flagged and invoiced without lifting a finger.
Perfect for service-based businesses. Create combo deals that bundle services together at a special price, let customers pick add-on features like premium upgrades or extended sessions, and offer configurable options like therapist preference, room type and group size. Attach custom metadata to any booking for internal tracking, apply discount rules based on loyalty, time of day, or promo codes, and enforce scheduling constraints automatically. Customers pick a time slot, get instant confirmation, and receive automated reminders. Staff calendars update in real time, and no-show tracking helps you optimize over time.
What it does
Online appointment booking
Automated reminders
Staff calendar sync
No-show tracking
What you can configure
Service combo bundles at special pricing
Add-on upgrades (premium, extended, VIP)
Configurable options (therapist, room, group size)
UniFi door and gate access for staff and customers.
Connect Neon to UniFi Access and your doors and gates follow your records. Employees get door access from their Neon record, by role and shift, and lose it the moment they're removed. Customers with an active rental, booking or storage contract get gate access for as long as the contract runs, and it closes when the contract does. Every entry comes back into Neon and lands on the employee's or customer's timeline, beside the shifts and contracts it belongs to.
Run the team from the same panel as everything else. Keep staff records in one place, let people clock in and out, build and publish schedules, and track PTO and timesheets without a separate payroll spreadsheet. Roles decide what each person can see and do, and the same team list feeds the booking calendar and driver assignment.
Every interaction in one timeline. See purchase history, rental contracts, booking records, and communication logs for each customer. Segment your audience, tag VIPs, and let the system surface insights so you can build relationships that last.
Talk to your customers from the system that already knows them. Build a segment out of anything Customers holds, whether that is lapsed accounts, VIPs, or everyone with a rental due back on Friday, and send it an email or an SMS on your own template. Campaigns are the marketing half. The other half is the quiet one, where a status update goes out on its own because a record changed. Consent, opt-outs and reply routing are handled for you, and every send lands back on the customer's timeline beside the orders and contracts it belongs to.
From warehouse to doorstep, track every package in real time. Assign drivers, optimize routes, and let customers follow their delivery with live status updates. Integrates directly with the Storefront and Inventory modules so stock levels adjust the moment an order ships.
Send orders anywhere a carrier goes. Rates are worked out at checkout from the weights and sizes on your products, labels print straight from the order, and the tracking number reaches the customer the moment it's created. Every parcel ships from the same stock count the Storefront sells from, and its tracking lands on the customer's timeline beside the order it belongs to.
Order online, collect in store, with ready-for-pickup alerts.
Let customers buy online and collect at the counter. Each order gets a pickup window at the location that holds the stock, the customer is told when it's ready, and staff mark it handed over from the same order screen. Pickup orders come out of the Storefront and the stock count like any other sale, and every handover is recorded on the customer's timeline.
Sell one-off items, surplus stock or retired rental equipment to the highest bidder. List lots from your inventory with photos, a starting price and an optional reserve, run timed auctions with a countdown, and let customers bid from your own site. Outbid bidders are told straight away, and every bid lands on the bidder's customer record. When a lot closes, the winner is invoiced automatically and the item comes off stock, so nothing is re-entered after the hammer falls.
Create professional invoices in seconds, send them via email or SMS, and track payment status in real time. Set up automatic reminders for overdue balances and get a clear financial picture with built-in reporting. Connects to Stripe for seamless online payments.
Every other module already records what happened. Forecasting is what the platform does with that history. Project demand per SKU from real sales, rental and booking records, see how many weeks of cover you have left at the current run rate, and get back the date you need to place an order rather than a number you have to interpret. Seasonality windows and supplier lead times are yours to set, and the projection re-runs itself as the underlying records change, so there is nothing to rebuild and no spreadsheet pointed at last month's export.
Machine-learning price estimates and stock outlooks.
Smart pricing estimates what each item or service should cost, with a machine-learning model that reads your own sales, rental and booking history alongside season and utilisation. Before you change a price, it shows the inventory outlook after the change: how demand is projected to move, how many weeks of cover that leaves, and when you'd need to reorder. The estimate is a suggestion. You decide what to charge.
Dashboards, reports & exports across every module.
One question, asked once, answered across the whole business. Revenue, orders, utilization, no-shows and outstanding balances all come out of the same records the operational screens read, so a dashboard here cannot disagree with an invoice there. Build the views you want, break them down by segment, period or module, schedule them to arrive in an inbox on Monday morning, and export anything an accountant asks for. There is nothing to join, nothing to reconcile, and no month-end afternoon spent making three tools agree on one number.
Drag-and-drop forms, one entries inbox & automations.
Build any form your business needs, from a contact form to a job application, and put it on your Helium site. Drag fields into place, add conditional rules, calculations and multiple pages, and watch the live preview as you work. Publish numbered versions you can compare in plain English and restore at any time. Entries from a linked site are all stored in Neon, where you can sort them, add notes and export them, and if Neon can't be reached the site queues each entry and sends it later. Any entry can start an Action, and one form can appear on any number of sites.
What it does
Drag-and-drop builder
Conditional logic & calculations
Version history
One entries inbox
Entries that start an Action
What you can configure
Starter templates for quotes, bookings, jobs and more
Automate the follow-ups nobody has time for. Draw each automation on a canvas and let it start when something happens, on a schedule, from a date on a record, from an inbound webhook or by hand. Steps can branch, wait, look up records, send email or SMS, call outside systems and update customers, invoices, bookings and rentals. Test mode shows who would be contacted and what would change without sending anything, and run history replays every run step by step. Anything that moves money or cancels something needs an approval from a second person first, and that approval says in plain terms what a yes will do.
Change your website without waiting on anyone. Drag sections, columns, stacks and grids into place, then fill them with headings, images, cards, FAQs and more, or start from a premade section. Embed a form or Neon Displays and your stock stays live on the page. Set a site's colours, corners, fonts and content width once, and saving restyles every published page on that site. Work in a draft, preview it by device, and publish when it's ready: a published page can even replace an existing one, including your home page.
What it does
Drag-and-drop layouts
Live forms & Displays on the page
Site-wide themes
Draft, preview & publish
Manage several sites
What you can configure
Blank, Landing page and About starters
Editable premade sections
Nested sections with their own background and overlay
Take control of how your business appears in search results. Edit meta titles, descriptions, and Open Graph tags for every page. Auto-generate sitemaps, manage canonical URLs, and get clear recommendations to improve your rankings. No SEO expertise required.
Share your expertise and keep customers engaged with a built-in blog. Write and schedule posts with a rich text editor, organize content with categories and tags, and let the SEO module optimize every article automatically. Perfect for company news, how-to guides, and thought leadership.
Open Neon to the rest of your tools. Generate API keys, read and write your records through the Data API, and receive inbound webhooks from the systems you already use, whether that is an outside point of sale, a BI tool or an app of your own.