Hang tight.
We're making some much needed upgrades.
, 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
— i.e. AFTER the\n client's — so equal-specificity ties resolve in our\n favour. !important is used only where a client declaration outranks us:\n inline style=\"\" attributes (4bkstorage index.astro:101/206/208, about.astro,\n units.astro, faq.astro; rockin2ind's four page heroes) and\n higher-specificity client rules (.field input:focus{outline:none} at\n bkstorage.css:1489-1495).\n\n SCOPING RULE, applied uniformly to underline / spacing / readable-font:\n effects reach PROSE CONTAINERS only — main, article, footer, [role=main],\n [role=contentinfo] — and never nav descendants. Verified reasons:\n * 4bkstorage: .brand{flex:none} (:375) + .brand-name{white-space:nowrap}\n (:387) + .nav-right{flex:none} (:432) leave .nav-links as the only\n shrinkable flex item, and the burger only appears at <=900px\n (:1724-1735). Widening header text wraps .nav-link inside position:sticky\n between ~901-1100px, changing header height mid-scroll.\n * rockin2ind is worse: .nav-bar{display:grid;\n grid-template-columns:auto 1fr auto; height:var(--nav-h)} (rockin2.css\n :151-157, --nav-h:76px at :66) with THREE white-space:nowrap children\n (.brand :168, .nav-phone :235, .open-badge :253). `auto` tracks cannot\n shrink below nowrap min-content, so the grid OVERFLOWS a hard-pinned\n 76px sticky bar — and rockin2.css:71-79 has NO overflow-x:hidden, so\n that is real horizontal document scroll, i.e. an SC 1.4.10 failure\n manufactured by an accessibility control.\n ========================================================================== */\n\n/* ---------- Motion ---------------------------------------------------------\n 1ms, not 0s: animationend/transitionend still fire, so client scripts that\n await them do not hang.\n\n CONTRACT NOTE for clients: 1ms is a FAST-FORWARD, not `animation: none`.\n An animation with fill-mode `both`/`forwards` snaps to its END state; one\n with fill-mode `none` (the default) reverts to the element's UN-ANIMATED\n computed style, which may never have been rendered. Any animation whose\n base or 0% appearance differs from its intended resting appearance needs its\n own explicit :root[data-a11y-motion=\"reduce\"] rule in the client sheet.\n Live example: rockin2ind's .open-badge .pulse::after (rockin2.css:263-274).\n ------------------------------------------------------------------------- */\n:root[data-a11y-motion=\"reduce\"] { scroll-behavior: auto !important; }\n:root[data-a11y-motion=\"reduce\"] *,\n:root[data-a11y-motion=\"reduce\"] *::before,\n:root[data-a11y-motion=\"reduce\"] *::after {\n animation-duration: 1ms !important;\n animation-delay: 0ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 1ms !important;\n transition-delay: 0ms !important;\n scroll-behavior: auto !important;\n}\n\n/* ---------- Underline links (prose only) -----------------------------------\n A bare `a { text-decoration: underline }` would wreck 4bkstorage: .btn is an\n in most CTAs (bkstorage.css:256-276) and .map-card (:1404) is an \n wrapping a background-grid map. Prose scoping has no false positives in\n either client.\n ------------------------------------------------------------------------- */\n:root[data-a11y-links=\"underline\"]\n :is(main, article, footer, [role=\"main\"], [role=\"contentinfo\"])\n :is(p, li, dd, dt, td, th, blockquote, figcaption, summary)\n a[href]:not([class*=\"btn\"]):not([class*=\"button\"]):not(nav *) {\n text-decoration-line: underline !important;\n text-decoration-thickness: max(1px, 0.06em);\n text-underline-offset: 0.15em;\n}\n\n/* ---------- Text spacing — SC 1.4.12 metrics, prose only -------------------\n overflow-wrap + min-width are mandatory, not cosmetic: bkstorage.css:134\n sets body{overflow-x:hidden}, which turns any overflow into INVISIBLE 1.4.10\n content loss. Unbreakable tokens (phone numbers, emails, unit-size labels,\n URLs) widen past a 320px viewport without them.\n\n Wide TABLES cannot be fixed from here (no wrapper to create) — DOCS.md makes\n an overflow-x:auto wrapper a client requirement for any table wider than\n ~40ch. Neither current client ships a