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:
- knobs — the overridable surface, first so a project can override them
- derive — everything computed, second so it sees the knobs
- base — the reset
- parts, alphabetically — a part never depends on another part, only on tokens, so the order among them means nothing
layout.css— last, deliberately, because its classes are applied to components and both selectors are one classforced-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.