Skip to content

Architecture

src/abilian_ds/
├── css/
│   ├── ads.css          # the entry sheet: @imports everything below, in cascade order
│   ├── knobs.css        # 27 overridable values
│   ├── derive.css       # 128 values computed from them
│   ├── base.css         # reset and element defaults
│   └── parts/           # one file per component family
├── templates/ads/
│   └── form.html        # the Jinja macros
├── situ.css             # the situ_ui token bridge
├── lint.py              # the checks
├── tokens.py            # TOKENS.md generator
├── cli.py               # sync / lint / tokens
└── allowlist.txt        # classes set by libraries at runtime

Cascade order is the architecture

ads.css is the only place the order is written down, and the order is the cascade:

  1. knobs — the overridable surface, first so a project can override them
  2. derive — everything computed, second so it sees the knobs
  3. base — the reset
  4. parts, alphabetically — a part never depends on another part, only on tokens, so the order among them means nothing
  5. layout.css — last, deliberately, because its classes are applied to components and both selectors are one class
  6. forced-colors.css — last of all

Why this file exists

The parts used to be enumerated in every page template — four lists that had to be edited in step, and they had drifted: the auth shell hand-picked a subset, so a component was styled on one page and not the next. An order that lives in five places is not an order.

The linter follows @import, so the sheet that lists its parts is its parts. Listing them again in the linter's config was the same order written twice, and the copy that rots is the one no page loads.

Two layers, one direction

Knobs are read by derivations; derivations are read by parts; parts read nothing else. There is no path back — a part cannot set a knob, and nothing in parts/ should contain a literal colour, length or radius. That is checkable by eye and is the first thing to look for in a review.

Why plain CSS

No build step, no PostCSS, no node_modules. The features that would have justified a build — nesting, custom properties, color-mix(), light-dark(), :has(), logical properties, container-independent min()/clamp() — are all in browsers now. A build step buys nothing here and costs a toolchain in every consuming project.

The one thing it would buy is dead-code elimination, and the linter's unused-class rule covers that better: it reports a class the stylesheet defines that no template names, which is a design smell rather than a bundle-size problem.

The guaranteed-invalid trick

A few knobs need to be able to switch a whole feature off. There is no if in CSS, but there is a useful property: a custom property whose value cannot be parsed makes every calc() that reads it invalid at computed-value time, so the property falls back to its initial value rather than being ignored. Combined with multiplicative derivations, that is what lets --ads-shadow-strength: 0 produce a genuinely flat design without a branch in every component.

Where the shell lives

Today the application shell (base.html, the topbar, the four layouts' markup) lives in the reference application, not in the package. The CSS for it is packaged; the markup is not.

That is a known gap rather than a decision, and it is the first thing a second application will force. See the roadmap in About.