# CSS system (`devplacepy/static/css/`)

This file documents the CSS conventions for every stylesheet in this directory. Claude Code auto-loads it when a file under `static/css/` is read or edited. The member-facing style guide lives at `/docs/styles*.html`; keep both in sync when a convention changes.

## Tokens (`variables.css` is the single source of truth)

- **Never hardcode a themable value.** Colours, spacing (`--space-xs`...`--space-2xl`), radii, shadows, fonts, layout measures, and z-indexes are always `var(--token)` references. A missing value means adding a token to `variables.css`, not inlining a literal.
- **Never give a global token a fallback.** `var(--accent)`, never `var(--accent, #hex)`. A fallback silently masks a dead or misspelled token - the page keeps rendering from a value nobody maintains. This was a real bug class: `var(--topic-discussion, #ef4444)`, `var(--bg-code, #1a1a2e)`, `var(--accent, #6366f1)` referenced tokens that never existed and shipped a foreign palette via their fallbacks.
- **Accent tints compose the channel token:** `rgba(var(--accent-rgb), 0.3)`, never a literal `rgba(255, 107, 53, ...)`. The raw accent literal exists only at its definition in `variables.css`.
- **Status is semantic:** `--success`/`--warning`/`--danger`/`--info` for state, `--topic-*` only for topic badges. Never repurpose a topic token as a status colour or vice versa.
- **One name per value.** Do not add alias tokens (`--space-base` and `--shadow-md` were removed for duplicating `--space-sm` and `--shadow`).

## File-scoped palettes

A feature with its own semantic palette defines it ONCE as a custom-property block at the top of its own stylesheet and references only those properties below: `devii.css` scopes `--devii-*` on `devii-terminal`; `isslop.css` scopes `--isslop-*` (solid + `-rgb` channel pairs for alpha composition) on `:root` (the file loads only on its own pages). Raw hex values may exist in exactly two places: `variables.css` and these blocks. A repeated hex or `rgba()` literal below a token block is a defect.

## Stacking (`--z-*` tokens, two bands)

Every `z-index` is a token from `variables.css`; literal values are forbidden except small local ladders (`z-index: 1`/`2`) inside an already-stacked component.

| Band | Order (bottom to top) |
|------|-----------------------|
| Content | `--z-fab` 900, `--z-nav-overlay` 998, `--z-nav-panel` 999, `--z-nav` 1000, `--z-nav-drop` 1001, `--z-modal` 2000, `--z-popover` 2100, `--z-lightbox` 10000 |
| Chrome | `--z-chrome-fw` 2147480000, `--z-chrome-devii` 2147483000, `--z-chrome-toast` 2147483400, `--z-chrome-menu` 2147483500, `--z-chrome-dialog` 2147483647 |

The chrome band is **user-CSS-proof by design**: per-user injected CSS (the customization feature) must never stack above floating windows, the Devii terminal, toasts, the context menu, or confirm dialogs, so they live at the top of the integer range AND carry `will-change: transform` (the layer-promotion group in `base.css` - any new always-on fixed chrome element must join both). Toasts sit in the chrome band deliberately: they fire over open modals and stay clickable. The Devii tutorial-overlay ladder (2147483600-602 in `devii.css`) is an intentional local sequence between `--z-chrome-menu` and the devii avatar. A new stacked element picks the matching token or adds one between two stops in `variables.css` - never a number at the use site.

## Breakpoints (exact, closed set)

`@media (max-width: 1024px | 768px | 480px | 360px)` plus the capability/preference queries `(hover: none) and (pointer: coarse)` and `(prefers-reduced-motion: reduce)`. Nothing else. The historical one-offs (900/720/640/600/560/520) were consolidated by snapping each UP to the next canonical stop - snap-up is the required direction because collapsing earlier can never overflow, while snapping down leaves a viewport band the compact styles no longer protect. Element `max-width` values (content measures like `max-width: 720px` on an article column) are not breakpoints and are unaffected by this rule.

## Reduced motion

One global rule at the end of `base.css` collapses every animation/transition to `0.01ms` under `prefers-reduced-motion: reduce`. Its four `!important`s are the documented exception to the no-`!important` rule: a zero-specificity `*` selector cannot otherwise beat any class rule, and per-rule opt-outs would need editing every declaration. Durations are near-zero rather than `none` so JS `transitionend`/`animationend` handlers still fire. Pages add their own reduced-motion query ONLY when content must change (e.g. `deepsearch.css` stopping an indeterminate bar), never to re-disable motion.

## Layout rules

- Flexbox + CSS Grid with `gap` only; floats for layout are forbidden.
- Approved page layouts and the shared shell are documented in `/docs/styles-layout.html`; a page supplies exactly one layout container inside `.page`.
- Every fluid grid/flex column sets `min-width: 0`, and every fluid grid track that holds content is written `minmax(0, 1fr)`, never a bare `1fr`. A `1fr` track's automatic minimum is the item's min-content size, so one unwrappable line inside a rendered code block (`.rendered-content pre`, `white-space: pre`) widens the track and blows the whole page open sideways. This is why a detail page (a `max-width` block, definite width, the `pre` scrolls inside it) survives content that destroys a listing grid. The rule covers a column that is fixed-width at desktop but becomes the single fluid column at a breakpoint (`.profile-sidebar`), and it is regression-tested by `assert_no_horizontal_overflow` in `tests/conftest.py`.
- `!important` is allowed only for: the `.hidden`/`[hidden]` display utilities, the global reduced-motion rule, and the devii-avatar third-party-beating override. Anything else is a specificity problem to be fixed structurally.
- Page-specific CSS lives in its own `static/css/*.css` loaded via `{% block extra_head %}`, never an inline `<style>` block; shared component styles live once (`components.css`, `feed.css` vote buttons) and are never redefined per page.
