These are five failures this codebase has produced, in the order they usually bite: one on
the way in, two about configuration, and two about a component that looked as though it should
have worked. Each entry is the symptom in your words, the cause, and the fix. Anything not here
is a component page away: [Components](/components) is the index.

## Everything is unstyled, or a component is missing its padding

**What you see.** Two shapes of one failure. Either the whole page is unstyled, no framework
colours, no spacing, no borders, or almost everything is right and one control is not: a
`Button` whose label touches its edges, an `Alert` with no padding, a card with its content
against the border. In the second shape the class is on the element and the rule is not in the
stylesheet. Inspect the button, read `px-3.5` off its class list, and search the generated CSS
for `.px-3\.5`: nothing.

**Why.** Two independent halves of one import.

- The stylesheet has to come *after* the Tailwind import: it extends the Tailwind theme with
  the framework's tokens (`--color-background: var(--ashee-background)` and the rest) and
  declares the layer order the two files are read in, so an import above Tailwind's own is the
  first thing to check when the page has no framework colours at all.
- Some of the class vocabulary is not in `@asheeui/web`. The radius and size vocabularies and
  most of the components' `*-styles.ts` modules are in `@asheeui/core`, and the stylesheet
  tells Tailwind to scan both packages with four patterns:

```css
@source "./**/*.{ts,tsx}";
@source "./**/*.js";
@source "../../core/src/**/*.{ts,tsx}";
@source "../../core/dist/**/*.js";
```

A build that scans the application and `@asheeui/web` but never `@asheeui/core` generates the
classes it can see and silently omits the rest. That is why the class is on the element and
the rule is missing: the padding of a default button is stated in
`packages/core/src/components/forms/button/button-styles.ts`, not in the renderer.

**What to do.** Import the package's stylesheet rather than pasting its tokens into your own
CSS, after the Tailwind import:

```css
@import "tailwindcss";
@import "@asheeui/web/styles";
```

If your build states its own content sources, a `source(none)` import, or an explicit list,
then the last two patterns above are yours to restate against your `node_modules`: the
package's lines are followed because the package's stylesheet is the file Tailwind reads, and
a copy of the tokens loses them. Rebuild and search the generated CSS for the class you saw on
the element; if it is still absent, the source list is still the answer.
[Installation](/getting-started/installation) states the import order once.

## My custom theme falls back to light

**What you see.** `color` declares the theme, `defaultTheme` names it, and the page paints the
built-in light palette. One line in the console separates the two faults:
`document.documentElement.className`. If it says `theme-light` or `theme-dark`, the theme was
never selected. If it says `theme-midnight`, the selection worked and the palette did not.

**Why.**

- The controller is one instance for the whole application, and every `AsheeUIProvider`
  configures it. The theme *list* only ever grows, a provider below the application's can add
  a theme but never take one away
  (`packages/web/src/theme/controller.ts`), but `defaultTheme` is replaced by whichever
  provider configured last, and with nothing stored under `ashee-theme` the selection resolves
  again from that. A second provider that mounts *after* the application's (a panel, a route, a
  demo opened on demand) drops your `defaultTheme` back to `system`, which is light on a light
  machine. A nested provider in the same commit is harmless: React runs a child's effects
  first, so the application's provider has the last word.
- With the right class and the wrong colours, the palette is the fault. A custom theme
  inherits the palette its `extends` names, and a name that is not in the map at that moment
  inherits the library's `light` instead
  (`packages/core/src/config/resolve-color.ts` falls back to `defaults.light`). That covers a
  typo, and it covers a theme that extends another custom theme written *later* in the same
  `color` object: custom themes resolve in the order they are written, and only `light` and
  `dark` are merged before them.

**What to do.** Keep one provider, at the root. If a second one is unavoidable, give it the
same `color` object and an explicit `defaultTheme`. For the palette, declare `extends` as
`"light"` or `"dark"`, or write the base theme before the theme that extends it. A theme
chosen with `useTheme().setTheme` is stored under `ashee-theme` and wins over `defaultTheme`
on later visits, so a fault that disappears after picking the theme by hand is the first cause
above. [Theming](/concepts/theming) has the mechanism and the API.

## Configuration has no effect on one component

**What you see.** `defaultRadius: "lg"` rounds most of the product and one component ignores
it: every `Alert` stays at `md` however the application is configured. Setting
`components.alert.radius` works, which is the clue.

