> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/header-alert.html
> Source: specs/components/header-alert/header-alert.md, specs/components/header-alert/header-alert-a11y.md, specs/components/header-alert/capabilities.json

---

# HeaderAlert · Oymyakon DS 3

> A page-level status banner pinned to the top of the screen, announcing a blocking or critical
> state with a centered title + subtitle and an optional close control.

**Version:** 3.0.7 · **Status:** Ready

---

## 1. Description

HeaderAlert is a full-width status banner that occupies the very top of a screen — it spans the
system status-bar area and sits above the app's own header. It is used to announce a **critical,
screen-level state** the user must notice immediately: a lost connection, a failed payment, an
account restriction, a trip problem. The saturated `Accent/Red 2` fill and fixed-light text make
it read as an alert at a glance.

Unlike an inline **Banner** (which flows in page content) or a **Toast** (which auto-dismisses),
HeaderAlert is anchored to the top chrome and stays until the state resolves or the user closes
it. It carries a short title and an optional supporting subtitle, centered so the message reads as
a system-level announcement rather than a list row.

It ships in three status tones — **Error**, **Warning**, **Success** — each a single accent fill
(`Accent/Red 2`, `Accent/Orange 2`, `Accent/Green 2`). Tone is a first-class variant (`Style`) of
the component; the structure and every other token are identical across tones — only the fill
changes. The banner also has an **RTL** axis (`RTL: Off / On`): RTL mirrors the horizontal slot
order and the reserved safe-area layout for right-to-left locales.

## 2. Anatomy

Root is a full-bleed vertical container filled with the tone's accent — edge to edge, with no
corner radius. It **reserves the top safe-area** — a blank zone the size of the system status bar
(`env(safe-area-inset-top)`, fallback `s44`) — so the accent fill runs under the OS status bar. The
component does **not draw a status bar**; it only leaves room for the one the platform paints on
top. (In Figma a mock status bar sits in this zone as a designer aid to visualize the safe-area —
it is not part of the component contract.) The message sits in the **Content** row, below the
reserved zone.

```
HeaderAlert                         root — full-bleed, tone fill, top safe-area reserve, content below
└── Content                         horizontal row, padding s8, top-aligned
      ├── leading-spacer            40×40 spacer — mirrors the end slot to keep the text optically centered (shown only when close is on)
      ├── center-slot               vertical, gap s4, min-height s40, content centered in s40 — the message (required)
      │     ├── title               Heading4 (required)
      │     └── subtitle            CompactBody, max 2 lines, grows downward (optional, hidden by default)
      └── end-slot                  40×40 close icon button, top-anchored — stays put as subtitle grows (optional, off by default)
```

- **root** — full-bleed (no corner radius). Top padding is the safe-area inset
  (`env(safe-area-inset-top)`, fallback `s44`) — a reserved zone, not rendered content, so the fill
  extends under the system status bar. The inset is platform-driven: iOS ≈ s44, Android varies.
- **leading-spacer** — a 40-wide empty slot that mirrors the end slot so the title/subtitle stay
  optically centered. Shown together with the close control; when close is off there is no end slot
  to balance, so the spacer is hidden and the message centers on its own.
- **center-slot** — required. Vertical stack of title + subtitle, gap `s4`, with a **`min-height` of
  `s40`**; its content is **center-aligned within that `s40`** box, so a lone title sits centered. When
  the subtitle is shown, title (20) + gap `s4` + subtitle (16) fills the `s40` exactly; a subtitle that
  wraps to a 2nd line grows the slot **downward** below `s40`. The side slots are anchored independently
  (see end-slot), so they never move as the center-slot grows.
- **title** — required. Single line, Heading4. The default state shows the title only.
- **subtitle** — optional, **hidden by default**. Supporting line, CompactBody, **clamped to a
  maximum of 2 lines**; overflow is truncated with an ellipsis.
- **end-slot** — optional, **off by default** (`EndSlot` boolean). A fixed **40 × 40** slot — a
  component-owned icon button (`.header-alert__close`) with the `close` icon (24 × 24) —
  **top-anchored** to the top of the Content row (below the
  `s8` top padding). Because it is top-anchored — not centered on the message — it **stays in exactly
  the same position** whether the subtitle is hidden, one line, or two lines: a growing subtitle never
  pushes the close down. The leading-spacer mirrors it (same fixed 40 × 40, top-anchored). When off,
  the slot and its balancing leading-spacer are both removed.

### RTL layout

