Autobahn Black
Lifetime warrantyThe entry level film, and the one that carries the lifetime warranty against bubbling, peeling and colour change. Dark, non reflective and it stays the colour you picked.
, 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