> ## Documentation Index
> Fetch the complete documentation index at: https://docs.9am.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Theme

> Design tokens, the light and dark palettes, fonts, and the styling conventions that make a script look like a 9AM script.

## How the theme is structured

The theme installs as two stylesheets in `src/styles/`:

| File            | Contents                                                                                              |
| --------------- | ----------------------------------------------------------------------------------------------------- |
| `9am-theme.css` | Tokens for both palettes, the primary ramp, radius scale, the `dark:` variant, and the NUI base rules |
| `9am-fonts.css` | Poppins and Phudu as `@font-face` declarations                                                        |

Both are **locked**. `9am-ui check` fails the build if either is edited locally — identical tokens across scripts is the whole point. Put your own styles in `index.css`, below the imports.

## Dark mode is class-driven

```css theme={null}
@custom-variant dark (&:where(.dark, .dark *));
```

Without this, every `dark:` utility keys off the player's operating system theme, which is neither what you want nor controllable from the UI. Instead the `.dark` class on `<html>` drives everything, and [`useTheme`](/ui-kit/nui#theme) toggles it.

Dark is the shipped default. `index.html` sets the class before first paint so the wrong theme never flashes.

## Palettes

Tokens are authored in OKLCH. Dark ships by default; light is a real second palette, not a tint.

<Tabs>
  <Tab title="Dark">
    ```css theme={null}
    --background: oklch(0.141 0.005 285.823);
    --foreground: oklch(0.985 0 0);
    --card:       oklch(0.21 0.006 285.885);
    --primary:    oklch(0.9089 0.1361 99.99);   /* bright gold */
    --primary-foreground: oklch(0.28 0.07 46);
    --muted-foreground:   oklch(0.705 0.015 286.067);
    --destructive:        oklch(0.704 0.191 22.216);
    --border:     oklch(1 0 0 / 10%);
    ```
  </Tab>

  <Tab title="Light">
    ```css theme={null}
    --background: oklch(0.985 0.002 285.9);
    --foreground: oklch(0.21 0.006 285.885);
    --card:       oklch(1 0 0);
    --primary:    oklch(0.58 0.135 80);         /* deeper amber */
    --primary-foreground: oklch(0.985 0.015 95);
    --muted-foreground:   oklch(0.505 0.014 285.9);
    --destructive:        oklch(0.583 0.222 27.2);
    --border:     oklch(0 0 0 / 10%);
    ```
  </Tab>
</Tabs>

<Note>
  Light mode's `--primary` is deliberately a **deeper** amber than dark mode's bright gold. Bright gold is unreadable as text on white and washes out as a fill. Choosing one deeper gold that works both as `bg-primary` with dark text *and* as `text-primary` on white is why roughly forty `text-primary` usages needed no per-file changes when light mode was added.
</Note>

## The primary ramp

Eleven fixed gold steps, independent of the active palette:

```
primary-50  primary-100  primary-200  primary-300  primary-400  primary-500
primary-600 primary-700  primary-800  primary-900  primary-950
```

Use them as normal Tailwind utilities — `bg-primary-500`, `text-primary-200`, `border-primary-700`.

<Warning>
  The ramp lives in `@theme inline`, which means Tailwind inlines the values into utilities instead of emitting CSS custom properties. `var(--color-primary-500)` resolves to **nothing**. Always use the generated utility, and spell the class out in full so Tailwind's scanner can see it — a template literal like `` `bg-primary-${n}` `` produces no CSS.
</Warning>

## Radius

```css theme={null}
--radius: 0.625rem;
```

`radius-sm` is `--radius - 4px`, `radius-md` is `- 2px`, `radius-lg` equals it, `radius-xl` is `+ 4px`.

In practice: `rounded-2xl` for containers and cards, `rounded-lg` for controls, `rounded-xl` for tiles and avatars.

## Type

| Family  | Weights            | Used for                                                         |
| ------- | ------------------ | ---------------------------------------------------------------- |
| Poppins | 400, 500, 600, 700 | All body copy. Applied globally to `html, body`.                 |
| Phudu   | 600, 700           | Display — headings, page titles, the `navigation` button variant |

Apply Phudu with `!font-[phudu]`:

```jsx theme={null}
<h1 className="!font-[phudu] text-2xl">Vehicle stock</h1>
```

### Choosing a font stylesheet

`9am-theme.css` does not import fonts, because which stylesheet you want depends on your app.

| Option                | Use when                                   | How                                                                   |
| --------------------- | ------------------------------------------ | --------------------------------------------------------------------- |
| `@9am/fonts` (base64) | Single HTML entry — the common case        | `bunx shadcn@latest add @9am/fonts`                                   |
| Linked woff2          | Several HTML entries, especially DUI pages | `bun <9am-ui>/cli/index.ts fonts`, then import `9am-fonts-linked.css` |

Embedding is the better default: one command, nothing else to fetch, and in a NUI everything loads off local disk so there is no network round-trip to save.

It becomes the wrong choice the moment you have multiple entry points. **Every DUI is its own CEF instance**, so each one would decode the entire 322 KB payload again with nothing shared between them. The vehicle shop is the worked example — it has three entries and creates one DUI per showroom slot, so it uses the linked variant.

## Styling conventions

These are what make a screen read as 9AM rather than generic shadcn.

### Translucent fills, not solid greys

```jsx theme={null}
<div className="bg-black/[0.05] dark:bg-white/10 border rounded-xl">
```

This is *the* raised-control surface — buttons, inputs, avatar tiles, list rows. Hairline borders are `border-black/10 dark:border-white/10`.

### Gold tint for accent states

```jsx theme={null}
<div className="hover:bg-[#F6E3711a] hover:text-primary">
```

`#F6E3711a` is gold at 10% opacity. It is the hover and active wash across links, tabs, and badges.

### Press feedback

```jsx theme={null}
<button className="active:scale-95 transition-all">
```

Every clickable element scales down slightly on press.

### Motion

Transitions sit between 150 ms and 300 ms with `easeOut`. Anything slower feels sluggish at the pace players interact with a NUI.

<Warning>
  Never author a colour outside the token set. A raw hex in a component is drift that `9am-ui check` cannot detect, because it only compares files against the registry — it cannot tell that your custom blue was never part of the design language.
</Warning>

## Base rules the theme applies

The theme sets a few NUI-specific defaults you should know about:

* `html`, `body`, and `#root` are **transparent** and `overflow: hidden` — the NUI is an overlay on the game, not a page.
* Focus outlines are suppressed globally. A NUI is pointer-driven, and keyboard focus otherwise draws a ring around whatever was last clicked.
* Images are not draggable and number inputs have no spinner buttons.
* `.scrollbar-none`, `.scrollbar-mini`, and `.scrollbar-mini-absolute` utilities are available.