RTL is a variant axis (`RTL: Off / On`), off by default and opt-in per instance. When on, the
**Content row reverses**: the horizontal order becomes end-slot → center-slot → leading-spacer
(mirror of LTR), and the reserved status-bar safe-area content mirrors to the opposite side. The
title/subtitle stay center-aligned; only the horizontal arrangement of the side slots flips. The
close icon is a directional-neutral glyph and is not itself mirrored. Web: opt in with `dir="rtl"`
on the component root (or the `.header-alert--rtl` modifier), which reverses the Content row.

## 3. Variants and sizes

HeaderAlert has a single size (full-width, fixed content height). Its variant axes are **Style**
(the tone) and **RTL** (layout direction); the close control and subtitle are independent boolean
options.

| Parameter | Type | Values |
|---|---|---|
| Style | variant | Error (`Accent/Red 2`, default) · Warning (`Accent/Orange 2`) · Success (`Accent/Green 2`) |
| RTL | variant | Off (default) · On |
| Close control (`EndSlot`) | boolean | Off (default) · On |
| Subtitle | boolean | Hidden (default) · Shown |
| Width | — | Fill (full screen width) |

Each Style is a single fill swap — structure, typography, spacing, and the `AlwaysLight` text/icon
color are identical across tones. CSS: `.header-alert--error` (base) / `.header-alert--warning` /
`.header-alert--success`. RTL is opt-in per instance (see § 5).

> Info (`Accent/Blue 2`) is **not** part of this release. Do not invent tone tokens — add a tone
> only against an existing `Accent/*` token and an owner decision.

## 4. States

The first / rest state is named **Standard**. HeaderAlert is a status container; its only
interactive element is the optional close control.

| State | Description |
|---|---|
| Standard | Banner visible at rest — tone fill, title only by default (subtitle and close are opt-in). |
| Close pressed | The close control is pressed — press feedback on the close button itself, timed by `--ha-pressed` / `--ha-released` (see § 5). The banner fill is unchanged. |
| Entering | Banner slides down from behind the status bar (see § 5). |
| Exiting | Banner slides back up off the top edge when dismissed or the state resolves. |

The banner itself has no disabled or loading state. The close control is a component-owned
button (`.header-alert__close`), not an `IconContainer` instance — and per `icon-container.md`
§ 5 press states always belong to the parent component, which here is HeaderAlert.

## 5. Animation and behavior

Reference: [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md). No hardcoded easing or durations.

HeaderAlert enters and leaves off-screen from the top edge, as a page-level surface. It slides
down from behind the status bar on appear and slides straight back up on dismiss — the exit starts
the moment the close control is released. The close control's press feedback is HeaderAlert-owned,
timed by `--ha-pressed` / `--ha-released` (`Transforming/Pressed` / `Released`); like enter/exit,
the component CSS ships the timing props, not state wiring — the integrator drives the transition
(the preview demo illustrates it with a scale dip on the close).

| Event | Pattern | Token | CSS |
|---|---|---|---|
| Appear | `Transforming/Enter/Default` | `--transforming-enter-default` | `transform: translateY(-100% → 0)` (250ms) |
| Hide | Exit (medium) | `duration.medium1` + exit curve | `transform: translateY(0 → -100%)` (250ms) |
| Close press | `Transforming/Pressed` → `Released` | `--ha-pressed` / `--ha-released` | press feedback on the close button (integrator-driven) |

Only the compositor-friendly `transform` property is animated. The banner never animates layout,
color, or height for enter/exit.

```css
@media (prefers-reduced-motion: reduce) {
  /* --ha-enter / --ha-exit / --ha-pressed / --ha-released resolve to 0ms —
     the banner appears/leaves instantly (override in shared.css :root) */
}
```

## 6. Color tokens

Every value is a semantic token. The fill is the tone's `Accent/* 2`; all text and the close icon
are `TextAndIcon/AlwaysLight`, which stays light in both themes so it reads on the accent fill
without inverting.

**Background fill by tone:**

| Tone | Token | Light | Dark |
|---|---|---|---|
| Error | `Accent/Red 2` (`--accent-red2`) | Red/700 | Red/600 |
| Warning | `Accent/Orange 2` (`--accent-orange2`) | Orange/700 | Orange/600 |
| Success | `Accent/Green 2` (`--accent-green2`) | Green/700 | Green/600 |

**Text & icon (all tones, both themes):**

