Skip to content

refactor(ui): make @theme the real token authority, and add a design-system page - #3288

Merged
vpetersson merged 15 commits into
Screenly:masterfrom
vpetersson-bot:refactor/design-system-tokens
Aug 16, 2026
Merged

refactor(ui): make @theme the real token authority, and add a design-system page#3288
vpetersson merged 15 commits into
Screenly:masterfrom
vpetersson-bot:refactor/design-system-tokens

Conversation

@vpetersson-bot

@vpetersson-bot vpetersson-bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Issues Fixed

No tracked issue. Six defects found while finishing the design system,
every one of them silent in normal use:

  • _styles.scss still declared its own :root token block after the
    @theme layer landed. Both sheets are unlayered and anthias.css
    loads second, so of the 39 names declared in both, the SCSS won all of
    them and @theme won none. Every radius rendered a pixel value while
    the token layer said rem, and the WCAG test was grading colours the
    browser never painted.
  • The --z-* scale in base.css was referenced by nothing but the
    design 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 Tailwind z-50 on the navbar — was
    still in place.
  • Every toast in the product rendered with an empty icon slot.
    @tabler/icons-webfont ships 5193 outline icons and zero filled ones,
    so all three -filled names resolved to no glyph. Same for
    ti-minus-circle on the flat load-average trend, which is also the
    wrong name (Tabler spells it ti-circle-minus).
  • Both import wizards had opted out of the system, each carrying a
    page-local copy of the same progress bar with a #2563eb fill — a
    blue in no token and nowhere else in the product.
  • .settings-section__row was in settings.html with no rule behind
    it, so each import provider's button stacked under its own
    description instead of sitting opposite it.
  • app-btn-secondary was used at two call sites and styled nowhere.

Description

Makes the design system true, then proves it.

  • Deletes the competing SCSS token block and moves its 431 references
    onto @theme / palette.css / base.css. $anthias-* colours are
    gone from the components too, retiring _variables.scss.
  • Puts every stacking value on a rung of the --z-* scale, order
    preserved.
  • Adds /_design/, a dev-only page rendering every token and component
    variant in both themes, registered behind DEBUG or IS_TEST so it
    cannot exist on a production image.
  • Removes the last Bootstrap values and puts login, _error and
    splash-page on the same stylesheet as everything else, which fixed a
    pre-existing centring bug on the login and error cards.
  • Canvas and chrome stay deep plum in both themes; only surfaces flip,
    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, and
the design page checked against @theme in both directions plus
value-by-value.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

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

vpetersson-bot and others added 7 commits August 16, 2026 08:58
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>
Copilot AI lite review requested due to automatic review settings August 16, 2026 11:06
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner August 16, 2026 11:06
@vpetersson-bot vpetersson-bot self-assigned this Aug 16, 2026
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@97c30a0). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/anthias_server/django_project/settings.py 75.00% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.css with supporting primitives (css/palette.css) and non-@theme globals (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.

Comment thread src/anthias_server/app/static/src/tailwind.css Outdated
Comment thread src/anthias_server/app/design_system.py Outdated
Comment thread src/anthias_server/app/templates/design_system.html
Comment thread src/anthias_server/app/static/src/css/base.css
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>
Copilot AI review requested due to automatic review settings August 16, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-toast tier 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 both dist/css/tailwind.css and dist/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 behind settings.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-top hardcodes 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

  • main hardcodes the nav/footer heights in its min-height calc, but base.css defines --content-min-height derived 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-modal and --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>
Copilot AI review requested due to automatic review settings August 16, 2026 11:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_re regex currently matches the class="..." 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 literal class="..." 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>
Copilot AI review requested due to automatic review settings August 16, 2026 11:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 both dist/css/tailwind.css and dist/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, but app/urls.py registers it when DEBUG or IS_TEST is 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>
Copilot AI review requested due to automatic review settings August 16, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-danger uses --color-danger for its fill, but the token layer explicitly splits --color-danger (ink) from --color-danger-fill (button background). In dark theme --color-danger is lightened, so the button fill will change and can drop contrast for the white label. Use --color-danger-fill for 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-8 is mapped to --color-scrim-6, which makes the name misleading and prevents callers from getting the intended 8% scrim rung. Since --color-scrim-8 is defined in the token layer, wire --surface-scrim-8 to 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.css introduces --content-min-height to centralize the 100vh - nav - footer calculation, but main still hardcodes min-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-height is now declared in css/base.css, but body still hardcodes padding-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>
Copilot AI review requested due to automatic review settings August 16, 2026 12:00
`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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @imports here, or remove the Sass imports).
@import "tailwindcss" source(none);
@import "flatpickr/dist/flatpickr.css";
@import "leaflet/dist/leaflet.css";

Copilot AI review requested due to automatic review settings August 16, 2026 12:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings August 16, 2026 15:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 introduce app-* 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>
Copilot AI review requested due to automatic review settings August 16, 2026 16:11
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @import lines here and keep anthias.scss as the single source).
@import "tailwindcss" source(none);
@import "flatpickr/dist/flatpickr.css";
@import "leaflet/dist/leaflet.css";

@vpetersson
vpetersson merged commit ce2abc7 into Screenly:master Aug 16, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants