Styling and Theming
Precompiled, never CDN
Tailwind is compiled ahead of time into static/dist/app.css. There is no
runtime JIT and no CDN script tag. The visual result is identical; the
difference is that classes Tailwind cannot see fail loudly in development
instead of silently in production, and pages ship one minified stylesheet
instead of a compiler.
npm install # once; pulls Tailwind, DaisyUI, Biome (devDependencies only)
make build-static # compile + fingerprint
In development you rarely need either: pages render without a build because
asset resolution falls back to source paths (see
Asset pipeline), and make serve runs a watcher that
rebuilds CSS on template changes.
Tailwind only emits classes it can see
tailwind.config.js scans app/components/web_frontend/templates/**/*.html
and static/js/**/*.js. A Tailwind class anywhere else (a Python string,
a new template directory) will not be in the compiled CSS. If you add a
template location, add its glob to content in tailwind.config.js.
The single rebrand point
The stack uses the appearance system from Aegis Steward. Theme controls
voice and shape (aegis or steward); mode controls the palette (dark,
light, or the system preference). The four resolved DaisyUI themes are
aegis-dark, aegis-light, steward-dark, and steward-light.
tailwind.config.js is the single source for palettes, shapes, light-mode
tints, and chart colors. Its PALETTES, SHAPES, and TINTS tables generate
the four themes. Tailwind's aegis-* utilities read DaisyUI's active color
variables, so a color is defined once. static/input.css supplies shared
components and applies the theme's scale and label style. static/js/charts.js
reads the same color variables.
static/js/theme.js applies the stored theme and mode before the stylesheet
paints. It exposes appearance() and setAppearance(key, value) to the
theme_toggle() macro in components/macros/layout.html, which appears on
navigation and auth pages. System mode follows prefers-color-scheme. The
default is aegis-dark, set on <html> in base.html. Existing
aegis-light preferences are migrated to theme aegis, mode light.
Nothing else names a color. A test fails on any hex literal, and on any
theme-blind class such as text-white or bg-black, in templates or
static JS; use text-aegis-text and bg-aegis-scrim instead.
The layers
Three places styling can live, in order of preference:
- Utility classes in templates. The default. Most of the shipped markup is plain Tailwind utilities.
-
static/input.css,@layer components. For a pattern that repeats across templates when a Jinja macro would be overkill:Utilities are emitted after this layer, so a call site can still override a component class.
-
static/css/app.css. Hand-authored CSS that is not Tailwind at all. It ships with[x-cloak]{display:none}(hides elements until Alpine initializes them, preventing a flash of unstyled content on htmx swaps) and the nativeaccent-color, read from the theme's token.color-schemelives with each generated DaisyUI theme intailwind.config.js.
Linting
make lint-frontend runs Biome over
static/js/ and djlint over the templates. Both are
scoped to the web frontend; neither touches the rest of the project. djlint
runs with the Jinja profile and a small, documented ignore list in
pyproject.toml ([tool.djlint]). make format-frontend reflows the JS with
Biome; it is opt-in because reformatting is noisy in diffs.