refactor(ui): make @theme the real token authority, and add a design-system page - #3288
Conversation
Tokens were forked between sass/_variables.scss and the Tailwind @theme block, with different hex under the same names. Whole categories had no tokens at all: 27 ad-hoc font sizes, 15 ad-hoc breakpoints, z-indexes scattered between SCSS and inline style attributes. - Move every design value into @theme: colour roles, type ramp, radii, elevation, breakpoints. Palette primitives live in css/palette.css as plain custom properties so they do NOT become utilities; only roles are reachable from markup. - Clear Tailwind's stock palette. text-red-600 and bg-white now emit nothing, closing that leak in the compiler rather than a style guide. - Add source(none) plus explicit globs. Tailwind was scanning the repo root on top of the declared @source, shipping Hugo marketing-site classes to players. - Add the light/dark role split across four planes, with dark overrides redeclaring roles only. - Add tests/test_design_tokens.py: every foreground/background pair holds WCAG AA 4.5:1 in both themes, the bar Lighthouse audits. Two contrast defects the new test caught: - Inherited Bootstrap red measured 4.53:1 as text on white, passing by 0.03 and failing on any tinted surface. - --color-danger could not serve as both ink and button fill: the dark theme lightens it for legible inline text, which left white label text on a destructive button at 2.75:1. Split into --color-danger and --color-danger-fill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The class names were renamed to .app-* long ago and a guard test covers templates, but the Bootstrap *values* survived where nothing looked: - The danger ramp was still #dc3545 / #c82333 / #bd2130. #dc3545 also measures 4.53:1 as text on white, passing WCAG AA by 0.03 and failing on any tinted surface. An upstream comment had already noticed the symptom and worked around it with a wash trio; this fixes the token. - .app-container still stepped through Bootstrap 5's grid tiers (576/768/992/1200/1400), the only reason those widths existed in the codebase. Replaced with one fluid container capped at the widest tier, which also removes the visible jump on resize. - text-capitalize sat in _asset_modal.html styling nothing, since Tailwind spells it `capitalize`. Guards so none of it comes back: - Two new stylesheet tests for Bootstrap palette hex and grid-tier media queries. The first immediately caught #dc3545 surviving as a palette primitive in the new token layer. - The template class guard gains the Bootstrap text/flex utilities whose Tailwind spellings differ, which is exactly how text-capitalize survived: wrong name, no styling, reads as working code. Intermediate viewports get wider from the container change; the widest tier is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
login.html, _error.html and splash-page.html each skipped tailwind.css on the grounds that they used no utility classes. That also meant they had no design tokens and no CSS reset, which stops being viable the moment components are shared across roots. The splash page's stated reason was saving a ~30 KB fetch on a Pi 1. Measured, that trade was the wrong way round: it was already pulling the entire 83 KB anthias.css to use a few percent of it, against Tailwind's 37 KB. Fixes a pre-existing bug found while verifying this. `body.auth-body main` (0,1,2) outranked `.auth-shell` (0,1,0), so the shell's min-height: 100vh never applied and the login and error cards have been top-aligned instead of vertically centred. The reset was unnecessary: `.auth-shell` already outranks the bare `main` selector on its own. Also adds bin/shoot_pages.py, which renders pages through the Django test client and captures them with Playwright. The integration suite is the real check but needs the compose stack; this is the fast loop for "did that CSS change break the login page", which the unit tests cannot see because they only assert on markup. It reuses conftest's Redis fake and the marketing captures' six-asset seed, and runs against a scratch database so it can never touch a real one. Verified: all four roots render correctly at three viewports with preflight now applying. Screenshots showed the centring bug, which is how it was found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four-plane model previously had the light theme put a near-white canvas behind everything, which is what a textbook light mode does. For Anthias that would have replaced the product's face rather than themed it: the deep-plum page behind white cards IS the product's look. So canvas and chrome now stay deep plum in both themes and only the surfaces flip. Consequences: - The light theme is exactly what Anthias looks like today. Shipping the toggle changes nothing for anyone who never touches it, and dark mode becomes purely additive. - Text sitting directly on the canvas is always light, so it needs a role of its own. --color-on-canvas is separate from --color-fg, which is text on a SURFACE and does flip. They are named apart because reaching for the wrong one is the easy mistake. - The feature card is lighter than the canvas in both themes, rather than changing direction between them. Fixes a modelling error in the contrast test found by this change: it composited translucent backgrounds over the canvas, but every translucent role we have is painted inside a card. Against the plum canvas the light theme's success wash read as dark green and its on-wash text scored 1.28:1. The backdrop is now the surface plane, and overridable per pair. Dark --color-fg-faint lightened to clear 4.5:1 on the tinted surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The commit that introduced the @theme layer never touched _styles.scss, which kept its own :root block. Both sheets are unlayered and anthias.css loads second, so for the 39 names declared in both, the SCSS won and @theme won none. Measured in Chromium, not inferred: - Every radius rendered the SCSS pixel value. --radius-sm was 4px while @theme said 0.25rem, so the rem-not-px rule held only on paper. - --color-warning-wash / -edge, --color-danger-wash / -edge, --color-focus-ring and --color-surface-tint each resolved to a value the token layer did not choose. - test_design_tokens.py scored @theme values for pairs the browser painted from SCSS, so the WCAG check was grading the wrong colours. - 21 of the 39 had no dark override, so SCSS light values leaked into dark mode. Dark itself was fine: :root[data-theme] outranks :root. Deletes the SCSS token block and moves its 431 references onto the token layer. Most of it is renaming, with three things worth flagging: - --space-N was a different scale from Tailwind's. --space-5 was 1.5rem, so 5/6/7/8 map onto 6/8/12/16, not onto themselves. - --color-text-on-dark was one name for three planes. Each of its 35 sites is resolved to the plane it actually paints on, which is what the canvas/chrome/feature split exists to distinguish. - --color-warning-soft / -strong were never declared anywhere. All 8 call sites had been rendering their inline fallback hex since the day they were written. The @theme block already diagnosed this; these are the call sites it never got to. @theme gains `static` because it has to: with the SCSS no longer declaring anything, 50 of the 109 tokens are reachable only through a var() in a sheet Tailwind does not scan, and v4 prunes those. $anthias-* colours are gone from the components too (13 sites), which retires _variables.scss. One of them, a stat-card link at #8819C7, was a fourth link colour nobody had noticed. Guarded by test_scss_declares_no_design_tokens: the SCSS may read a token, never declare one. None of this was visible in a diff of either file alone, which is why it needs a test and not a convention. Verified: 1948 unit tests pass, all four document roots render at three viewports in both themes, and a pixel diff against the pre-migration captures is empty except for the swatches whose values this corrects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One dev-only page at /_design/ rendering every token and every component variant. It is the reference for what the system contains, the place to compare both themes, and the cheapest test surface available: a single request exercises the whole component library. A page like this is only worth having if it is true, and it stops being true silently — a role is added to @theme, nobody adds the swatch, and the page now describes a slightly smaller system than the one that ships. So the catalogue is checked against @theme in both directions, and every documented value is checked against the declared one. That last check is the one that would have caught the radii claim this branch just fixed, where the page said 0.25rem and the browser painted 4px. The catalogues are data, so adding a token means adding a tuple. Writing the drift test surfaced nine roles that already existed and had never been documented: chrome-scrim, accent-hover, the three ring colours, focus-ring, and the strong success variants. The route is registered behind `DEBUG or IS_TEST`, not DEBUG alone. pytest-django sets settings.DEBUG = False for the whole run and urls.py is imported lazily from inside a test, so a DEBUG-only gate deletes the route exactly where the tests need it — which is what the original gate did, making the test it promised impossible to write. settings.py grows ENVIRONMENT and IS_TEST for this, replacing the same `getenv(...) == 'test' or _running_under_pytest` expression that had been written out at two other sites. IS_TEST deliberately does not feed DEBUG: a stray process whose argv mentions pytest must never be able to turn debug error pages on in production. The page immediately earned its keep. `app-btn-secondary` was documented as a variant, used at two call sites in _asset_modal.html, and styled nowhere — those two buttons had been rendering bare since they were written. Both are neutral actions on a light surface, which is app-btn-outline-dark, so the fix is the call sites and the phantom variant is gone. test_every_app_class_in_templates_is_styled now catches the whole class of it: an .app-* class in a template that the stylesheet never defines. It resolves SCSS nesting, since most component children are written as `&__name`. Verified: 1963 unit tests pass, and the page renders at three viewports in both themes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3288 +/- ##
=========================================
Coverage ? 90.31%
=========================================
Files ? 85
Lines ? 9942
Branches ? 1098
=========================================
Hits ? 8979
Misses ? 709
Partials ? 254 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR completes the UI design-token refactor by making Tailwind v4’s @theme layer the single authority for design tokens (colors, radii, typography, breakpoints, etc.), removing competing SCSS token declarations, and adding a dev-only /_design/ page that renders/catalogues the system and is guarded by unit tests.
Changes:
- Centralizes tokens in
static/src/tailwind.csswith supporting primitives (css/palette.css) and non-@themeglobals (css/base.css), and removes legacy SCSS variables/tokens. - Adds a dev-only design-system page (
/_design/) plus test guards for token drift, template class coverage, Bootstrap value leakage, and contrast ratios. - Updates multiple templates/components to use role-based utilities/classes (e.g.,
text-danger,capitalize,app-btn-outline-dark) and ensures shared roots include Tailwind tokens/reset.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_template_views.py | Adds guards for Bootstrap utility class leakage and verifies .app-* template classes exist in _styles.scss. |
| tests/test_design_tokens.py | New fast CSS-source parsing tests: token authority, Bootstrap value bans, and WCAG contrast checks. |
| tests/test_design_system_page.py | New tests validating /_design/ renders, stays in sync with @theme, and is dev/test-only. |
| src/anthias_server/django_project/settings.py | Introduces ENVIRONMENT/IS_TEST derivations and reuses them across settings branches. |
| src/anthias_server/app/urls.py | Registers /_design/ only under DEBUG or IS_TEST. |
| src/anthias_server/app/templates/splash-page.html | Adds Tailwind CSS to splash root (keeps Tabler Icons omitted). |
| src/anthias_server/app/templates/migrate_to_screenly.html | Replaces hardcoded Tailwind palette utilities with role-based classes (text-danger). |
| src/anthias_server/app/templates/login.html | Adds Tailwind CSS to login root so it receives tokens/reset consistently. |
| src/anthias_server/app/templates/import_content.html | Replaces hardcoded Tailwind palette utilities with role-based classes (text-danger). |
| src/anthias_server/app/templates/design_system.html | New standalone dev-only design system catalogue/demo page. |
| src/anthias_server/app/templates/_settings_toggle.html | Switches fixed colors to theme roles (border-divider, text-fg-muted). |
| src/anthias_server/app/templates/_error.html | Adds Tailwind CSS to error roots for consistent tokens/reset (still no JS). |
| src/anthias_server/app/templates/_asset_modal.html | Fixes unstyled secondary button usage and corrects text-capitalize to Tailwind’s capitalize. |
| src/anthias_server/app/static/src/tailwind.css | Expands token definitions in @theme static, adds strict source globs, imports palette/base/theme CSS. |
| src/anthias_server/app/static/src/css/theme-dark.css | New dark-theme role overrides keyed off [data-theme="dark"]. |
| src/anthias_server/app/static/src/css/palette.css | New primitive palette (non-@theme) to prevent primitives being reachable from markup. |
| src/anthias_server/app/static/src/css/base.css | New non-@theme globals (z-index scale, layout constants, selection, x-cloak). |
| src/anthias_server/app/static/sass/_variables.scss | Removes legacy Sass color variables/gradient. |
| src/anthias_server/app/static/sass/_styles.scss | Removes SCSS token declarations and migrates components to read from the new token layer. |
| src/anthias_server/app/design_system.py | Adds the view + catalogues backing the /_design/ page. |
| bin/shoot_pages.py | Adds a local (non-compose) screenshot helper using Django test client + Playwright. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Five of the eight were one finding: css:S8778, every @import after the first sitting at an invalid position. CSS allows only @charset and @layer before an import, and @source counts as a rule, so the vendor and palette imports below the @source globs were invalid even though Tailwind resolves them. All six imports are now one contiguous block above @source, which is also what the v4 docs do. Moving theme-dark.css and base.css up with them is safe and verified: no file imported there declares a name another one declares, the dark overrides win on specificity rather than order, and the token probe plus a pixel diff of all six design captures both come back identical. Their old position had a stated reason — keeping tokens alive by letting Tailwind see the var() references — which `@theme static` retired. The rest: - python:S3752 — the design view accepted any method. @require_safe. - python:S3776 — shoot_pages.main() at 16 cognitive complexity. The Playwright triple loop moves to _capture(). - python:S1192 — '0.75rem' appearing in three catalogues. Annotated, not extracted: it is a type step, a spacing step and a radius that happen to coincide, and one constant would couple three scales whose independence is the point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (11)
src/anthias_server/app/static/sass/_styles.scss:2564
- This uses a hardcoded z-index even though base.css defines a
--z-toasttier for toast/overlay UI. Using the token keeps the stacking scale consistent across overlays.
z-index: 1090;
src/anthias_server/app/static/src/tailwind.css:11
- This header comment says the build outputs a single
dist/css/anthias.css, but templates now link bothdist/css/tailwind.cssanddist/css/anthias.css(e.g. base.html/login.html/_error.html/splash-page.html). Updating the comment avoids confusion about which bundle owns what.
* Output is ONE stylesheet, dist/css/anthias.css, linked by all four
* document roots (base.html, login.html, _error.html, splash-page.html).
src/anthias_server/app/design_system.py:13
- The module docstring says the route is gated only by
settings.DEBUG, but urls.py registers it behindsettings.DEBUG or settings.IS_TEST. Updating this avoids misleading future readers about how the production gate works.
Dev-only. The route is registered in urls.py behind ``settings.DEBUG``,
so production images (ENVIRONMENT=production, hence DEBUG=False) never
expose it. The guard lives at route registration rather than inside the
view so there is no way to reach it at all.
src/anthias_server/app/templates/design_system.html:294
- The design-system page says “Never write a raw
@media(max-width: …)”, but the codebase (including _styles.scss) still uses raw media queries for non-scale, component-specific cutovers. This sentence is likely too absolute and could mislead contributors.
Never write a raw <code>@media (max-width: …)</code>.
src/anthias_server/app/static/sass/_styles.scss:49
padding-tophardcodes the nav height (4.5rem), but base.css now declares--nav-height. Using the token prevents these values drifting if the nav height changes.
padding-top: 4.5rem; // app-nav height
src/anthias_server/app/static/sass/_styles.scss:59
mainhardcodes the nav/footer heights in itsmin-heightcalc, but base.css defines--content-min-heightderived from--nav-height/--footer-height. Using the token keeps layout constants centralized.
min-height: calc(100vh - 4.5rem - 13rem); // app-nav + footer
src/anthias_server/app/static/sass/_styles.scss:2509
- Toast stacking uses a hardcoded z-index even though base.css defines
--z-toast. Using the token keeps the implementation aligned with the documented scale and avoids regressions if the scale changes.
This issue also appears on line 2564 of the same file.
z-index: 1080;
src/anthias_server/app/static/sass/_styles.scss:2744
- The bulk action bar still uses a Bootstrap-era z-index literal. base.css defines
--z-bulk-bar; using it ensures the bar stays below modals while remaining consistent with the global stacking scale.
z-index: 1040;
src/anthias_server/app/static/src/tailwind.css:19
- This comment describes “Custom CSS imported at the bottom” and token pruning based on scan usage, but this file ends after the
@theme+ dark variant and does not import the component SCSS/CSS. The comment should reflect the actual build so future changes don’t rely on behavior that isn’t present.
* Custom CSS is imported at the bottom rather than compiled separately.
* That is load-bearing: Tailwind reads those files, so the tokens they
* reference are marked used and kept in :root, while tokens nothing
* references anywhere are pruned. One authority, no duplication, no
* dead tokens.
src/anthias_server/app/static/sass/_styles.scss:843
- The modal overlay’s z-index is still a hardcoded literal. base.css defines
--z-modal; using the token keeps the stacking order consistent with the documented scale and prevents drift.
position: fixed;
inset: 0;
padding: calc(var(--spacing) * 6);
overflow-y: auto;
background: rgba(15, 0, 25, 0.55);
src/anthias_server/app/static/sass/_styles.scss:1050
- Flatpickr’s z-index is pinned with Bootstrap-era literals (1050/1100) even though base.css defines
--z-modaland--z-datepicker. Using the tokens keeps the scale consistent and makes the relationship explicit.
// Flatpickr in modal — keep the popup above the modal overlay's
Audit of every page against the design system. Two findings, both the
same shape as the token bug: the thing was declared, documented, and
then never actually wired up.
Layering. base.css said its --z-* scale "replaces 1040/1050/1060/1080/
1090/1100 scattered across the old SCSS plus three inline style=
z-index attributes". It had not. Every original number was still there,
the three inline attributes were still there, the navbar was on
Tailwind's z-50, and the scale was referenced by exactly one thing in
the codebase: the design page's table documenting it. A declared and
unused scale is worse than none, because it reads as done.
Everything is now on a rung, with relative order preserved:
.bulk-bar 1040 -> --z-bulk-bar 300
.modal-overlay 1050 -> --z-modal 400
3 inline overlays 1060 -> --z-modal-nested 410
.app-toast-stack 1080 -> --z-toast 600
.review-cta 1090 -> --z-nudge 610
flatpickr 1100 -> --z-datepicker 500
.app-nav z-50 -> --z-nav 200
--z-nudge is new; the scale had no rung for the review CTA. Toast and
datepicker do swap relative order, which the scale already intended.
The map's pin and readout keep their raw 500: they sit inside the
canvas's own stacking context and are chosen to clear Leaflet's internal
panes, so they are not rungs on this ladder. Said so in a comment.
The nested overlays needed a .modal-overlay--nested modifier rather than
a z-(--z-modal-nested) utility in the markup. Caught by measuring
instead of assuming: the utility ties with .modal-overlay at (0,1,0) and
loses on order, since anthias.css loads after tailwind.css, so all five
overlays flattened to 400. The inline style had only ever worked because
inline beats every stylesheet. Same cascade trap as the token block, one
layer up.
Icons. @tabler/icons-webfont ships 5193 outline icons and zero filled
ones, so every -filled name resolves to no glyph:
- all three toast icons, meaning every toast rendered with an empty
icon slot
- ti-minus-circle on the flat load-average trend, which is also just
the wrong name (Tabler spells it ti-circle-minus)
The design page had been showing correct outline icons for toasts all
along, which is how the mismatch surfaced.
Guarded by test_no_magic_z_index_values (no four-digit z-index in any
stylesheet or template) and by the layering table now being checked
against base.css in both directions.
Verified: 1964 unit tests pass, and computed z-index measured in
Chromium for all seven components plus each of the five overlays.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/anthias_server/app/static/src/tailwind.css:11
- The header comment says this produces a single output stylesheet at dist/css/anthias.css, but the build/templates still treat tailwind.css and anthias.css as separate bundles. This comment should reflect the actual output(s) to avoid future confusion when changing the pipeline.
* Output is ONE stylesheet, dist/css/anthias.css, linked by all four
* document roots (base.html, login.html, _error.html, splash-page.html).
src/anthias_server/app/design_system.py:13
- This docstring says the route is gated only by settings.DEBUG, but urls.py registers it behind
DEBUG or IS_TEST. Keeping the doc accurate matters since the production-safety guarantee depends on both flags.
Dev-only. The route is registered in urls.py behind ``settings.DEBUG``,
so production images (ENVIRONMENT=production, hence DEBUG=False) never
expose it. The guard lives at route registration rather than inside the
view so there is no way to reach it at all.
tests/test_template_views.py:3482
- The
class_attr_reregex currently matches theclass="..."substring inside Alpine bindings like:class="...". That makes the scan less predictable and can miss/ignore.app-*classes that are present only in bound expressions. Consider restricting the match to literalclass="..."attributes (not:class,x-bind:class, etc.).
django_tag_re = re.compile(r'\{%[^%]*%\}|\{\{[^}]*\}\}')
class_attr_re = re.compile(r'class="([^"]+)"')
templates = root / 'src/anthias_server/app/templates'
Both import wizards had opted out of it entirely, each with a page-local <style> block holding its own copy of the same progress bar under a different class name — a #e5e7eb track and a #2563eb fill, a blue that appears nowhere else in the product and in no token. The bar is now a real component, .app-progress, in the stylesheet where both pages can use it. Track and fill are --color-surface-sunken and --color-link, the same role the resource pies already use for a measured quantity. Neither page carries a <style> block any more. Their six inline status colours (#16a34a green, #d97706 amber) become text-success-on-wash and text-warning-on-wash, which is what the rest of the app uses for status ink on a surface — see .stat-card's --ok/--warn. Unlike the raw hex, both are covered by the WCAG test. Also fixes .settings-section__row, which was in settings.html with no rule behind it. Measured rather than assumed: it computed to display: block, so every import provider's button stacked underneath its own description instead of sitting opposite it like every other section header on the page. Now flex, 112px tall to 70px, with a divider between consecutive rows. Leaves alone, having checked each: .resource-pie--memory is a Playwright selector in test_app.py, and the remaining unstyled names (confirm-delete, upload-asset-tab, asset-select--all, modal-alert--warning) are inert rather than wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/anthias_server/app/static/src/tailwind.css:19
- This header comment claims the build output is a single stylesheet (
dist/css/anthias.css) and that custom CSS is imported into this file, but templates still link bothdist/css/tailwind.cssanddist/css/anthias.css, and this file does not import the SCSS component layer. Updating the comment would avoid confusion about what actually ships.
* Output is ONE stylesheet, dist/css/anthias.css, linked by all four
* document roots (base.html, login.html, _error.html, splash-page.html).
* Tabler Icons stays a separate <link>: it references its font files
* with relative URLs that would not resolve from our bundle.
*
src/anthias_server/app/design_system.py:13
- The module docstring says the /_design/ route is gated only by
settings.DEBUG, butapp/urls.pyregisters it whenDEBUGorIS_TESTis true. This is misleading for readers trying to understand why the route exists under pytest but not in production.
Dev-only. The route is registered in urls.py behind ``settings.DEBUG``,
so production images (ENVIRONMENT=production, hence DEBUG=False) never
expose it. The guard lives at route registration rather than inside the
view so there is no way to reach it at all.
From Copilot's review. All three are comments that describe an intent rather than the code, which is the same failure this branch has been chasing in the stylesheets. tailwind.css's header was wrong three ways: it claimed one output stylesheet when there are two (tailwind.css and anthias.css, both linked by every root), it put z-index and duration in @theme when they live in base.css, and it justified importing custom CSS at the bottom on pruning grounds — a position the imports no longer occupy and a reason `@theme static` retired. Rewritten to say which file owns what, and to state the cascade fact that matters: anthias.css loads second, so _styles.scss declares no tokens. design_system.py's docstring still said the route was gated on DEBUG alone. The design page told readers never to write a raw @media (max-width), which _styles.scss does seventeen times. Not a lapse: that file is compiled by sass and never passes through Tailwind, so @variant is not available to it and a plain media query is the only option. The page now says which sheets can use @variant and which cannot, and notes the five off-scale component widths. Copilot's fourth comment, that base.css claimed a --z-* scale nothing used, was already fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/anthias_server/app/static/sass/_styles.scss:242
.app-btn-dangeruses--color-dangerfor its fill, but the token layer explicitly splits--color-danger(ink) from--color-danger-fill(button background). In dark theme--color-dangeris lightened, so the button fill will change and can drop contrast for the white label. Use--color-danger-fillfor the default fill/border (hover already uses the fill tokens).
.app-btn-danger {
background: var(--color-danger);
border-color: var(--color-danger);
color: var(--color-on-danger);
&:hover {
src/anthias_server/app/static/sass/_styles.scss:134
--surface-scrim-8is mapped to--color-scrim-6, which makes the name misleading and prevents callers from getting the intended 8% scrim rung. Since--color-scrim-8is defined in the token layer, wire--surface-scrim-8to that token.
--surface-scrim-2: var(--color-scrim-2);
--surface-scrim-5: var(--color-scrim-5);
--surface-scrim-8: var(--color-scrim-6);
--surface-scrim-10: var(--color-scrim-10);
src/anthias_server/app/static/sass/_styles.scss:60
css/base.cssintroduces--content-min-heightto centralize the100vh - nav - footercalculation, butmainstill hardcodesmin-height: calc(100vh - 4.5rem - 13rem). Use the token so layout constants remain consistent across roots.
main {
display: block;
min-height: calc(100vh - 4.5rem - 13rem); // app-nav + footer
padding: calc(var(--spacing) * 8) 0 calc(var(--spacing) * 12);
src/anthias_server/app/static/sass/_styles.scss:49
--nav-heightis now declared incss/base.css, butbodystill hardcodespadding-top: 4.5rem. Using the token keeps the navbar height single-sourced and prevents future drift.
This issue also appears in the following locations of the same file:
- line 57
- line 131
- line 238
background-color: var(--color-canvas);
margin: 0;
padding-top: 4.5rem; // app-nav height
font-family: "Plus Jakarta Sans", Helvetica, Arial, sans-serif;
font-size: 1rem;
From Copilot's second pass, which suppressed all four of these as low-confidence. The first is a real accessibility bug. .app-btn-danger filled itself with --color-danger. That token is INK: it lightens in dark mode so it stays readable on a dark surface. The fill role, --color-danger-fill, deliberately does not, and both the @theme comment and theme-dark.css say so in as many words. Painting white on the ink measures 2.75:1 in dark — the exact number the @theme comment predicted when it explained why the roles were split. The button's own :hover was already using the fill family; only its resting state was wrong. The curated contrast test could not have caught this. It checks that each token PAIR clears AA, which is a statement about the token layer's intent. Nothing checked which token a component actually reaches for. test_ink_tokens_are_not_used_as_fills closes that: a background carrying --color-on-danger must come from the --color-danger-fill ramp, and test_every_fill_in_a_ramp_carries_its_ink holds every rung of that ramp to 4.5:1 so hover and active are measured rather than assumed. It only flags fill-with-text-on-it. The load-average bar, the legend swatch and the map pin all fill with --color-danger and are correct to: they carry no text, and lightening is what keeps them visible on a dark surface. Three smaller ones, all the same "declared, never wired" shape this branch keeps finding: - --nav-height and --content-min-height existed in base.css while body, main, .app-nav and the sticky bulk bar all hardcoded 4.5rem / 13rem. - $footer-height duplicated --footer-height and was referenced nowhere. - --surface-scrim-8 resolved to --color-scrim-6. Its three siblings map 1:1, so this was an outlier rather than a tuned value. Verified: 1967 tests pass, every danger fill rung measured in both themes (6.47 / 7.96 / 9.59), and the guard confirmed to fail when the original bug is reintroduced — including the declaration ordering that defeated the first version of it, since .app-btn-danger writes its background above its color. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`stack: list[list]` tripped mypy's type-arg check in CI. The frame was heterogeneous — a colour and a list of backgrounds — which is awkward to express as one list. Two parallel stacks say the same thing and type cleanly. My error: I ran mypy on the files I had touched rather than project-wide, which is what CI runs and what CLAUDE.md asks for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/anthias_server/app/static/src/tailwind.css:62
- tailwind.css now inlines Flatpickr + Leaflet CSS, but those vendor styles are also already bundled into dist/css/anthias.css via src/anthias_server/app/static/sass/anthias.scss (see package.json build:css:components). Since templates link both tailwind.css and anthias.css, this will ship the same vendor CSS twice (payload bloat + potential cascade-order surprises). Keep these vendor styles in exactly one bundle (either remove these
@importshere, or remove the Sass imports).
@import "tailwindcss" source(none);
@import "flatpickr/dist/flatpickr.css";
@import "leaflet/dist/leaflet.css";
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/anthias_server/app/static/src/css/palette.css:22
- This comment references sass/_variables.scss as the prior competing source of truth, but that file is deleted in this PR. Updating the wording to refer to the old SCSS token block (or to _styles.scss historically) will keep the doc accurate for future readers.
/* Brand plum ramp.
*
* Resolves a fork that had been live for some time: sass/_variables.scss
* and the old @theme block declared different hex under the same
* names (purple-2 was #492955 in one and #3c1762 in the other, and
Eight defects from a review of this branch, none of them caught by the guards it added. - Re-add `@plugin "@tailwindcss/forms"`, dropped when the entry file was rewritten. The bundle went from 33 `[type=...]` rules to the 3 that Preflight ships, so every checkbox in the two import wizards fell back to a raw OS control. - Wire the navbar, nav drawer and footer onto `--color-chrome*`. They still hardcoded `rgba(15, 0, 25, ...)`, so the dark theme's chrome override never painted. Chrome is a veil over the canvas, so the roles are now one ink at three alphas, the light values byte-identical to what those three rules already rendered. - Put the datepicker back above the toasts. Renumbering had inverted them, so a toast arriving mid-edit covered an open calendar. - `--color-warning-edge` was the on-wash ink, an opaque brown, where every sibling edge is a tint. Restores the amber it replaced. - Two inline styles in the asset modal still read the deleted `--space-*`, collapsing the modal body's padding to 0. - `--out` in shoot_pages.py no longer rmtree's whatever it points at. - Correct two comments that described work this branch did not do: a CI bundle-size assertion, and a splash-page weight saving that is a 47 KB cost today. Guards: `@source` globs must match files, since `source(none)` makes a stale glob fail silently. Contrast now composites the chrome plane against the canvas rather than the surface, and covers all three rungs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
tests/test_template_views.py:3490
- This guard only scans literal
class="..."attributes. Alpine:class="..."/x-bind:class="..."can also introduceapp-*class names (e.g. templates/_toasts.html uses a bound app-toast modifier), so the test currently won’t catch missing.app-*classes added via bindings.
django_tag_re = re.compile(r'\{%[^%]*%\}|\{\{[^}]*\}\}')
class_attr_re = re.compile(r'class="([^"]+)"')
templates = root / 'src/anthias_server/app/templates'
`class="…"` also matches inside `:class="…"`, so the bindings looked
covered while nothing in them was checked: their value is a JS
expression, and every class name sits in a string literal that a token
scan reads straight past.
- Capture the binding prefix so bound values are parsed as expressions,
pulling names out of their string and template literals.
- Hold an interpolated name to its static prefix, since
`app-toast--${t.kind}` has no single value to look up. That still
catches the whole family being renamed or dropped.
Nothing is unstyled today; all four bound names resolve. Verified by
mutation: breaking the interpolated prefix and pointing a binding at an
undefined class each fail the guard.
Also tidies the previous commit: drops an import alias and single-sources
the backdrop value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/anthias_server/app/static/src/tailwind.css:64
- tailwind.css now inlines Flatpickr/Leaflet vendor CSS, but anthias.scss also imports the same vendor styles (src/anthias_server/app/static/sass/anthias.scss:2 and :5). Because pages link both dist/css/tailwind.css and dist/css/anthias.css, this duplicates the vendor CSS in the rendered page (larger payload, harder-to-reason-about cascade order). Prefer importing these vendor styles in exactly one bundle (either keep them here and remove them from anthias.scss, or drop these two
@importlines here and keep anthias.scss as the single source).
@import "tailwindcss" source(none);
@import "flatpickr/dist/flatpickr.css";
@import "leaflet/dist/leaflet.css";



Issues Fixed
No tracked issue. Six defects found while finishing the design system,
every one of them silent in normal use:
_styles.scssstill declared its own:roottoken block after the@themelayer landed. Both sheets are unlayered andanthias.cssloads second, so of the 39 names declared in both, the SCSS won all of
them and
@themewon none. Every radius rendered a pixel value whilethe token layer said rem, and the WCAG test was grading colours the
browser never painted.
--z-*scale inbase.csswas referenced by nothing but thedesign page's own table documenting it. Every value it claimed to have
replaced — 1040/1050/1060/1080/1090/1100, plus three inline
style="z-index"attributes and a Tailwindz-50on the navbar — wasstill in place.
@tabler/icons-webfontships 5193 outline icons and zero filled ones,so all three
-fillednames resolved to no glyph. Same forti-minus-circleon the flat load-average trend, which is also thewrong name (Tabler spells it
ti-circle-minus).page-local copy of the same progress bar with a
#2563ebfill — ablue in no token and nowhere else in the product.
.settings-section__rowwas insettings.htmlwith no rule behindit, so each import provider's button stacked under its own
description instead of sitting opposite it.
app-btn-secondarywas used at two call sites and styled nowhere.Description
Makes the design system true, then proves it.
onto
@theme/palette.css/base.css.$anthias-*colours aregone from the components too, retiring
_variables.scss.--z-*scale, orderpreserved.
/_design/, a dev-only page rendering every token and componentvariant in both themes, registered behind
DEBUG or IS_TESTso itcannot exist on a production image.
login,_errorandsplash-pageon the same stylesheet as everything else, which fixed apre-existing centring bug on the login and error cards.
so the light theme is what Anthias looks like today and shipping the
toggle changes nothing for anyone who never touches it.
Six new guards, because none of these failures is visible in a diff of
any single file: no token declarations in SCSS, no four-digit z-index,
no
.app-*class in a template that the stylesheet does not define, andthe design page checked against
@themein both directions plusvalue-by-value.
Checklist
Verification: 1964 unit tests pass. All four document roots captured at
three viewports in both themes, with a pixel diff against pre-migration
captures empty apart from the swatches whose values this corrects.
Token resolution and computed stacking order both measured in Chromium
rather than inferred — which is how the nested-overlay regression got
caught before it shipped. Not yet run on Pi hardware; the splash page is
the one surface the viewer renders and it is in the changed set.
🤖 Generated with Claude Code