# Toast (Lumen Halo): prompt.md (v1.0.0)

- id: `lumen-toast` · version 1.0.0 · component · free
- category: Feedback
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/lumen-toast.json`
- npm dependencies: @base-ui/react@^1
- registry dependencies: utils, @beautiful-ui/lumen-button, https://beautiful-ui.dev/r/lumen-foundation.json
- docs: https://beautiful-ui.dev/components/lumen-toast
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Panes of smoked glass that stack with a peek, fan out on hover and swipe away; a status cell lit by type (a red glow for errors, cells acquiring while loading) and one cue per toast. Promise toasts go from loading to done on their own. shadcn's native Toast, cut from Lumen glass.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `@base-ui/react@^1`.
- Files: `components/ui/lumen/toast.tsx`; shared code: `lib/beautiful-ui/lumen/glyphs.tsx`, `lib/beautiful-ui/lumen/portal.ts`, `lib/beautiful-ui/lumen/sound.ts`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `lumen-button`, `lumen-foundation`.
- Builds: one set of files for both, but its dependencies come in Base UI and Radix builds. Install the one that matches the project (see Install): a free item's bare URL installs the Base UI build of it and its dependencies.
- Exports to keep: `Toaster`, `toast / createToastManager / useToastManager`, `Toast / ToastContent / ToastTitle / ToastDescription / ToastAction / ToastClose`, and every exported type.
- CSS: the install merges this item's rules (the registry `css` field) into your global stylesheet, in `@layer components`, and adds the lumen foundation (tokens, keyframes, motion levels) once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Tokens: retheme with the `--lumen-*` custom properties (`--lumen-accent`, `--lumen-accent-foreground`, `--lumen-accent-text`, `--lumen-bad`, `--lumen-bad-text`, `--lumen-chart-accent`, `--lumen-chart-accent-foreground`, `--lumen-chart-accent-text`, `--lumen-focus`, `--lumen-good`, `--lumen-good-text`, `--lumen-hairline`, `--lumen-ink`, `--lumen-muted-ink`, `--lumen-series-1`, `--lumen-series-2`, `--lumen-series-3`, `--lumen-series-4`, `--lumen-series-5`, `--lumen-series-6`, `--lumen-warn`, `--lumen-warn-text`); this item's CSS also reads `--lumen-font-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { Toaster, toast } from "@/components/ui/lumen/toast";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `Toaster` | `toast-viewport` | Provider, portal and viewport in one; position is Beautiful UI's. |
| `toast / createToastManager / useToastManager` | `toast` | Add, update, close and promise toasts. |
| `Toast / ToastContent / ToastTitle / ToastDescription / ToastAction / ToastClose` | `toast-content` | Build your own toast. |

Style a part with `[data-slot="<slot>"]` selectors or its `className`; keep the attributes when editing.

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `GlassSoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`, import from `@/components/beautiful-ui/glass-sound`); `GlassSoundToggle` is its mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/ui/lumen/toast.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- toast, notification, snackbar, flash message, undo, promise toast, sonner alternative, shadcn toast, Lumen
- Confirming something happened (saved, exported, invited)
- An undo right after a destructive action
- Background work that finishes later (promise toasts)

### Not when
- Errors that block the page: use Alert
- Anything the person must act on before continuing: use Alert Dialog

## Mistakes
- Keep titles to a few words; details go in the description
- Offer Undo instead of confirming reversible deletes

## Usage

```tsx
import { Toaster, toast } from "@/components/ui/lumen/toast";

// once, near the root
<Toaster position="bottom-right" />

toast.add({ title: "Report exported", type: "success" });
toast.promise(saveReport(), { loading: { title: "Saving…" }, success: { title: "Saved" }, error: { title: "Couldn't save" } });
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `Toaster position` | `"top-left" \| "top-center" \| "top-right" \| "bottom-left" \| "bottom-center" \| "bottom-right"` | `"bottom-right"` | Where toasts appear (Beautiful UI). |
| `Toaster limit / timeout` | `number` |  | How many show at once; how long they stay. |
| `toast.add({ title, description, type, actionProps })` | `function` |  | type: success \| error \| warning \| info \| loading. |
| `toast.promise(promise, { loading, success, error })` | `function` |  | One toast that updates itself. |

Full docs: https://beautiful-ui.dev/components/lumen-toast

## Customising
- Colours: set the `--lumen-*` tokens on `:root`, or on a container with the `lumen-scope` class to retheme one area. Add the `lumen-inherit` class to follow your shadcn palette instead (`--chart-N`, `--destructive`).
- Dark mode follows the `.dark` class on an ancestor (the shadcn and next-themes convention).
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| F6 | Focus the toasts |
| Tab | Move between actions |
| Escape | Dismiss the focused toast |

## Motion inventory

| Interaction | What moves |
|---|---|
| Arrive | Rises in on the spring; older toasts shrink behind |
| Hover | The stack fans out |
| Swipe | Follows the finger and leaves that way |
| Loading | Three cells acquire in turn |

## Accessibility contract (preserve when editing)
- Announced through a live region; errors assertively
- Hovering or focusing the stack pauses the timers
- F6 moves focus to the toasts

## Install

Base UI project (a base-* style in components.json):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/lumen-toast.json
```

