A component's appearance is decided by tokens, `solid`, `faded`, `md`, `xs`, `primary`,
rather than by raw values, and every token comes from the same four places in the same order.
That rule is the cascade. Once you know it, a component's props read as a record of which tier
each option stops at instead of a list to memorise.

## The four tiers

`resolveCascade(instance, section, globalDefault, hardFallback)` is the whole rule: it returns
the first argument that states a value, that is, the first that is not `undefined` or `null`.
Components resolve through it, directly for a single axis or through `resolveConfigCascade` for
a group of them. A radius goes through `resolveRadiusKey`, the same rule with `"md"` as the
default value of its fallback argument.

| Order | Tier | Where it is stated | Example |
| --- | --- | --- | --- |
| 1 | The instance | a prop on the element | `<Button radius="full">` |
| 2 | `components.<name>` | the application's component section, with the component's registered default underneath it | `components: { button: { radius: "full" } }` |
| 3 | The global default | `defaultVariant`, `defaultColor`, `defaultRadius` | `defaultRadius: "full"` |
| 4 | The hard fallback | the component's own `FALLBACK_*_CONFIG` | `"md"` |

The first tier that states a value wins and the tiers below it are never consulted.

**Tier 2 is one tier with two sources.** The application's section and the component's
registered default are combined before the cascade runs, with the application's value winning.
So tier 2 is always populated for a registered axis, and populated only by the application for
an unregistered one.

**Tier 3 exists only where the configuration defines one.** On the web that is exactly three
axes: `defaultVariant`, `defaultColor` and `defaultRadius`. A component passes `undefined` at
this tier for every other axis, `size`, `fullWidth`, `animate`, `labelAlign`, `status`, a
layout's `spacing`, and those axes go from tier 2 straight to tier 4.

**Tier 4 is a floor rather than a tier you normally see.** On the three global axes the
provider always resolves a value, so the hard fallback is reached only by an axis that has no
global default; there it is the value the component documents, and the one it renders when no
other tier speaks.

`defaultTheme` is not part of this cascade: it selects a palette, and a palette is not a value
any of these axes resolves. See [Theming](/concepts/theming).

The native renderer resolves the same four tiers, and its default treatment is the same `faded`
as the web's: an axis the global tier answers has to answer the same way on both platforms. Only
the baselines around it differ, the platform configuration adds a `defaultSize`, and its radius
baseline is `md` rather than the web's `xs`. The order is unchanged, and a registered axis still
answers first. See [React Native](/platforms/native).

## What a registered default does

A component's config module registers its defaults when the module is evaluated:

```ts
import { registerComponentDefaults } from "@asheeui/core";

registerComponentDefaults("button", {
  size: "md",
  animate: true,
  fullWidth: false,
});
```

On the web, the `components` object the provider publishes is a getter over that registry
rather than a snapshot: it merges the registered defaults with the application's sections at
the moment a component reads it. That laziness is deliberate. A code-split client can evaluate
a component's module after the provider has mounted, and a snapshot taken at mount would then
disagree with what the server rendered.

Registration fills tier 2. It does not add a tier above the application and it does not lock
the axis: `components: { button: { size: "sm" } }` still beats the registered `md`, because
both are tier 2 and the application's object is applied second.

What registration does change is which tier answers for that axis. A registered axis has
already answered at tier 2 for every instance, so the global default never reaches it. That is
a decision per axis, and three components show the whole shape of it:

| Component | Registers | Leaves to the application | The result |
| --- | --- | --- | --- |
| [Button](/components/forms/button) | `size`, `animate`, `fullWidth` | `variant`, `color`, `radius` | `defaultVariant`, `defaultColor` and `defaultRadius` reach every button |
| [Alert](/components/feedback/alert) | `type`, `radius: "md"` | `variant` | an alert is `md` whatever `defaultRadius` states |
| [Badge](/components/data-display/badge) | `size`, `radius: "full"` | `variant`, `color` | a badge is a pill in a boxy product, unless it is told otherwise |

This is the one thing to check when `defaultRadius` does nothing. A component that registers
`radius` has already answered the tier the application is trying to set, so the global key is
not ignored, it never gets that far. State the axis for that component instead:

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

The field family makes the same point from the other direction. The web's field default
registers `variant: "bordered"`, so an application's `defaultVariant: "solid"` reaches buttons,
badges and cards but not an input, which registers the family's own surface: a control's
treatment is a different decision from a button's, and registering the axis is how the family
says so. See [Field anatomy](/concepts/fields).

## A worked example

A button with no configuration resolves like this on the web:

| Axis | 1 instance | 2 `components.button` | 3 global | 4 fallback | Resolves to |
| --- | --- | --- | --- | --- | --- |
| `variant` | | (not registered) | `faded` | `bordered` | `faded` |
| `color` | | (not registered) | `primary` | `primary` | `primary` |
| `size` | | `md` (registered) | | `md` | `md` |
| `radius` | | (not registered) | `xs` | `md` | `xs` |

So `<Button>Save</Button>` is a `faded`, `primary`, `md` button with `xs` corners: the
framework's boxy default, with the treatment, accent and corners coming from the configuration
and the density from the component's own registration.

