# CSS architecture ## File layout ``` static/src/css/ ├── website_sale.css # index only: @import list, no rules of its own ├── base/ │ ├── variables.css # :root tokens — the ONLY file with hex literals │ └── utilities.css # tiny single-purpose helpers, when Bootstrap lacks one ├── layout/ │ ├── pages.css # page shells, containers, backgrounds │ └── header.css ├── components/ # one file per reusable component │ ├── product-card.css │ ├── order-card.css │ ├── cart.css │ ├── buttons.css │ ├── quantity-control.css │ ├── forms.css │ ├── alerts.css │ └── tag-filter.css └── sections/ # page-specific composition only ├── products-grid.css ├── order-list.css ├── checkout.css └── info-cards.css ``` Rules of placement: - A rule that could appear on two pages is a **component**, never a section. - A **section** file may only position and space components — if it restyles a component's internals, the component is missing a modifier. - Each component file owns its own media queries, at the bottom. There is no global responsive file in a healthy tree; a legacy `layout/responsive.css` should shrink to zero as components are touched. - `website_sale.css` contains imports and comments only. ## Naming Prefix everything with `eskaera-` (the product name already used for page-level classes). Unprefixed names like `.product-card`, `.cart-header`, `.qty-control` sit in the same namespace as Bootstrap, Odoo core and every other installed addon — that collision is what forces `!important` later. ``` .eskaera-product-card /* block */ .eskaera-product-card__title /* element */ .eskaera-product-card--out-of-stock /* modifier */ .is-loading .is-active .has-error /* state, always paired with a block class */ ``` `js-` prefix for hooks JS queries, and never style a `js-` class — that way markup can be restyled without breaking behaviour, and behaviour can move without breaking styles. When renaming existing classes, do it component by component: template + CSS + JS selectors + tests in the same commit, then grep for the old name across `views/`, `static/src/`, and `README*`. ## Specificity — how to stop writing `!important` Overriding Bootstrap globally (`.list-group-item { … }`, `.alert-warning { … }`) is a fight you win only with `!important`, and it leaks into every other addon. Scope the override under the page root instead: equal specificity plus one class always wins, no escape hatch needed. ```css /* ❌ global override, needs !important to beat Bootstrap's own later rules */ .list-group-item { border-radius: 0 !important; } /* ✅ scoped: higher specificity, no !important, no leakage */ .eskaera-checkout-page .list-group-item { border-radius: 0; } ``` Other rules: - Max **3** levels of nesting in a selector. Deeper means the markup is doing the work the class should do. - No ID selectors for styling; IDs are for anchors and `aria-*` references. - No element selectors outside `base/` (`div.product-card` locks the markup). - `!important` is allowed in exactly two places: the `prefers-reduced-motion` reset, and a documented override of a core rule you cannot scope — with a comment saying which rule and why. ## Ordering inside a file ```css .eskaera-product-card { /* 1. layout */ display: flex; flex-direction: column; /* 2. box */ padding: var(--ac-space-md); border: 1px solid var(--ac-border-light); border-radius: var(--ac-radius-lg); /* 3. typography */ font-size: var(--ac-text-base); color: var(--ac-text-primary); /* 4. paint & motion */ background: var(--ac-surface); box-shadow: var(--ac-shadow-sm); transition: box-shadow var(--ac-transition-fast); } ``` Then, in order: `:hover` (inside `@media (hover: hover)`), `:focus-visible`, `:active`, `:disabled`, state classes, modifiers, media queries. ## Tokens Full rules in `SKILL.md` §2. In short: ```css :root { /* alias Odoo/Bootstrap's unprefixed custom properties so theming still works */ --ac-color-primary: var(--primary, #007bff); --ac-color-danger: var(--danger, #dc3545); --ac-surface: var(--body-bg, #fff); --ac-text-primary: var(--body-color, #1a202c); --ac-border-light: var(--border-color, #e2e8f0); /* own scales */ --ac-space-xs: 0.25rem; --ac-space-sm: 0.5rem; --ac-space-md: 1rem; --ac-space-lg: 1.5rem; --ac-space-xl: 2rem; --ac-radius-sm: 0.25rem; --ac-radius-md: 0.5rem; --ac-radius-lg: 0.75rem; --ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%); --ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%); --ac-transition-fast: 200ms ease; } ``` Migrating a legacy token set: add the `--ac-` token aliased to the old name, move consumers file by file, delete the old token when `grep -rn "\-\-old-name" static/src/` is empty. Don't rename all at once. ## Anti-patterns seen in Odoo addons | Smell | Fix | | --- | --- | | `!important` to beat Bootstrap | scope under the page root class | | Same colour as `#28a745` in six files | one token in `variables.css` | | `responsive.css` overriding twelve components | move each query into its component | | `style="margin-top:20px"` in QWeb | utility class or component spacing | | `.card .body .row .col span` | give the span a class | | `px` for font sizes | `rem`, so browser zoom and user font size work | | Colour hardcoded instead of `var(--primary)` | breaks the website theme editor silently | | Both `@import` chain **and** manifest entries | double download; pick one (see SKILL.md §6) | ## Formatting Prettier handles `.css` via pre-commit: `printWidth: 100`, `tabWidth: 4`. Run `pre-commit run --all-files` (or `make format`) before committing; never hand-align properties, it will be reformatted.