Radix project (a radix-*, new-york or default style):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/radix-nova/lumen-toast.json
```

Or add the `@beautiful-ui` registry to components.json and run `npx shadcn@latest add @beautiful-ui/lumen-toast`: the CLI picks the build from your style.

## Credits
- Built on shadcn/ui (https://ui.shadcn.com)
- Base UI Toast (https://base-ui.com/react/components/toast)

## Source (Base UI build)

### components/ui/lumen/toast.tsx

```tsx
"use client";

/**
 * Toast (Lumen Halo) v1.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/lumen-toast · Agent prompt: https://beautiful-ui.dev/md/lumen-toast.md
 * Licensed to the purchaser under the Beautiful UI license: https://beautiful-ui.dev/license
 */

import * as React from "react";
import { Toast as ToastPrimitive } from "@base-ui/react/toast";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/lumen/button";
import { LumenIcon } from "@/lib/beautiful-ui/lumen/glyphs";
import { useLumenPortal } from "@/lib/beautiful-ui/lumen/portal";
import { emitSound, useMergedRef } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Toast. shadcn's native Toast in Lumen Halo: each toast is a pane of smoked glass with a status
 * cell lit by its type (success green, error red and glowing, warning amber, info in the accent, and
 * loading as cells acquiring in turn). Toasts stack with a peek, fan out on hover, swipe away, and
 * play one cue for their type. Same exports and data-slots as shadcn (Base UI's Toast, both builds);
 * Beautiful UI adds `position` on the Toaster.
 */

const toast = ToastPrimitive.createToastManager();

type Position = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";

function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
  return <ToastPrimitive.Provider {...props} />;
}

function ToastPortal({ container, ...props }: ToastPrimitive.Portal.Props) {
  const scope = useLumenPortal();
  return <ToastPrimitive.Portal data-slot="toast-portal" container={container ?? scope} {...props} />;
}

function ToastViewport({ className, position = "bottom-right", ...props }: ToastPrimitive.Viewport.Props & { position?: Position }) {
  return <ToastPrimitive.Viewport data-slot="toast-viewport" data-position={position} className={cn(`lumen-toast-viewport`, className)} {...props} />;
}

const CUE: Record<string, string> = { success: "success", error: "error", warning: "warning" };

function Toast({ className, toast: item, ref, ...props }: ToastPrimitive.Root.Props) {
  const own = React.useRef<HTMLDivElement>(null);
  const merged = useMergedRef(own, ref as React.Ref<HTMLDivElement>);
  // One cue per toast, for its type, when it arrives.
  React.useEffect(() => {
    const cue = CUE[item.type ?? ""];
    if (cue) emitSound(own.current, cue);
  }, [item.id, item.type]);
  return <ToastPrimitive.Root ref={merged} toast={item} data-slot="toast" data-type={item.type} className={cn(`lumen-toast lumen-control`, className)} {...props} />;
}

function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
  return <ToastPrimitive.Content data-slot="toast-content" className={cn(`lumen-toast-content`, className)} {...props} />;
}

function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
  return <ToastPrimitive.Title data-slot="toast-title" className={cn(`lumen-toast-title`, className)} {...props} />;
}

function ToastDescription({ className, ...props }: ToastPrimitive.Description.Props) {
  return <ToastPrimitive.Description data-slot="toast-description" className={cn(`lumen-toast-description`, className)} {...props} />;
}