| Element | Token |
|---|---|
| Title | `TextAndIcon/AlwaysLight` |
| Subtitle | `TextAndIcon/AlwaysLight` |
| Close icon | `TextAndIcon/AlwaysLight` |

> `AlwaysLight` (not `InversePrimary`) is deliberate: the fill is a fixed accent that does not
> invert per theme, so the text must stay light in both.

## 7. Typography

| Element | Style | Lines |
|---|---|---|
| Title | Heading 4 (Suisse Intl · Semibold · 17px) | 1 |
| Subtitle | Compact Body (Suisse Intl · Book · 14px) | max 2 (ellipsis on overflow) |

Both are centered within the center-slot. Subtitle is clamped to 2 lines; a longer string is
truncated with an ellipsis.

## 8. Spacing

All internal spacing uses SP tokens (`var(--sp-sN)`).

| Where | Token |
|---|---|
| Top safe-area (status-bar zone) | `env(safe-area-inset-top)` · fallback `var(--sp-s44)` |
| Content row padding | `var(--sp-s8)` on all sides |
| Title ↔ subtitle gap | `var(--sp-s4)` |
| Center-slot min-height | `var(--sp-s40)` — keeps the slot `s40` tall with or without a subtitle |
| Leading spacer / end slot | fixed `var(--sp-s40)` × `var(--sp-s40)`, top-anchored |
| Close icon | 24 × 24 icon inside the end slot |

The root reserves the top safe-area so the fill runs under the status bar. Below it, the Content row
has `s8` vertical padding and its slots are **top-anchored**: the start / end slots are fixed
`s40 × s40`, pinned to the top. The center-slot has a `min-height` of `s40` and **centers its content
within that `s40`** (a lone title sits centered). When the subtitle is shown it fills the `s40`; a
subtitle that wraps to a 2nd line grows the center-slot **downward** below `s40` — but because the
side slots are top-anchored independently, **the close never shifts** when the subtitle is toggled or
wraps.

## 9. Usage context

**Use when:**
- A screen-level, blocking or critical state must be announced at the top of the screen (lost
  connection, failed payment, account restriction, trip problem).
- The message needs to persist until the state resolves or the user dismisses it.

**Do not use when:**
- The message is informational or transient → use **Toast / Snackbar** (auto-dismiss).
- The message belongs inside page content rather than the top chrome → use **Banner**.
- A decision or confirmation is required → use **Modal / Dialog**.

**Related components:** Banner (inline, page-level), Toast (transient), Modal (blocking
decisions).

## 10. Accessibility