**Why.** The cascade has four tiers, and the first one that answers wins: instance props, then
`components.<name>`, then the global default, then the built-in fallback. A component that
states an axis in its registration has already answered the second tier, for every instance,
before the application says anything.
`packages/web/src/components/feedback/alert/alert-config.ts` registers `radius: "md"`, so
`components.alert.radius` is populated and `config.defaultRadius` is never consulted.
`packages/web/src/components/forms/button/button-config.ts` is the other case: it registers
`size`, `animate` and `fullWidth` and deliberately leaves `variant`, `color` and `radius` out,
so the application's `defaultVariant`, `defaultColor` and `defaultRadius` reach every button.
The registration is the component's design decision, an alert is `md`, not a fault in the
merge: `packages/core/src/config/resolve-config.ts` merges that registry as the base of the
`components` tier.

**What to do.** State the axis for that component:

```tsx
<AsheeUIProvider config={{ components: { alert: { radius: "lg" } } }}>
  <App />
</AsheeUIProvider>
```

The global key is the floor for the components that leave an axis unregistered; the
per-component key is the lever for one that pins it. To tell which you are looking at, open the
component's config module and read the object passed to `registerComponentDefaults`: a key in
that object will not be reached by the global default.
[Configuration](/concepts/configuration) is the cascade in full.

## A field never opens the on-screen keyboard

**What you see.** You focus or tap a field and nothing appears at the foot of the window. There
is no warning and no error, the field types with the real keyboard, exactly as it would
without the framework, and the on-screen keyboard never shows up anywhere in the page.

**Why.** `enableVirtualKeyboard` defaults to `true`, and on its own that means nothing: the
field asks a context for the keyboard, and with no `KeyboardProvider` above it there is no
context to ask. `packages/web/src/components/system/keyboard/KeyboardProvider.tsx` owns the
open state and renders the keyboard, and
`packages/web/src/components/system/keyboard/use-keyboard-field.ts` returns early when that
context is absent, which is why the failure is silent rather than reported. A keyboard
rendered in a portal at the end of the document is easy to look for in the wrong place, too:
it will not appear inside your form's markup.

**What to do.** Mount a provider above the fields, inside `AsheeUIProvider` because it reads
the application's configuration:

```tsx
import { Input, KeyboardProvider } from "@asheeui/web";

<KeyboardProvider>
  <Input label="Recipient" />
</KeyboardProvider>;
```

One provider around a form, a screen or the whole application is enough; a page opts in once
and switches individual fields out with `enableVirtualKeyboard={false}`. A nested provider owns
the keyboard for its own subtree, which is how a group of fields gets a different set of keys.
For a field that is not one of the framework's, `useKeyboardField` returns `hasProvider`, the
difference between a field that could open a keyboard and one that cannot, so the affordance
can be drawn only where it works.
[Keyboard](/components/system/keyboard) covers the layouts, the keys and the configuration.

## An overlay cannot be dismissed, or a page stops scrolling

**What you see.** A dialog is open, pressing <kbd>Tab</kbd> cycles focus round inside it, the page
behind will not scroll, and neither <kbd>Escape</kbd>, nor a press on the dimmed backdrop, nor the
close control in the corner appears to do anything, the only way out is a reload, or whatever
else in the application sets the state that opened it.

**Why.** The component applied its scroll lock and its focus trap, so as far as it is concerned
the layer *can* close: it was given an `onClose`. What that callback does is the other half, and
the component cannot check it. If it closes a different piece of state, or does nothing while
`isOpen` stays true, every dismissal gesture succeeds and the page does not move, the lock
follows `isOpen`, not the gesture (`packages/web/src/libs/use-body-scroll-lock.ts`), and focus
stays trapped for the same reason.

Two failures that used to look like this one are fixed, so rule them out before looking above:

- A layer mounted with no `onClose` used to trap the page: the lock and the trap were applied
  anyway and no gesture was wired. It is now presented non-modally, no scroll lock, no focus
  trap, no `aria-modal`, and logs a warning that names the missing prop, so that case announces
  itself in the console instead.
- One press of <kbd>Escape</kbd> used to close every open layer, because each layer listened on
  the window and none asked which was on top. <kbd>Escape</kbd> is now answered by the topmost
  layer only, through a shared stack (`packages/web/src/libs/layer-stack.ts`), so a modal opened
  over a drawer closes alone.

**What to do.** Read `onClose`: it has to set `isOpen` to false, or dispatch the state change that
does. When a layer must not be dismissed mid-request, keep `onClose` and let that callback refuse
while the request is pending rather than dropping the prop; turn both switches off so the only way
out is the control you choose, the built-in close control, or a cancel button in the panel. If
the page cannot scroll after everything looks closed, find the `<Modal isOpen>` or
`<Drawer isOpen>` still mounted: the lock follows `isOpen`, not dismissal.
[Focus and dismissal](/concepts/focus-and-dismissal) is the contract these two layers share, and
[Modal](/components/overlays/modal) states what the modal adds to it.