function ToastAction({ className, render = <Button variant="outline" size="sm" />, ...props }: ToastPrimitive.Action.Props) {
  return <ToastPrimitive.Action data-slot="toast-action" render={render} className={cn(`lumen-toast-action`, className)} {...props} />;
}

function ToastClose({ className, children, render = <Button variant="ghost" size="icon-sm" sound={false} />, ...props }: ToastPrimitive.Close.Props) {
  return (
    <ToastPrimitive.Close data-slot="toast-close" aria-label="Close toast" render={render} className={cn(`lumen-toast-close`, className)} {...props}>
      {children ?? <LumenIcon name="close" />}
    </ToastPrimitive.Close>
  );
}

/** The status cell: lit by the toast's type; loading acquires three cells in turn. */
function ToastIcon({ type }: { type: string | undefined }) {
  if (!type || !["success", "info", "warning", "error", "loading"].includes(type)) return null;
  return (
    <span data-slot="toast-icon" data-type={type} aria-hidden className={`lumen-toast-icon`}>
      {type === "loading" ? (
        <>
          <i />
          <i />
          <i />
        </>
      ) : (
        <i />
      )}
    </span>
  );
}

function ToastList() {
  const { toasts } = ToastPrimitive.useToastManager();
  return toasts.map((t) => (
    <Toast key={t.id} toast={t}>
      <ToastContent>
        <ToastIcon type={t.type} />
        <div className={`lumen-toast-text`}>
          <ToastTitle />
          <ToastDescription />
        </div>
        <ToastAction />
        <ToastClose />
      </ToastContent>
    </Toast>
  ));
}

function Toaster({ children, toastManager = toast, position = "bottom-right", ...props }: ToastPrimitive.Provider.Props & { position?: Position }) {
  return (
    <ToastProvider toastManager={toastManager} {...props}>
      {children}
      <ToastPortal>
        <ToastViewport position={position}>
          <ToastList />
        </ToastViewport>
      </ToastPortal>
    </ToastProvider>
  );
}

const createToastManager = ToastPrimitive.createToastManager;
const useToastManager = ToastPrimitive.useToastManager;

export { Toaster, Toast, ToastAction, ToastClose, ToastContent, ToastDescription, ToastPortal, ToastProvider, ToastTitle, ToastViewport, createToastManager, toast, useToastManager };
```

### lib/beautiful-ui/lumen/glyphs.tsx

```tsx
import type * as React from "react";

/*
 * Lumen's glyphs for its shadcn-built controls: the 4-cell loading ticker, the check that draws
 * itself in, the ! glyph, and the cell-drawn icon set. The same drawings as the glass family.
 */

