{# `data-base` is what makes a custom theme inherit: tokens.css matches `[data-base="shire"]` as well as `[data-theme="shire"]`, so a custom light theme gets the whole parchment palette underneath its own handful of colours. Without it, four light colours would sit on Moria's near-black surfaces. `data-themes` is the list of `id:base` pairs, which is what the browser needs in order to set both attributes when somebody switches -- one attribute to split rather than a JSON island to parse. #} {# `viewport-fit=cover` is what lets `env(safe-area-inset-*)` resolve to anything but zero, and without it the `black-translucent` status bar style below is a promise with nothing behind it: iOS puts the page under the clock and the notch and the tokens that would have paid for it stay at 0. No `maximum-scale` and no `user-scalable=no` -- pinch-zoom is somebody's accessibility setting, not a layout problem to be suppressed. #} {% block title %}{{ brand.name }}{% endblock %} {# An uploaded favicon wins; then the icon derived from an uploaded logo; then the shipped leaf. Three rungs rather than two because somebody who uploads a logo and no favicon still expects the tab to change. #} {% if brand.favicon_path %} {% elif brand.icon_paths.favicon %} {% else %} {% endif %} {# Installing as an app. The manifest is a route, not a file, because it carries the instance name; the icons are PNG because a launcher will not take an SVG. theme-color is rewritten by app.js when the theme changes -- the value here is only what the browser paints with before the stylesheet has resolved. #} {# Two, scoped by preference, so the browser has an answer before any of our CSS or JavaScript has run. There used to be one and it was Moria's near-black, so every reader of the light theme got a dark browser chrome on every page load until `app.js` -- which is deferred -- corrected it. `applyTheme` still has the last word, and still reads the value from `--bg` rather than repeating a hex here; these two are only what is painted before it can. #} {% if brand.icon_paths['apple-touch'] %} {% else %} {% endif %} {# Last, so an administrator's rules win, and before {% block head %} so a page's own stylesheet still comes after it. The query string is a hash of everything the route builds, so the URL changes exactly when the stylesheet does -- with a fixed URL the browser's cache would be what decided when a rebrand took effect. Deliberately NOT in the service worker's precache list for the same reason: that cache is versioned by the release, and branding changes between releases. #} {% block head %}{% endblock %} {# Applied before first paint so a reload never flashes the wrong theme. The server already rendered data-theme from the signed-in user's preference; this only corrects it for a visitor whose local choice differs (or who is signed out, where the server has nothing to go on). #} {% include "partials/icons.html" %} {# Every fetch this application makes, said out loud. htmx has had `htmx-request` on the triggering element since the beginning and nothing here has ever used it, so a click that saved a setting, opened a panel or loaded a page of a list looked exactly like a click that did nothing until the answer arrived. On a local endpoint that is a few milliseconds and on anything else it is long enough to click again. One bar for the whole page rather than a spinner per control: the interesting question is "is the application busy", and an indicator on the control would need adding to every control ever written, which is how the last one came to be used nowhere. `aria-hidden` because the answer arriving is the thing worth announcing, and htmx already moves focus for that. #} {% block body %}{% endblock %} {# commands.js before composer.js: the second reads the first's table to draw the `/` menu, and both are deferred so the order here is the run order. #} {# The version in the query string is what versions the worker's cache, so a release invalidates it without anyone remembering to bump a constant. serviceWorker is absent over plain http, which is why a LAN install without TLS silently offers no install prompt -- that is the browser's rule, not ours. #} {% block scripts %}{% endblock %}