New markup over an old stylesheet

Reported from a desktop browser: a stray close button beside the logo, badly
drawn, and a page that would not scroll. None of it was in the code that was
running -- it was the code the browser had not fetched.

The worker caches /static/ under a cache named for the release while the files
in it carried no version, and a page is fetched network-first. That only ever
worked because the worker used to seize every open tab the instant it installed
and wipe the old cache. 1.1.0 stopped it doing that, rightly -- it was swapping
stylesheets out from under a streaming reply -- and a momentary mismatch became
a permanent one: new markup over the previous release's CSS for as long as the
old worker lived. `.sidebar__close` had no rule there, so `.btn--icon` made it
inline-flex: visible everywhere, placed by nothing.

Every /static/ URL carries the release now, written by `templating.asset` and
precached by `sw.js:versioned` -- both halves, because caches.match compares the
query too and precaching the bare path would cache entries nothing requests.
Self-correcting: updating is enough.

The header was also a brand with a button appended and margin-left:auto doing
the placing, which holds exactly while that button is last. Two slots now: a
brand that shrinks and truncates, and a rail on the trailing edge.

Verified before changing anything: with the current stylesheet the button is
display:none at 1280 and, with thirty chats and forty messages, both scrollers
scroll. The first measurement said the thread did not -- that was
scroll-behavior: smooth reporting where it started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 18:16:24 +00:00
co-authored by Claude Opus 5
parent 28390095a9
commit 201281d616
18 changed files with 216 additions and 48 deletions
+12 -12
View File
@@ -34,7 +34,7 @@
{% 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">
<link rel="icon" href="{{ asset('img/favicon.svg') }}" type="image/svg+xml">
{% endif %}
{#
@@ -57,15 +57,15 @@
{% 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') }}">
<link rel="apple-touch-icon" href="{{ asset('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') }}">
<link rel="stylesheet" href="{{ asset('css/tokens.css') }}">
<link rel="stylesheet" href="{{ asset('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
@@ -128,16 +128,16 @@
{% 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>
<script src="{{ asset('vendor/htmx.min.js') }}" defer></script>
<script src="{{ asset('vendor/htmx-ext-sse.js') }}" defer></script>
<script src="{{ asset('vendor/alpine.min.js') }}" defer></script>
<script src="{{ asset('js/app.js') }}" defer></script>
<script src="{{ asset('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>
<script src="{{ asset('js/commands.js') }}" defer></script>
<script src="{{ asset('js/composer.js') }}" defer></script>
<script src="{{ asset('js/audio.js') }}" defer></script>
{#
The version in the query string is what versions the worker's cache, so a