Two lines change it for every button in the application:

```ts
import type { ExternalConfig } from "@asheeui/web";

export const config: ExternalConfig = {
  defaultVariant: "solid",
  defaultRadius: "full",
};
```

| Axis | Resolves to | Which tier answered |
| --- | --- | --- |
| `variant` | `solid` | 3, because the button leaves `variant` unregistered |
| `color` | `primary` | 3 |
| `size` | `md` | 2, because the configuration states nothing for it |
| `radius` | `full` | 3, because the button leaves `radius` unregistered |

Nothing about the component changed. `variant` and `radius` were left unregistered so the
application could answer them, and `size` was registered so it could not. The same two lines
leave an `Alert` at `md`, because it registers that radius; a `Badge` registers `full` and
would ignore `defaultRadius: "none"` in the same way.

## When the options are a group

Some components do not expose one axis but a set of options: a hero's `align`, `spacing` and
`background`; a navbar's `position`, `variant` and `align`; a split's `stackAt` and `ratio`.
`resolveConfigCascade(instance, section, fallback)` applies the same precedence to every key
of that set in one call:

- the instance's key wins, then the component section's, then the fallback's;
- there is no global tier, because an option set has no application-wide default;
- only the keys the fallback declares are resolved and returned, so a key the fallback does
  not know is dropped rather than half-applied. A typo in a component section therefore
  cannot render a value nobody designed;
- the instance argument is the component's own props gathered by hand, in `Hero`,
  `{ align, spacing, background, mediaPosition, containerSize, contained }`, so the
  precedence is written once for all six.

The helper is one level deep. A group that nests, the Card's `image`, the sidebar's
`options.active`, the menu options a `Dropdown` passes under `components.dropmenu.menu`, is
resolved one key at a time with `resolveCascade`, which is the same rule applied one level
down. `Image` resolves its five flat options the same way: the helper is a convenience for a
flat set, not a second rule.

## The instance is a decision

The rule compares with `??`, so stating nothing and stating a deliberate value are different
things:

| The instance states | What happens | Why |
| --- | --- | --- |
| `undefined`, or `null` from JavaScript | falls through to the next tier | nothing was stated |
| `false` | stops the cascade | "off" is a decision |
| `0` or `""` | stops the cascade | a zero or an empty string is a value |
| `"ghost"` | stops the cascade | the instance is the first tier |

So `<Button fullWidth={false}>` is narrower than a configured `fullWidth: true`, and
`showSkeleton={false}` turns a picture's placeholder off against a registered `true`. A
component that read its props with `||` would lose both.

The corollary concerns configuration rather than an instance: `undefined` inside a
configuration object is skipped by the merge, so a registered default cannot be removed by
stating nothing. State the value you want, on the component or on the instance.

## Validation

The configuration is validated once per `AsheeUIProvider` mount, while the provider renders, so
a mistake is reported rather than silently replaced by a default:

- an invalid value throws, and the error lists every problem at once. The enumerable axes are
  checked: `defaultTheme`, `defaultVariant`, `defaultColor`, `defaultRadius`, every colour
  value in a theme, a custom theme's `extends`, and the typography section's roles and tokens.
- an unknown key warns in development and is silent in production. A stray key is usually a
  typo, and a typo should not take a running application down.

Two things are deliberately not checked, because the check would be wrong rather than because
they do not matter: a component section's name and its option names. Component defaults
register as their modules are evaluated, so at mount time the registry is incomplete and an
unknown name cannot be told from a not-yet-imported one; and the option names differ per
component (`"card"` is a radio's treatment and not a card's). A configuration typed as
`ExternalConfig` gets TypeScript's help instead, but a JavaScript or JSON configuration does
not: `components.Button` (capitalised) and `components.button.varaint` are both silent, and
both do nothing. [Configuration](/concepts/configuration) states the file format and the
accepted keys.

## Where the resolved value changes

The cascade decides which value is used; it does not decide what the value means. A family
narrows a resolved name before it reaches a class map, and each rule is stated once so both
renderers agree:

- a button with the `underlined` treatment has no rounded corners, so `UnderlineRadius`
  resolves any radius to `none`;
- a message surface has no underlined treatment, so `resolveMessageVariant` turns a global
  `underlined` into `bordered` for an `Alert` or a toast;
- a card, a badge and a chip can draw only some of the shared treatments, so
  `resolveCardVariant`, `resolveBadgeVariant` and `resolveChipVariant` land the rest on the
  nearest surface each one has;
- `resolveVariantClass` has a last resort of `bordered` with the `secondary` accent, for a
  variant and colour pair no dictionary carries.

None of these is a fifth tier. Each runs after resolution, on the one value the four tiers
produced, and each is a statement about what the family can render rather than about where the
value came from.

The tiers are the same for every component on both platforms; what differs is which axes a
component leaves open. Each component page states which axes its own component registers,
[Configuration](/concepts/configuration) states the file and its keys,
[Theming](/concepts/theming) the palette a colour role resolves against, and
[styling](/concepts/styling) how a resolved name becomes a class.