const SVG = { width: "1.1em", height: "1.1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, focusable: false } as const;
const BOX: React.CSSProperties = { display: "block", flex: "none" };
const ST = { stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" } as const;
const cell = (x: number, y: number, s = 3) => <rect key={`${x}-${y}`} x={x} y={y} width={s} height={s} rx={0.9} fill="currentColor" />;

/** The 4-cell loading ticker in currentColor (.32em cells, .16em gaps). */
export function LumenTicker({ className }: { className?: string }) {
  return (
    <span aria-hidden className={className ? `lumen-tick ${className}` : `lumen-tick`}>
      <span />
      <span />
      <span />
      <span />
    </span>
  );
}

/** The check that draws itself in (stroke-dashoffset 16 → 0). Remount it (key) to replay. */
export function LumenCheckGlyph() {
  return (
    <svg {...SVG} className={`lumen-check`} style={BOX}>
      <path d="M3.5 8.4l2.9 2.9 6.1-6.6" {...ST} strokeWidth={1.9} />
    </svg>
  );
}

/** The ! glyph, cropped tight so its gap to a label matches the other glyphs. */
export function LumenErrorGlyph() {
  return (
    <svg {...SVG} width=".36em" viewBox="6.2 0 3.6 16" style={{ ...BOX, margin: "0 1px 0 -1px" }}>
      <rect x={6.9} y={2.6} width={2.2} height={7} rx={1.1} fill="currentColor" />
      <rect x={6.9} y={11.2} width={2.2} height={2.2} rx={0.8} fill="currentColor" />
    </svg>
  );
}

export const LUMEN_ICON_NAMES = ["plus", "grid", "more", "search", "filter", "copy", "arrow", "trash", "close", "check"] as const;
export type LumenIconName = (typeof LUMEN_ICON_NAMES)[number];
export const isLumenIconName = (v: unknown): v is LumenIconName => typeof v === "string" && (LUMEN_ICON_NAMES as readonly string[]).includes(v);

/** One icon from the Lumen set, 1.1em square in currentColor: plus, grid and more are cells; the rest are 1.6px strokes. */
export function LumenIcon({ name }: { name: LumenIconName }) {
  const p = { ...SVG, style: BOX };
  switch (name) {
    case "plus":
      return <svg {...p}>{[cell(6.5, 2.5), cell(2.5, 6.5), cell(6.5, 6.5), cell(10.5, 6.5), cell(6.5, 10.5)]}</svg>;
    case "more":
      return <svg {...p}>{[cell(2, 6.5), cell(6.5, 6.5), cell(11, 6.5)]}</svg>;
    case "grid":
      return <svg {...p}>{[cell(3, 3, 4), cell(9, 3, 4), cell(3, 9, 4), cell(9, 9, 4)]}</svg>;
    case "search":
      return (
        <svg {...p}>
          <circle cx={7} cy={7} r={4.25} {...ST} />
          <path d="M10.3 10.3L13.5 13.5" {...ST} />
        </svg>
      );
    case "copy":
      return (
        <svg {...p}>
          <rect x={5.5} y={5.5} width={8} height={8} rx={2} {...ST} />
          <path d="M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5" {...ST} />
        </svg>
      );
    case "trash":
      return (
        <svg {...p}>
          <path d="M2.8 4.5h10.4M6.4 4.5V3h3.2v1.5M4.3 4.5l.6 8.5h6.2l.6-8.5" {...ST} />
        </svg>
      );
    case "arrow":
      return (
        <svg {...p}>
          <path d="M3 8h10M9 4l4 4-4 4" {...ST} />
        </svg>
      );
    case "close":
      return (
        <svg {...p}>
          <path d="M4.5 4.5l7 7M11.5 4.5l-7 7" {...ST} />
        </svg>
      );
    case "filter":
      return (
        <svg {...p}>
          <path d="M2.5 4.5h11M4.5 8h7M6.5 11.5h3" {...ST} />
        </svg>
      );
    case "check":
      return <LumenCheckGlyph />;
  }
}
```

### lib/beautiful-ui/lumen/portal.ts

```tsx
"use client";

import * as React from "react";

/*
 * Where Lumen overlays (dialogs, sheets, menus, popovers, select) portal to. Unset, they portal to
 * document.body, as shadcn's do. Provide an element to keep them inside a themed or scoped part of
 * the page: a preview, a shadow root, an embedded widget.
 *
 *   <LumenPortalContext value={element}>…</LumenPortalContext>
 */
export const LumenPortalContext = React.createContext<HTMLElement | null>(null);

/** The container for an overlay's Portal: the provided element, else undefined (document.body). */
export function useLumenPortal(): HTMLElement | undefined {
  return React.useContext(LumenPortalContext) ?? undefined;
}
```

### lib/beautiful-ui/lumen/sound.ts

```tsx
"use client";

import * as React from "react";

/**
 * Asks the page's sound layer (beautiful-ui-sound) to play `cue`. Silent when nothing listens, and inside
 * anything marked data-sound="off". `index` pitches select cues by position. No audio code ships in
 * the components themselves.
 */
export function emitSound(el: Element | null, cue: string, force?: boolean, index?: number) {
  if (!el || typeof CustomEvent === "undefined" || el.closest('[data-sound="off"]')) return;
  el.dispatchEvent(new CustomEvent("beautiful-ui:sound", { bubbles: true, detail: { cue, force, index } }));
}

/**
 * Plays the cue `pick` returns whenever `value` changes (never on mount): an error when a field
 * turns invalid, a success when a check passes.
 */
export function useCueOnChange<T>(ref: React.RefObject<Element | null>, value: T, pick: (next: T, prev: T) => string | null) {
  const prev = React.useRef(value);
  const pickRef = React.useRef(pick);
  React.useEffect(() => {
    pickRef.current = pick;
  });
  React.useEffect(() => {
    if (Object.is(prev.current, value)) return;
    const before = prev.current;
    prev.current = value;
    const cue = pickRef.current(value, before);
    if (cue) emitSound(ref.current, cue);
  }, [ref, value]);
}

/** Points one ref (callback or object) at `node`; returns how to let go of it. */
function assign<T>(r: React.Ref<T> | undefined, node: T | null): () => void {
  if (typeof r === "function") {
    const cleanup = r(node);
    return typeof cleanup === "function" ? cleanup : () => r(null);
  }
  if (r) {
    (r as React.RefObject<T | null>).current = node;
    return () => ((r as React.RefObject<T | null>).current = null);
  }
  return () => {};
}

/**
 * One stable callback ref for the component's own ref and the caller's. It changes only when one of
 * them does, so a caller's callback ref isn't detached and reattached on every render, and it
 * honours React 19 ref cleanups.
 */
export function useMergedRef<T>(own: React.Ref<T> | undefined, theirs: React.Ref<T> | undefined): React.RefCallback<T> {
  return React.useCallback(
    (node: T | null) => {
      const release = [assign(own, node), assign(theirs, node)];
      return () => release.forEach((f) => f());
    },
    [own, theirs],
  );
}

/** True when `el` sits in an input group whose status part speaks for it (one cue per change). */
export const statusSpeaksFor = (el: Element | null) => Boolean(el?.closest('[data-slot="input-group"]')?.querySelector('[data-slot="input-group-status"]'));
```

### CSS (the registry `css` / `cssVars`, merged into the global stylesheet by the shadcn CLI)

```css
:root {
  --lo-scrim: rgba(20,24,40,.18);
  --lo-panel: linear-gradient(180deg,rgba(255,255,255,.97),rgba(247,248,251,.96));
  --lo-panelSh: 0 0 0 1px rgba(20,24,40,.08),0 1px 2px rgba(20,24,40,.06),0 24px 60px -20px rgba(20,24,40,.35);
  --lo-rim: inset 0 1px 0 #fff;
}
.dark, [data-theme="dark"] {
  --lo-scrim: rgba(0,0,0,.5);
  --lo-panel: linear-gradient(180deg,rgba(38,40,46,.96),rgba(18,19,23,.97));
  --lo-panelSh: 0 0 0 1px rgba(0,0,0,.8),0 30px 80px -24px rgba(0,0,0,.95);
  --lo-rim: inset 0 0 0 1px rgba(255,255,255,.08),inset 0 1px 0 rgba(255,255,255,.12);
}
@layer components {
  @keyframes lumen-toast-acquire {
    0%,100% {
      opacity: .25;
    }
    40% {
      opacity: 1;
    }
  }
  .lumen-toast-viewport {
    position: fixed;
    z-index: 60;
    width: min(380px,calc(100vw - 32px));
    outline: none;
  }
  .lumen-toast-viewport[data-position^="bottom"] {
    bottom: 16px;
  }
  .lumen-toast-viewport[data-position^="top"] {
    top: 16px;
  }
  .lumen-toast-viewport[data-position$="right"] {
    right: 16px;
  }
  .lumen-toast-viewport[data-position$="left"] {
    left: 16px;
  }
  .lumen-toast-viewport[data-position$="center"] {
    left: 50%;
    transform: translateX(-50%);
  }
  .lumen-toast {
    --gap: 12px;
    --peek: 12px;
    --scale: calc(max(0,1 - (var(--toast-index) * .1)));
    --shrink: calc(1 - var(--scale));
    --height: var(--toast-frontmost-height,var(--toast-height));
    position: absolute;
    inset-inline: 0;
    z-index: calc(1000 - var(--toast-index));
    box-sizing: border-box;
    height: var(--height);
    border-radius: calc(16px * var(--lumen-radius-k, 1));
    background: var(--lo-panel);
    box-shadow: var(--lo-rim),var(--lo-panelSh);
    color: var(--gc-ink);
    font: 400 13.5px/1.45 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    user-select: none;
    outline: none;
    will-change: transform;
    transition: transform .5s cubic-bezier(.22,1,.36,1),opacity .5s cubic-bezier(.22,1,.36,1),height .15s ease;
  }
  .lumen-toast:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), var(--lo-rim),var(--lo-panelSh);
  }
  .lumen-toast::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    height: calc(var(--gap) + 1px);
  }
  [data-position^="bottom"] .lumen-toast {
    --offset-y: calc(var(--toast-offset-y) * -1 + var(--toast-index) * var(--gap) * -1 + var(--toast-swipe-movement-y));
    bottom: 0;
    transform-origin: bottom;
    transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - var(--toast-index) * var(--peek) - var(--shrink) * var(--height))) scale(var(--scale));
  }
  [data-position^="bottom"] .lumen-toast::after {
    top: 100%;
  }
  [data-position^="bottom"] .lumen-toast[data-expanded] {
    height: var(--toast-height);
    transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
  }
  [data-position^="bottom"] .lumen-toast[data-starting-style],[data-position^="bottom"] .lumen-toast[data-ending-style]:not([data-limited]):not([data-swipe-direction]) {
    transform: translateY(150%);
  }
  [data-position^="top"] .lumen-toast {
    --offset-y: calc(var(--toast-offset-y) + var(--toast-index) * var(--gap) + var(--toast-swipe-movement-y));
    top: 0;
    transform-origin: top;
    transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) + var(--toast-index) * var(--peek) + var(--shrink) * var(--height))) scale(var(--scale));
  }
  [data-position^="top"] .lumen-toast::after {
    bottom: 100%;
  }
  [data-position^="top"] .lumen-toast[data-expanded] {
    height: var(--toast-height);
    transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
  }
  [data-position^="top"] .lumen-toast[data-starting-style],[data-position^="top"] .lumen-toast[data-ending-style]:not([data-limited]):not([data-swipe-direction]) {
    transform: translateY(-150%);
  }
  .lumen-toast[data-limited] {
    opacity: 0;
  }
  .lumen-toast[data-ending-style][data-swipe-direction="down"] {
    transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
  }
  .lumen-toast[data-ending-style][data-swipe-direction="up"] {
    transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
  }
  .lumen-toast[data-ending-style][data-swipe-direction="left"] {
    transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
  }
  .lumen-toast[data-ending-style][data-swipe-direction="right"] {
    transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
  }
  .lumen-toast-content {
    display: flex;
    align-items: center;
    gap: 12px;
    box-sizing: border-box;
    height: 100%;
    padding: 14px 12px 14px 16px;
    overflow: hidden;
    transition: opacity .25s cubic-bezier(.22,1,.36,1);
  }
  .lumen-toast-content[data-behind] {
    opacity: 0;
  }
  .lumen-toast-content[data-expanded] {
    opacity: 1;
  }
  .lumen-toast-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }
  .lumen-toast-title {
    font-weight: 600;
    letter-spacing: -.005em;
  }
  .lumen-toast-description {
    color: var(--gc-sec);
    font-size: 13px;
  }
  .lumen-toast-action {
    flex: none;
  }
  .lumen-toast-close {
    flex: none;
    color: var(--gc-sec);
  }
  .lumen-toast-icon {
    display: flex;
    flex: none;
    gap: 3px;
    align-self: flex-start;
    margin-top: 5px;
  }
  .lumen-toast-icon>i {
    width: 10px;
    height: 10px;
    border-radius: calc(3px * var(--lumen-radius-k, 1));
    background: var(--gc-acc);
    box-shadow: 0 0 10px color-mix(in srgb,var(--gc-acc) 60%,transparent);
  }
  .lumen-toast-icon[data-type="success"]>i {
    background: var(--gc-good);
    box-shadow: 0 0 10px color-mix(in srgb,var(--gc-good) 60%,transparent);
  }
  .lumen-toast-icon[data-type="warning"]>i {
    background: var(--gc-warn);
    box-shadow: 0 0 10px color-mix(in srgb,var(--gc-warn) 60%,transparent);
  }
  .lumen-toast-icon[data-type="error"]>i {
    background: var(--gc-bad);
    box-shadow: 0 0 14px color-mix(in srgb,var(--gc-bad) 80%,transparent);
  }
  .lumen-toast-icon[data-type="loading"]>i {
    width: 6px;
    height: 10px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    animation: lumen-toast-acquire calc(1.2s * var(--gcp-k,1)) ease-in-out infinite;
  }
  .lumen-toast-icon[data-type="loading"]>i:nth-child(2) {
    animation-delay: .15s;
  }
  .lumen-toast-icon[data-type="loading"]>i:nth-child(3) {
    animation-delay: .3s;
  }
  @media (prefers-reduced-motion:reduce) {
    .lumen-toast {
      transition-duration: .01s;
    }
    .lumen-toast-icon[data-type="loading"]>i {
      animation: none;
      opacity: .7;
    }
  }
}
```

The lumen foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://beautiful-ui.dev/r/lumen-foundation.json.
