# ui-core styles

`@djangocfg/ui-core` owns the semantic CSS token contract used by every
frontend. CSS is the single source of truth for colors, typography, radius,
status surfaces, charts, and sidebar tokens.

## Layout

```text
styles/
├── css/                     # complete CSS layer
│   ├── full.css             # Tailwind v4 + tokens + base + utilities
│   ├── index.css            # unlayered compatibility entry
│   ├── globals.css          # backwards-compatible global entry
│   ├── base.css             # resets and document defaults
│   ├── sources.css          # Tailwind @source directives
│   ├── theme.css            # token/animation aggregator
│   ├── theme/               # tokens, light, dark, animations
│   ├── utilities.css        # ui-core utility aggregator
│   ├── utilities/            # focused utility modules
│   └── presets/              # authoritative static preset token sheets
│       ├── default.css
│       ├── django-cfg.css
│       ├── ios.css
│       ├── macos.css
│       ├── windows.css
│       ├── soft.css
│       ├── dense.css
│       └── high-contrast.css
├── palette/                 # runtime readers of computed CSS variables
└── presets/                  # TypeScript names/order only
    ├── index.ts
    ├── presets.ts
    └── types.ts
```

There are deliberately no TypeScript color maps and no runtime CSS generator.
Do not add a second palette in React or TypeScript.

`styles/presets/` contains only the TypeScript preset names and ordering used by
the public API. The actual values always live in `styles/css/presets/*.css`.

## Consumer contract

Import the golden path first, then exactly one product preset:

```css
@import "@djangocfg/ui-core/styles/full";
@import "@djangocfg/ui-core/styles/presets/macos";
```

The preset export resolves to `src/styles/css/presets/macos.css`; consumers do
not depend on the physical path. The preset defines both `:root` (light) and
`.dark` (dark) values. `ThemeProvider` only controls the `html.dark` class.

For a product-specific adjustment, add a small override after the preset:

```css
:root,
.dark {
  --font-size-base: 0.9375rem;
  --font-size-sm: 0.875rem;
}
```

Do not create a `theme-preset.ts`, append a `<style>` tag, or call a token
builder during application startup.

### Import order

Keep the application entrypoint minimal and deterministic:

```css
@import "@djangocfg/ui-core/styles/full";
@plugin "tailwindcss-animate";
@import "@djangocfg/ui-core/styles/presets/macos";
@import "@djangocfg/layouts/styles";
@import "@djangocfg/ui-tools/styles";
```

`full.css` already imports Tailwind, the theme tokens, base styles, and
ui-core utilities. Do not add a second `@import "tailwindcss"`, `base.css`,
or `utilities.css` in the consumer. Load the preset after `full.css`, so its
`:root` and `.dark` variables win over defaults. Keep application overrides
after the preset.

The `tailwindcss-animate` plugin is registered once by the consumer. It
provides classes such as `animate-in`, `fade-in-0`, and `zoom-in-95`; the
package's component source uses those classes but does not register the
plugin itself.

### Tailwind source scanning

`css/sources.css` is part of the `full` entry and must scan the package source
root from its own location:

```css
@source "../../**/*.{ts,tsx}";
```

This is important. Scanning only `../` scans `src/styles` and silently drops
utilities used by components, including `bg-overlay` and state/animation
utilities. A consumer may add extra `@source` directives for local packages,
but should not replace the ui-core source directive.

### Overlays and glass surfaces

Dialog, alert-dialog, sheet, and drawer backdrops use the semantic
`bg-overlay` class. The class gets its color from `--overlay` and its frosting
from `--overlay-blur` (the default is `5px`):

```css
.bg-overlay {
  background: var(--overlay);
  backdrop-filter: blur(var(--overlay-blur));
}
```

Glass surfaces must have a translucent background; an opaque `bg-card` or
`bg-popover` makes the blur invisible. A dropdown rendered inside an ancestor
that already has `backdrop-filter` is also unable to blur the page behind that
ancestor because it becomes a backdrop root. PublicLayout desktop dropdowns
are therefore rendered through a body portal and positioned from the trigger.
Use the same pattern for new floating surfaces that must blur content outside
their parent stacking context.

For custom glass utilities, prefer a Tailwind utility such as
`backdrop-blur-[5px]` when the consuming build must emit the standard
`backdrop-filter` declaration. Keep the visual token (`--overlay-blur`) in
CSS rather than duplicating a pixel value in React.

## Token format

Color variables are complete CSS colors, never bare HSL triplets:

```css
:root {
  --background: hsl(240 17% 97%);
}
.dark {
  --background: hsl(240 5% 8%);
}
```

Tailwind maps semantic utilities to these variables through `@theme inline`.
Use `var(--background)` or `bg-background`; do not write
`hsl(var(--background))`.

Typography variables are ordinary CSS values:

```css
:root,
.dark {
  --font-size-base: 0.8125rem;
  --font-size-sm: 0.75rem;
}
```

## Preset rules

- Presets are authored directly in `styles/css/presets/`.
- Every preset must define valid `hsl(...)`, `color-mix(...)`, or other complete
  CSS color values.
- Keep light and dark pairs together in the same file.
- A preset may omit tokens that intentionally inherit from `theme.css`.
- In light mode, `--muted` must be at least 2 HSL lightness points darker
  than `--background`. This keeps translucent muted fills visible on the
  canvas and is enforced by `check:contrast`.
- Changes to a preset require checking both light and dark modes.
- Product density overrides belong in the consuming app's CSS, not in a copied
  TypeScript map.

## Verification

```bash
pnpm check:contrast
pnpm check
```

`check:contrast` merges every static preset over the base theme and validates
the light-mode background/muted surface hierarchy. It also keeps the complete
macOS compact-text audit: semantic pairs must reach WCAG AA 4.5:1 in both
modes, including primary, destructive, sidebar, status-surface, on-fill, and
muted text against every common surface. When tuning a token, change the
semantic pair together rather than overriding text in a consumer component.

At the consumer, build the app that imports the preset and verify both modes.
The important invariant is that changing a preset requires editing one CSS
file, and no React runtime code is involved in applying it.

For a local cross-repository change, use the consumer's sync command and
restart its dev server afterward:

```bash
pnpm sync:cfg:one ui-core
pnpm sync:cfg:one layouts
pnpm sync:cfg:one i18n       # required peer when ui-core is synced locally
rm -rf apps/web/.next
```

Do not delete `pnpm-lock.yaml` to force a package update. Update the package
range with pnpm, then let the lockfile record the resolved version. A clean
install restores published packages and therefore requires publishing a new
package version for source changes to survive.
