<!doctype html>
{#
  `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.
#}
<html lang="en" data-theme="{{ theme }}" data-base="{{ brand.theme(theme).base }}"
      data-themes="{{ brand.theme_list }}"{% if layout %} style="{{ layout }}"{% endif %}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ brand.name }}{% endblock %}</title>
<meta name="description" content="{{ brand.tagline or brand.name ~ ' — a web UI for your language models.' }}">
<meta name="color-scheme" content="dark light">

{# 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 %}
<link rel="icon" href="/branding/{{ brand.favicon_path }}">
{% elif brand.icon_paths.favicon %}
<link rel="icon" href="/branding/{{ brand.icon_paths.favicon }}">
{% else %}
<link rel="icon" href="{{ url_for('static', path='img/favicon.svg') }}" type="image/svg+xml">
{% 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.
#}
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#101317">
{% if brand.icon_paths['apple-touch'] %}
<link rel="apple-touch-icon" href="/branding/{{ brand.icon_paths['apple-touch'] }}">
{% else %}
<link rel="apple-touch-icon" href="{{ url_for('static', path='img/apple-touch-icon-180.png') }}">
{% endif %}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="{{ brand.name }}">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

<link rel="stylesheet" href="{{ url_for('static', path='css/tokens.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='css/app.css') }}">
{#
  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.
#}
<link rel="stylesheet" href="/branding.css?v={{ brand.revision }}">
{% 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).
#}
<script>
  (function () {
    try {
      var stored = localStorage.getItem("lembas-theme");
      if (stored && stored !== document.documentElement.dataset.theme) {
        document.documentElement.dataset.theme = stored;
      }
    } catch (e) { /* private mode: the server-rendered theme stands */ }

    /* Panel widths, for the same reason and in the same breath. app.js also
       applies these, but it is deferred -- so without this a panel dragged
       wider opens at its default and jumps once the script runs. */
    try {
      var widths = JSON.parse(localStorage.getItem("lembas-panel-widths") || "{}");
      Object.keys(widths).forEach(function (name) {
        if (name.indexOf("--") === 0) {
          document.documentElement.style.setProperty(name, widths[name] + "px");
        }
      });
    } catch (e) { /* the stylesheet's defaults stand */ }
  })();
</script>
</head>
<body{% block body_attrs %}{% endblock %}>
{% include "partials/icons.html" %}

{% block body %}{% endblock %}

<script src="{{ url_for('static', path='vendor/htmx.min.js') }}" defer></script>
<script src="{{ url_for('static', path='vendor/htmx-ext-sse.js') }}" defer></script>
<script src="{{ url_for('static', path='vendor/alpine.min.js') }}" defer></script>
<script src="{{ url_for('static', path='js/app.js') }}" defer></script>
<script src="{{ url_for('static', path='js/ui.js') }}" defer></script>
{# 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. #}
<script src="{{ url_for('static', path='js/commands.js') }}" defer></script>
<script src="{{ url_for('static', path='js/composer.js') }}" defer></script>
<script src="{{ url_for('static', path='js/audio.js') }}" defer></script>

{#
  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.
#}
<script>
  if ("serviceWorker" in navigator) {
    window.addEventListener("load", function () {
      navigator.serviceWorker.register("/sw.js?v={{ version }}").catch(function () {
        /* An install failure must never break the page it was loaded from. */
      });
    });
  }
</script>
{% block scripts %}{% endblock %}
</body>
</html>