The banner is an assertive status announcement; screen readers should read the title and subtitle
together, and the close control must be an independently focusable button with a clear label. Text
on the `Accent/Red 2` fill uses `AlwaysLight` — verify contrast for the shipped Red values. Full
spec: [`specs/components/header-alert/header-alert-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/header-alert.md).

## 11. Analytics and coverage contract

Reference: `docs/prototype-analytics-and-coverage.md` (repo root).

| Field | Value |
|---|---|
| `data-ds-component` | `HeaderAlert` |
| Coverage unit | yes |
| Tap target model | multiple targets (the close control only) |
| Actions | `dismiss` (on the close control) |
| Internal targets | `close` |
| Emits value | no |

The banner root is the coverage unit; the only tappable target is the close control. When the
close control is off, the component has no tap targets and reports no actions.

Required prototype markup:

```html
<div
  data-ds-component="HeaderAlert"
  data-ds-component-id="{screen}.{module}.{instance}"
  data-ds-variant="error"
  data-ds-state="standard">
  <div class="header-alert__content">
    <div class="header-alert__spacer"></div>
    <div class="header-alert__center">
      <div class="header-alert__title">…</div>
      <div class="header-alert__subtitle">…</div>
    </div>
    <button
      class="header-alert__close"
      data-ds-target="close"
      data-ds-action="dismiss"
      aria-label="Dismiss">
      <!-- close icon -->
    </button>
  </div>
</div>
```

---

## Changelog

Newest entry first.

Version scheme `3.X.Y` tracks the **Figma component** — `[HeaderAlert] 3.0`, so the lineage is
`3.0.Y`. The earlier repo-only `3.1.0–3.3.4` chain was a divergent counter (same entries,
renumbered 2026-07-27; e.g. old `3.3.4` = `3.0.7`).

| Version | Date | Change |
|---|---|---|
| 3.0.7 | 2026-07-24 | **Close-control ownership corrected** (owner decision): the close is a component-owned icon button (`.header-alert__close`), **not** an `IconContainer 3.0` instance — §§ 2/4/5/8/9 no longer claim IconContainer ownership (per `icon-container.md` § 5, press states belong to the parent anyway; `.icon-container` CSS is not shipped in `shared.css`). § 5 close-press row now names the real timing contract `--ha-pressed` / `--ha-released` (`Transforming/Pressed` / `Released`), integrator-driven like enter/exit. DRIFT note removed from `capabilities.json`. Preview page gains an **RTL demo instance** (the RTL axis previously had no rendered example) — and the demo immediately exposed an **RTL CSS bug, fixed**: with `dir="rtl"` the rtl writing direction already reverses the flex row, and the extra `row-reverse` double-flipped the slots back to LTR; `row-reverse` now applies only to the `.header-alert--rtl` class path (`:not([dir="rtl"])`), so both opt-in paths mirror correctly. |
| 3.0.6 | 2026-07-24 | **Capability contract authored** (`capabilities.json` — axes Style/Close/Subtitle/RTL, anatomy, constraints, analytics bindings; adversarially reviewed) + registry row now points to it. **Reduced-motion override added** for the `--ha-*` motion props in `shared.css` (was declared-only; the § 5 note referenced the stale `--pattern-scale-opacity-*` names — corrected to `--ha-*`). Contract flags two drifts for reconcile: the close control ships as a bare button (spec claims IconContainer 3.0 ownership), and the `--ha-enter`/`--ha-exit` props are a timing contract the integrator consumes — the component CSS ships no enter/exit state classes. |
| 3.0.5 | 2026-07-21 | Start / end slots are now fixed `s40 × s40` **top-anchored** (was `align-self: stretch`, which let the close re-center and drift down as the subtitle grew). The center-slot keeps its `min-height: s40` and **centers its content** within it (a lone title stays centered); a 2-line subtitle grows the center-slot **downward** below `s40`. Because the side slots are top-anchored independently, the **close and start slots stay in exactly the same position** whether the subtitle is hidden, one line, or two. |
| 3.0.4 | 2026-07-09 | Fixed close/leading-spacer vertical alignment. The center-slot now has a `min-height` of `s40` (hug + centered), so it is always `s40` tall whether or not a subtitle is shown; the side slots stretch to that height and centre their content, so the close icon always sits at the `s40` centre and the composition/geometry no longer shifts when the subtitle is toggled (was: close locked to the title line). Matches the Figma component. |
| 3.0.3 | 2026-07-09 | Synced to the Figma component-set: tone is now a first-class **Style** variant (Error/Warning/Success — no longer framed as DS extensions of Error); added an **RTL** variant axis (Off/On) that mirrors the Content-row slot order. New defaults — **close (`EndSlot`) off by default** (was on), **subtitle hidden by default**, default state = title only; the leading-spacer shows only alongside the close control. Clarified the top safe-area is a **reserved zone** the system status bar occupies — the component does not draw a status bar (the Figma mock status bar is a designer aid). |
| 3.0.2 | 2026-07-08 | Enter/exit changed from `fadeM` (opacity + scale) to an off-screen move from the top edge: appear `Transforming/Enter/Default` `translateY(-100% → 0)` (250ms); exit `translateY(0 → -100%)` (250ms, `duration.medium1` + exit curve), starting the moment the close control is released. Only `transform` animated. Subtitle wording tidied ("hidden when there is no subtitle"). |
| 3.0.1 | 2026-07-03 | Full-bleed banner (no corner radius) with top safe-area (`env(safe-area-inset-top)`, fallback `s44`) so the fill runs under the status bar. Close control + leading spacer lock to the title line (Content row top-aligned; slot height = title line box) — the close stays put as the subtitle grows. Subtitle clamped to 2 lines with ellipsis. Close icon updated to Icons 3.35.0 shape. |
| 3.0.0 | 2026-07-03 | Initial spec from Figma `[HeaderAlert] 3.0`: anatomy (leading spacer / center-slot / end-slot), Heading4 title + CompactBody subtitle, optional close, `fadeM` enter/exit, a11y, analytics contract. Three status tones — Error (`Accent/Red 2`, from Figma) plus Warning (`Accent/Orange 2`) and Success (`Accent/Green 2`) as DS extensions; text/icon `TextAndIcon/AlwaysLight` across all tones. |

---

# HeaderAlert — Accessibility

Reference spec: [`specs/components/header-alert/header-alert.md`](https://super-dollop-pzmo65r.pages.github.io/header-alert.md)

HeaderAlert is an assertive, screen-level status banner. It announces a critical state as soon as
it appears, so it must be exposed as a **live region / alert** that interrupts the screen reader.
Its only interactive element is the optional **close** control, which must be an independently
focusable button.

Because the banner sits in the top chrome and its message is centered for sighted users, the
title and subtitle must be grouped so a screen reader reads them as one announcement, not two
stray text nodes with an empty spacer between them.

---

## Android · TalkBack

### Announcement behavior

- On appear, the banner should be announced **assertively** — set the container as a live region
  (`android:accessibilityLiveRegion="assertive"`) or post an announcement event, so TalkBack
  interrupts and reads the title + subtitle together.
- The leading spacer is decorative — mark it `importantForAccessibility="no"` so it is never
  focused or announced.
- Title and subtitle are grouped into one focusable node (`title, subtitle`), read as a single
  status message.

### Element table

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| HeaderAlert root | Title + subtitle read together (assertive live region) | — | Alert / status | — |
| leading spacer | — | — | Not important for accessibility (decorative) | — |
| center-slot (title + subtitle) | "{title}, {subtitle}" | — | Text | — |
| close control | "Dismiss" (or contextual, e.g. "Dismiss alert") | — | Button | "Double-tap to dismiss" |

### Edge states

- **No subtitle:** the announcement is the title only. The center-slot exposes just the title node.
- **Close off:** the banner has no focusable interactive element — it is a pure status
  announcement. Nothing in the subtree is focusable.
- **Contrast:** text and the close icon use `TextAndIcon/AlwaysLight` on the `Accent/Red 2` fill.
  Verify the pairing meets the contrast target for the shipped Red values (Red/700 light,
  Red/600 dark) — see `color-rules.md` § contrast.

---

## iOS · VoiceOver

### Announcement behavior

- On appear, post a `UIAccessibility.post(notification: .announcement, …)` with the combined
  title + subtitle, or expose the container with the `.updatesFrequently` / alert semantics so
  VoiceOver reads it immediately.
- The leading spacer is decorative — `isAccessibilityElement = false`.
- Title and subtitle are combined into one accessibility element (`accessibilityLabel =
  "{title}, {subtitle}"`).

### Element table

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| HeaderAlert root | Title + subtitle announced on appear | — | Static text (announced) | — |
| leading spacer | — | — | Not an accessibility element (decorative) | — |
| center-slot (title + subtitle) | "{title}, {subtitle}" | — | Static text | — |
| close control | "Dismiss" (or contextual) | — | Button | — |

### Edge states

- **No subtitle:** label is the title only.
- **Close off:** the banner exposes no button; it is a status announcement only.
- **Dimmed:** HeaderAlert has no disabled state; it is either present or removed.

---

## Machine contract — `specs/components/header-alert/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "header-alert",
  "name": "HeaderAlert",
  "version": "3.0.7",
  "description": "A page-level status banner pinned to the top of the screen, announcing a blocking or critical state with a centered title + subtitle and an optional close control.",
  "files": {
    "spec": "specs/components/header-alert/header-alert.md",
    "a11y": "specs/components/header-alert/header-alert-a11y.md",
    "preview": "src/header-alert.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.26.0 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH"
  },
  "root": { "class": "header-alert", "dataDsComponent": "HeaderAlert" },
  "anatomy": {
    "content": {
      "class": "header-alert__content",
      "notes": "Horizontal row below the reserved top safe-area; padding s8 on all sides, slots top-aligned."
    },
    "leadingSpacer": {
      "class": "header-alert__spacer",
      "optional": true,
      "onlyWhen": { "close": [true] },
      "notes": "Fixed s40×s40 empty slot mirroring the end-slot so the message stays optically centered. Decorative (never announced). Exists ONLY paired with the close control — removed together with it."
    },
    "centerSlot": {
      "class": "header-alert__center",
      "notes": "Required. Vertical stack of title + subtitle, gap s4, min-height s40 with content centered in the s40 box; a 2-line subtitle grows it downward without moving the side slots."
    },
    "title": {
      "class": "header-alert__title",
      "notes": "Required. Single line, Heading 4, centered."
    },
    "subtitle": {
      "class": "header-alert__subtitle",
      "optional": true,
      "notes": "Compact Body, centered, clamped to 2 lines with ellipsis. Hidden by default."
    },
    "endSlot": {
      "class": "header-alert__close",
      "optional": true,
      "onlyWhen": { "close": [true] },
      "notes": "Fixed s40×s40 slot holding an IconContainer 3.0 with the close icon (24×24), top-anchored — it never moves as the subtitle grows. Off by default."
    }
  },
  "axes": {
    "style": {
      "title": "Style (tone)",
      "type": "enum",
      "values": ["error", "warning", "success"],
      "default": "error",
      "css": { "modifierTemplate": ".header-alert--{value}" },
      "figma": {
        "kind": "variant-property",
        "property": "Style",
        "values": { "error": "Error", "warning": "Warning", "success": "Success" }
      },
      "constraints": [
        "A tone is a single fill swap (--accent-red2 / --accent-orange2 / --accent-green2) — structure, typography, spacing and the AlwaysLight text colour are identical across tones.",
        "Info (Accent/Blue 2) is not part of this release — a new tone is added only against an existing Accent/* token with an owner decision."
      ]
    },
    "close": {
      "title": "Close control (EndSlot)",
      "type": "boolean",
      "default": false,
      "css": { "mechanism": "Off (default) = .header-alert--no-close on the root — the ONLY CSS-enforced path; it hides the close AND its paired leading-spacer together. If instead omitting nodes in static markup, omit BOTH .header-alert__close and .header-alert__spacer — omitting only one silently breaks the optical centering (no CSS enforcement on that path)." },
      "figma": { "kind": "variant-property", "property": "EndSlot", "values": { "false": "Off", "true": "On" } },
      "notes": "The close is a component-owned icon button (<button.header-alert__close>), NOT an IconContainer instance (owner decision, 3.0.7). Press timing contract = --ha-pressed / --ha-released (Transforming/Pressed / Released), integrator-driven like enter/exit — the component CSS ships the props, not state wiring."
    },
    "subtitle": {
      "title": "Subtitle",
      "type": "text",
      "default": "",
      "constraints": [
        "Clamped to a maximum of 2 lines; overflow truncates with an ellipsis.",
        "Announced together with the title as ONE screen-reader node (\"{title}, {subtitle}\")."
      ],
      "css": { "mechanism": "Render .header-alert__subtitle inside the center-slot; Compact Body, centered. Empty/absent = hidden (the default)." },
      "figma": { "kind": "variant-property", "property": "Subtitle", "values": { "false": "Hidden", "true": "Shown" } }
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": { "mechanism": "dir=\"rtl\" on the root (or .header-alert--rtl) — the Content row reverses (end-slot ↔ leading-spacer); title/subtitle stay centered; the close glyph is directional-neutral and is not mirrored." },
      "figma": { "kind": "variant-property", "property": "RTL", "values": { "false": "Off", "true": "On" } }
    }
  },
  "states": {
    "banner": ["standard", "entering", "exiting"],
    "close": ["standard", "pressed"]
  },
  "constraints": [
    "Single size — the banner always fills the full screen width; there is no size axis.",
    "Full-bleed with NO corner radius — roundness cannot be added.",
    "The root reserves the top safe-area (env(safe-area-inset-top), fallback var(--sp-s44)) — the component does not draw a status bar, it only leaves room for the platform's.",
    "All text and the close icon are TextAndIcon/AlwaysLight on every tone and in both themes — never InversePrimary, never theme-branched.",
    "The banner has no disabled and no loading state; it is either present or removed.",
    "The leading-spacer never appears without the close control (they are a pair).",
    "Enter/exit animate transform only (translateY(-100%) ↔ 0) — never layout, colour, or height.",
    "Enter/exit timing contract = --ha-enter / --ha-exit (250ms, zeroed under prefers-reduced-motion). The component CSS ships NO enter/exit state classes — the integrator drives the transition and consumes these props; the preview page's .ha-anim loop is a demo that hardcodes its own timeline.",
    "All internal spacing via SP tokens (var(--sp-sN)); raw px/em/rem/hex prohibited."
  ],
  "analytics": {
    "dataDsComponent": "HeaderAlert",
    "action": "dismiss",
    "targets": ["close"],
    "notes": "The banner root is the coverage unit; the only tappable target is the close control. With close off the component has no tap targets and reports no actions. Emits no value."
  },
  "rtl": {
    "supported": true,
    "notes": "Opt-in per instance (default Off). The Content row's horizontal slot order mirrors; the reserved safe-area layout mirrors; the close glyph is not mirrored."
  }
}
```
