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

- id: `lumen-chart` · version 1.0.0 · component · free
- category: Charts
- 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-chart.json`
- npm dependencies: none
- registry dependencies: utils, https://beautiful-ui.dev/r/lumen-foundation.json
- docs: https://beautiful-ui.dev/components/lumen-chart
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Everything around the plot, done once: shadcn's ChartConfig as live colour variables, a header, legend and readout, a glass tooltip that follows pointer and keys, a mode switch with a gliding thumb, and the table screen readers get. The base every Lumen story chart stands on; bring your own plot.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: none beyond React.
- Files: `components/ui/lumen/chart.tsx`, `components/ui/lumen/chart-client.tsx`; shared code: `lib/beautiful-ui/lumen/chart-config.ts`, `lib/beautiful-ui/lumen/glyphs.tsx`, `lib/beautiful-ui/lumen/indicator.ts`, `lib/beautiful-ui/lumen/sound.ts`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `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: `ChartContainer / ChartStyle`, `ChartHeader / ChartEyebrow / ChartTitle / ChartDescription`, `ChartReadout`, `ChartModes / ChartMode`, `ChartPlot`, `ChartTooltip / ChartTooltipHeader / ChartTooltipRow`, `ChartLegend / ChartLegendItem`, `ChartTable`, `ChartEmpty / ChartLoading / ChartLive`, 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-text`, `--lumen-bad`, `--lumen-bad-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-mono`, `--lumen-font-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { ChartContainer, ChartHeader, ChartLegend, ChartLegendItem, ChartModes, ChartMode, ChartPlot, ChartTitle, ChartTooltip, ChartTooltipRow, type ChartConfig } from "@/components/ui/lumen/chart";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `ChartContainer / ChartStyle` | `chart` | Writes the config as --color-<key>, light and dark. |
| `ChartHeader / ChartEyebrow / ChartTitle / ChartDescription` | `chart-header` | The story's headline, with an optional live dot. |
| `ChartReadout` | `chart-readout` | The headline number and its context. |
| `ChartModes / ChartMode` | `chart-modes` | The segmented switch with the gliding thumb. |
| `ChartPlot` | `chart-plot` | Holds the plot's size across loading, empty and drawn. |
| `ChartTooltip / ChartTooltipHeader / ChartTooltipRow` | `chart-tooltip` | The glass tooltip. |
| `ChartLegend / ChartLegendItem` | `chart-legend` | Cells or lines per series; optional toggles. |
| `ChartTable` | `chart-table` | The data for screen readers, or on demand. |
| `ChartEmpty / ChartLoading / ChartLive` | `chart-empty` | The quiet states and the live region. |

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/chart.tsx`, `components/ui/lumen/chart-client.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
- chart, chart base, chart container, ChartConfig, legend, tooltip, chart modes, data table, custom chart, svg chart, Lumen
- Building your own chart (SVG or canvas) that should look, sound and read like Lumen's
- The frame around any plot: header, readout, legend, modes, tooltip, table
- Colours from shadcn's ChartConfig, light and dark, without re-rendering

### Not when
- A ready-made chart: install a Lumen story chart, which is built on this

## Mistakes
- Colour series with var(--color-<key>), not the raw hex, so the theme and dark mode apply
- Give the plot aria-describedby the tooltip's id while it's open
- Keep the ChartTable: it's how screen readers read your chart

## Usage

```tsx
import { ChartContainer, ChartHeader, ChartLegend, ChartLegendItem, ChartModes, ChartMode, ChartPlot, ChartTitle, ChartTooltip, ChartTooltipRow, type ChartConfig } from "@/components/ui/lumen/chart";

const config = {
  teams: { label: "Teams", color: "var(--chart-1)" },
} satisfies ChartConfig;

<ChartContainer config={config}>
  <ChartHeader>
    <ChartTitle>Weekly active teams</ChartTitle>
  </ChartHeader>
  <ChartModes defaultValue="teams" aria-label="Measure">
    <ChartMode value="teams">Teams</ChartMode>
    <ChartMode value="seats">Seats</ChartMode>
  </ChartModes>
  <ChartPlot aspect="16 / 7">
    {/* your SVG: fill="var(--color-teams)" */}
    <ChartTooltip open={hover != null} x={x} y={y}>
      <ChartTooltipRow name="teams" config={config} value="8,940" />
    </ChartTooltip>
  </ChartPlot>
  <ChartLegend>
    <ChartLegendItem name="teams" config={config} />
  </ChartLegend>
</ChartContainer>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `ChartContainer config` | `ChartConfig` |  | shadcn's shape: { [key]: { label, color } \| { label, theme: { light, dark } } }. Each key becomes --color-<key>. |
| `ChartModes value / defaultValue / onValueChange` | `string` |  | A radio group with the gliding thumb; arrow keys move and choose; one select cue per change. |
| `ChartTooltip open / x / y` | `boolean / number / number` |  | Placed above the point inside the plot, kept within its edges, below when there's no room. |
| `ChartLegendItem name / pressed` | `string / boolean` |  | The key's colour and label; with pressed, a toggle that shows or hides its series. |
| `ChartTable caption / columns / rows` | `string / string[] / (string \| number)[][]` |  | Hidden for screen readers up to 500 cells; above that, a "View as table" disclosure. |
| `useChartSelection / useChartKeyboard / useChartSize` | `hooks` |  | Controlled-or-not state, arrow keys over the data (one tab stop), and the plot's size after mount. |

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

## 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 |
|---|---|
| ← → ↑ ↓ | Modes: move and choose. Plot (useChartKeyboard): move between points |
| Home / End | First or last mode or point |
| Enter / Space | Pin the focused point |
| Escape | Clear the pin |

## Motion inventory

| Interaction | What moves |
|---|---|
| Mode change | The thumb glides to the chosen mode (.6s spring) |
| Tooltip | Fades in and out; follows the point without lag |
| Reduced motion | The thumb moves in place; fades shorten to 150ms |

## Accessibility contract (preserve when editing)
- ChartTable gives screen readers the data; big tables become a "View as table" disclosure
- ChartModes is a radio group: one tab stop, arrows move and choose
- useChartKeyboard gives a plot one tab stop with arrow keys, Enter to pin and Escape to clear
- ChartLive announces one sentence at a time as the chart changes
- Legend toggles say whether their series is shown (aria-pressed)

## Install

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

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

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

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

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

## Credits
- ChartConfig from shadcn/ui (https://ui.shadcn.com/docs/components/chart)

## Source (Base UI build)

### components/ui/lumen/chart.tsx

```tsx
/**
 * Chart (Lumen Halo) v1.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/lumen-chart · Agent prompt: https://beautiful-ui.dev/md/lumen-chart.md
 * Licensed to the purchaser under the Beautiful UI license: https://beautiful-ui.dev/license
 */
import * as React from "react";
import { cn } from "@/lib/utils";
import { CHART_TABLE_LIMIT, chartColor, chartLabel, chartStyleCss, type ChartConfig } from "@/lib/beautiful-ui/lumen/chart-config";
import { LumenTicker } from "@/lib/beautiful-ui/lumen/glyphs";
import { ChartTableDisclosure, ChartTableMarkup } from "@/components/ui/lumen/chart-client";

/*
 * Lumen Chart: the base every Lumen story chart is built on, in shadcn's shape. ChartContainer takes
 * shadcn's ChartConfig and writes each series as --color-<key> (light and dark), so recolouring never
 * re-renders. Around the plot: a header, a legend, a readout, the data table screen readers get, and
 * the empty and loading states. Everything in this file is server-safe; the tooltip, the modes and
 * the hooks are client parts (chart-client), exported from here too.
 */

function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
  const css = chartStyleCss(id, config);
  return css ? <style data-slot="chart-style" dangerouslySetInnerHTML={{ __html: css }} /> : null;
}

function ChartContainer({
  id,
  config,
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  config: ChartConfig;
}) {
  const uid = React.useId();
  // Any string works: the style's selector escapes it, so two ids never collide.
  const chartId = id ?? `chart${uid}`;
  return (
    <div data-slot="chart" data-chart={chartId} className={cn(`lumen-chart`, className)} {...props}>
      <ChartStyle id={chartId} config={config} />
      {children}
    </div>
  );
}

/** The story-chart glass: a deep body, a rim of light and a sheen. Recipe cards stand in it. */
function ChartSurface({ className, ...props }: React.ComponentProps<"section">) {
  return <section data-slot="chart-surface" className={cn(`lumen-chart-surface`, className)} {...props} />;
}

function ChartHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="chart-header" className={cn(`lumen-chart-header`, className)} {...props} />;
}

function ChartEyebrow({ className, live, children, ...props }: React.ComponentProps<"div"> & { /** The green live dot, for data that updates. */ live?: boolean | undefined }) {
  return (
    <div data-slot="chart-eyebrow" data-live={live ? "" : undefined} className={cn(`lumen-chart-eyebrow`, className)} {...props}>
      {children}
    </div>
  );
}

function ChartTitle({ className, ...props }: React.ComponentProps<"h2">) {
  return <h2 data-slot="chart-title" className={cn(`lumen-chart-title`, className)} {...props} />;
}

function ChartDescription({ className, ...props }: React.ComponentProps<"p">) {
  return <p data-slot="chart-description" className={cn(`lumen-chart-description`, className)} {...props} />;
}

/** The chart's headline number and its context, e.g. a StatValue with "of 20 markets beat target". */
function ChartReadout({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="chart-readout" className={cn(`lumen-chart-readout`, className)} {...props} />;
}

function ChartLegend({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="chart-legend" role="list" className={cn(`lumen-chart-legend`, className)} {...props} />;
}

type LegendItemProps = {
  /** The config key: its label and --color-<key>. */
  name?: string | undefined;
  config?: ChartConfig | undefined;
  /** A colour instead of the key's. */
  color?: string | undefined;
  /** A count or share after the label. */
  value?: React.ReactNode;
  /** "cell" (default): Lumen's square; "line": a stroke, for a trace or a target. */
  indicator?: "cell" | "line" | undefined;
};

/** A legend entry; with `pressed` it's a toggle button that shows or hides its series. */
function ChartLegendItem({
  className,
  name,
  config,
  color,
  value,
  indicator = "cell",
  pressed,
  children,
  ...props
}: Omit<React.ComponentProps<"button">, "value"> & LegendItemProps & { pressed?: boolean | undefined }) {
  const label = children ?? (name ? chartLabel(config ?? {}, name) : null);
  const swatch = { "--lc-c": color ?? (name ? chartColor(name) : "currentColor") } as React.CSSProperties;
  const inner = (
    <>
      <span aria-hidden data-indicator={indicator} className={`lumen-chart-legend-swatch`} />
      <span className={`lumen-chart-legend-label`}>{label}</span>
      {value != null && <span className={`lumen-chart-legend-value`}>{value}</span>}
    </>
  );
  if (pressed === undefined)
    return (
      <span data-slot="chart-legend-item" role="listitem" className={cn(`lumen-chart-legend-item`, className)} style={swatch}>
        {inner}
      </span>
    );
  return (
    <span role="listitem" className="contents">
      <button type="button" data-slot="chart-legend-item" aria-pressed={pressed} data-sound={pressed ? "toggleOff" : "toggleOn"} className={cn(`lumen-chart-legend-item`, className)} style={swatch} {...props}>
        {inner}
      </button>
    </span>
  );
}

/** Where the plot sits: holds its height, so loading, empty and drawn states never jump. */
function ChartPlot({ className, aspect, style, ...props }: React.ComponentProps<"div"> & { /** CSS aspect-ratio before a height is known, e.g. "16 / 7". */ aspect?: string | undefined }) {
  return <div data-slot="chart-plot" className={cn(`lumen-chart-plot`, className)} style={{ aspectRatio: aspect, ...style }} {...props} />;
}

/** The empty state: a quiet line in Lumen's voice, e.g. "No weeks yet". */
function ChartEmpty({ className, children = "No data yet", ...props }: React.ComponentProps<"div">) {
  return (
    <div data-slot="chart-empty" role="status" className={cn(`lumen-chart-state`, className)} {...props}>
      <span aria-hidden className={`lumen-chart-empty-cells`}>
        <i />
        <i />
        <i />
        <i />
      </span>
      {children}
    </div>
  );
}

/** Loading: Lumen's ticker in the plot area, with the size held. */
function ChartLoading({ className, children = "Loading", ...props }: React.ComponentProps<"div">) {
  return (
    <div data-slot="chart-loading" role="status" className={cn(`lumen-chart-state lumen-control`, className)} {...props}>
      <span className={`lumen-chart-loading-ticker`}>
        <LumenTicker />
      </span>
      <span className="sr-only">{children}</span>
    </div>
  );
}

/**
 * The data as a table. Up to 500 cells it's visually hidden and read by screen readers; above that,
 * a "View as table" disclosure renders it on demand (spec 18 §7).
 */
function ChartTable({
  className,
  caption,
  columns,
  rows,
  viewLabel = "View as table",
  visible,
  ...props
}: Omit<React.ComponentProps<"table">, "children"> & {
  caption: string;
  /** Column headers; the first names the row headers. */
  columns: readonly string[];
  /** Each row: its header, then one value per remaining column. */
  rows: readonly (readonly (string | number)[])[];
  viewLabel?: string | undefined;
  /** Always show it (for print or a "table" mode). */
  visible?: boolean | undefined;
}) {
  const cells = rows.length * Math.max(1, columns.length);
  const table = { caption, columns, rows, className, ...props };
  if (visible) return <ChartTableMarkup {...table} />;
  if (cells <= CHART_TABLE_LIMIT)
    return (
      <div className="sr-only">
        <ChartTableMarkup {...table} />
      </div>
    );
  // Big tables build their rows only when opened.
  // A caller's (deprecated) table `summary` attribute still wins, as it did when it was spread after.
  return <ChartTableDisclosure {...table} summary={table.summary ?? viewLabel} />;
}

/** A polite live region for what the chart says as it changes (one sentence at a time). */
function ChartLive({ className, ...props }: React.ComponentProps<"span">) {
  return <span data-slot="chart-live" aria-live="polite" aria-atomic="true" className={cn("sr-only", className)} {...props} />;
}

export {
  ChartContainer,
  ChartStyle,
  ChartSurface,
  ChartHeader,
  ChartEyebrow,
  ChartTitle,
  ChartDescription,
  ChartReadout,
  ChartLegend,
  ChartLegendItem,
  ChartPlot,
  ChartEmpty,
  ChartLoading,
  ChartTable,
  ChartLive,
};
export { chartColor, chartKey, chartLabel, chartStyleCss, fromMatrix, fromRows, interpolate, CHART_TABLE_LIMIT, type ChartConfig } from "@/lib/beautiful-ui/lumen/chart-config";
export { ChartTooltip, ChartTooltipHeader, ChartTooltipRow, ChartModes, ChartMode, ChartTableMarkup, ChartTableDisclosure, useChartSize, useChartSelection, useChartKeyboard } from "@/components/ui/lumen/chart-client";
```

### components/ui/lumen/chart-client.tsx

```tsx
"use client";

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

import * as React from "react";
import { cn } from "@/lib/utils";
import { chartColor, chartLabel, type ChartConfig } from "@/lib/beautiful-ui/lumen/chart-config";
import { useActiveIndicator } from "@/lib/beautiful-ui/lumen/indicator";
import { emitSound, useMergedRef } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Chart's client parts: the glass tooltip that follows the pointer and the keyboard, the mode
 * switch (a radio group with Lumen's gliding thumb), and the hooks every story chart shares: size,
 * controlled-or-not selection, and arrow-key navigation over the data.
 */

// ---------------------------------------------------------------------------------------------
// Hooks
// ---------------------------------------------------------------------------------------------

const useIsoLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

/** The element's content size after mount (null before, so the first render matches the server's). */
export function useChartSize(ref: React.RefObject<HTMLElement | null>) {
  const [size, setSize] = React.useState<{ width: number; height: number } | null>(null);
  useIsoLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    const read = () => {
      const width = el.clientWidth;
      const height = el.clientHeight;
      setSize((s) => (s && s.width === width && s.height === height ? s : { width, height }));
    };
    read();
    const ro = new ResizeObserver(read);
    ro.observe(el);
    return () => ro.disconnect();
  }, [ref]);
  return size;
}

/**
 * A value that is controlled (`value` + `onValueChange`) or not (`defaultValue`), as every Lumen chart's
 * mode and selection are. IDs are strings, never indexes.
 */
export function useChartSelection<T>({ value, defaultValue, onValueChange }: { value?: T | undefined; defaultValue: T; onValueChange?: ((value: T) => void) | undefined }) {
  const [inner, setInner] = React.useState(defaultValue);
  const controlled = value !== undefined;
  const current = controlled ? value : inner;
  const onChange = React.useRef(onValueChange);
  React.useEffect(() => {
    onChange.current = onValueChange;
  });
  const set = React.useCallback(
    (next: T) => {
      if (!controlled) setInner(next);
      onChange.current?.(next);
    },
    [controlled],
  );
  return [current, set] as const;
}

/**
 * Arrow keys through the data (one tab stop per plot): Left/Right step, Up/Down step by `columns`
 * when the data is a grid, Home/End jump, Enter or Space pins, Escape clears.
 */
export function useChartKeyboard({
  count,
  index,
  onIndexChange,
  onPin,
  onClear,
  columns,
}: {
  count: number;
  /** The focused point, or -1 for none. */
  index: number;
  onIndexChange: (index: number) => void;
  onPin?: ((index: number) => void) | undefined;
  onClear?: (() => void) | undefined;
  columns?: number | undefined;
}) {
  return React.useCallback(
    (e: React.KeyboardEvent) => {
      if (!count) return;
      const rtl = getComputedStyle(e.currentTarget as HTMLElement).direction === "rtl";
      const at = index < 0 ? 0 : index;
      const step: Record<string, number> = { ArrowRight: rtl ? -1 : 1, ArrowLeft: rtl ? 1 : -1, ArrowDown: columns ?? 1, ArrowUp: -(columns ?? 1) };
      let next: number | null = null;
      const delta = step[e.key];
      if (delta !== undefined) next = index < 0 ? 0 : Math.min(count - 1, Math.max(0, at + delta));
      else if (e.key === "Home") next = 0;
      else if (e.key === "End") next = count - 1;
      else if ((e.key === "Enter" || e.key === " ") && index >= 0 && onPin) {
        e.preventDefault();
        onPin(index);
        return;
      } else if (e.key === "Escape" && onClear) {
        e.preventDefault();
        onClear();
        return;
      }
      if (next == null) return;
      e.preventDefault();
      if (next !== index) onIndexChange(next);
    },
    [count, index, onIndexChange, onPin, onClear, columns],
  );
}

// ---------------------------------------------------------------------------------------------
// Tooltip
// ---------------------------------------------------------------------------------------------

/**
 * The glass tooltip, placed at (x, y) inside the plot (px from its top-left): above the point, kept
 * inside the plot's edges, below the point when there's no room above. Give the plot
 * `aria-describedby` the tooltip's id while it's open.
 */
export function ChartTooltip({ className, open, x, y, style, ref, ...props }: React.ComponentProps<"div"> & { open: boolean; x: number; y: number }) {
  const own = React.useRef<HTMLDivElement>(null);
  const merged = useMergedRef(own, ref);
  useIsoLayoutEffect(() => {
    const el = own.current;
    const box = el?.offsetParent as HTMLElement | null;
    if (!el || !box || !open) return;
    const w = el.offsetWidth;
    const h = el.offsetHeight;
    const H = box.clientHeight;
    // Kept inside the plot, and inside the part of it a scrolling ancestor shows.
    let lo = 8;
    let hi = box.clientWidth - 8;
    for (let a = box.parentElement; a; a = a.parentElement) {
      if (a.scrollWidth <= a.clientWidth || !/(auto|scroll|hidden)/.test(getComputedStyle(a).overflowX)) continue;
      const b = box.getBoundingClientRect();
      const r = a.getBoundingClientRect();
      lo = Math.max(lo, r.left - b.left + 8);
      hi = Math.min(hi, r.right - b.left - 8);
      break;
    }
    const left = Math.max(lo, Math.min(hi - w, x - w / 2));
    // Above when it fits, else below when that fits, else whichever side has more room, clamped in.
    const above = y - h - 14;
    const under = y + 16;
    const below = above < 4 && (under + h <= H - 4 || H - y > y);
    const top = Math.max(4, Math.min(H - h - 4, below ? under : above));
    el.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
    el.dataset.side = below ? "bottom" : "top";
  }, [open, x, y]);
  return <div ref={merged} role="tooltip" data-slot="chart-tooltip" data-open={open ? "" : undefined} className={cn(`lumen-chart-tooltip`, className)} style={style} {...props} />;
}

export function ChartTooltipHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="chart-tooltip-header" className={cn(`lumen-chart-tooltip-header`, className)} {...props} />;
}

/** A row: the series' cell, its label and its value. */
export function ChartTooltipRow({ className, name, config, color, label, value, ...props }: Omit<React.ComponentProps<"div">, "color"> & { name?: string | undefined; config?: ChartConfig | undefined; color?: string | undefined; label?: React.ReactNode; value: React.ReactNode }) {
  return (
    <div data-slot="chart-tooltip-row" className={cn(`lumen-chart-tooltip-row`, className)} style={{ "--lc-c": color ?? (name ? chartColor(name) : "currentColor") } as React.CSSProperties} {...props}>
      <span aria-hidden className={`lumen-chart-legend-swatch`} />
      <span className={`lumen-chart-tooltip-label`}>{label ?? (name ? chartLabel(config ?? {}, name) : null)}</span>
      <span className={`lumen-chart-tooltip-value`}>{value}</span>
    </div>
  );
}

// ---------------------------------------------------------------------------------------------
// Modes: a radio group with the gliding thumb
// ---------------------------------------------------------------------------------------------

const ModesContext = React.createContext<{ value: string; select: (v: string, el: HTMLElement) => void } | null>(null);

export function ChartModes({
  className,
  value,
  defaultValue,
  onValueChange,
  children,
  ref,
  onKeyDown,
  ...props
}: Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & { value?: string | undefined; defaultValue?: string | undefined; onValueChange?: ((value: string) => void) | undefined }) {
  const [current, set] = useChartSelection({ value, defaultValue: defaultValue ?? "", onValueChange });
  const list = React.useRef<HTMLDivElement>(null);
  const merged = useMergedRef(list, ref);
  useActiveIndicator(list, '[aria-checked="true"]');
  // The tab stop is the chosen mode if it's enabled, else the first enabled one, so the group stays reachable.
  useIsoLayoutEffect(() => {
    const radios = Array.from(list.current?.querySelectorAll<HTMLButtonElement>('[role="radio"]') ?? []);
    const enabled = radios.filter((r) => !r.disabled);
    const stop = enabled.find((r) => r.getAttribute("aria-checked") === "true") ?? enabled[0];
    for (const r of radios) r.tabIndex = r === stop ? 0 : -1;
  });
  // The cue plays when the value really changes (a controlled owner may keep the old one).
  const pending = React.useRef<{ value: string; el: HTMLElement; index: number } | null>(null);
  React.useEffect(() => {
    const p = pending.current;
    if (!p) return;
    pending.current = null;
    if (p.value === current) emitSound(p.el, "select", false, p.index);
  }, [current]);
  const ctx = React.useMemo(
    () => ({
      value: current,
      select: (v: string, el: HTMLElement) => {
        if (v === current) return;
        const radios = Array.from(list.current?.querySelectorAll('[role="radio"]') ?? []);
        pending.current = { value: v, el, index: Math.max(0, radios.indexOf(el)) };
        set(v);
      },
    }),
    [current, set],
  );
  const keys = (e: React.KeyboardEvent<HTMLDivElement>) => {
    onKeyDown?.(e);
    if (e.defaultPrevented) return;
    const radios = Array.from(list.current?.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)') ?? []);
    const at = radios.findIndex((r) => r === document.activeElement);
    if (at < 0) return;
    const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
    const d = { ArrowRight: rtl ? -1 : 1, ArrowDown: 1, ArrowLeft: rtl ? 1 : -1, ArrowUp: -1 }[e.key];
    const next = d != null ? radios[(at + d + radios.length) % radios.length] : e.key === "Home" ? radios[0] : e.key === "End" ? radios[radios.length - 1] : null;
    if (!next) return;
    e.preventDefault();
    next.focus();
    next.click();
  };
  return (
    <ModesContext.Provider value={ctx}>
      <div {...props} ref={merged} role="radiogroup" data-slot="chart-modes" className={cn(`lumen-chart-modes`, className)} onKeyDown={keys}>
        <span aria-hidden className={`lumen-chart-modes-thumb`} />
        {children}
      </div>
    </ModesContext.Provider>
  );
}

export function ChartMode({ className, value, onClick, ...props }: Omit<React.ComponentProps<"button">, "value"> & { value: string }) {
  const ctx = React.useContext(ModesContext);
  const on = ctx?.value === value;
  return (
    <button
      type="button"
      {...props}
      role="radio"
      aria-checked={on}
      tabIndex={on && !props.disabled ? 0 : -1}
      data-slot="chart-mode"
      data-sound="none"
      className={cn(`lumen-chart-mode`, className)}
      onClick={(e) => {
        onClick?.(e);
        if (!e.defaultPrevented) ctx?.select(value, e.currentTarget);
      }}
    />
  );
}

// ---------------------------------------------------------------------------------------------
// The table: its markup, and the disclosure that builds a big one only when opened
// ---------------------------------------------------------------------------------------------

type TableProps = Omit<React.ComponentProps<"table">, "children"> & { caption: string; columns: readonly string[]; rows: readonly (readonly (string | number)[])[] };

export function ChartTableMarkup({ className, caption, columns, rows, ...props }: TableProps) {
  return (
    <table data-slot="chart-table" className={cn(`lumen-chart-table`, className)} {...props}>
      <caption>{caption}</caption>
      <thead>
        <tr>
          {columns.map((c, i) => (
            <th key={i} scope="col">
              {c}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((r, i) => (
          <tr key={i}>
            {r.map((v, j) =>
              j === 0 ? (
                <th key={j} scope="row">
                  {v}
                </th>
              ) : (
                <td key={j}>{v}</td>
              ),
            )}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export function ChartTableDisclosure({ summary, ...table }: TableProps & { summary: string }) {
  const [open, setOpen] = React.useState(false);
  return (
    <details data-slot="chart-table-disclosure" className={`lumen-chart-table-disclosure`} onToggle={(e) => setOpen(e.currentTarget.open)}>
      <summary>{summary}</summary>
      {open && <ChartTableMarkup {...table} />}
    </details>
  );
}
```

### lib/beautiful-ui/lumen/chart-config.ts

```tsx
/*
 * Lumen Chart's core: shadcn's ChartConfig, its colours as CSS variables, data adapters and the
 * table policy. Plain data in and out, no React, so a Server Component can prepare a chart.
 */

/** shadcn's ChartConfig: one entry per series key. Plain data, so it crosses the server/client boundary. */
export type ChartConfig = Record<
  string,
  {
    /** Shown in legends, tooltips and the table. */
    label?: string;
  } & ({ color?: string; theme?: never } | { color?: never; theme: { light: string; dark: string } })
>;

// Keys become CSS custom property names and ids go in a selector inside a <style>: both are escaped
// (injectively, so "a.b" and "a?b" stay two series), and values that could end a rule are dropped.
/** A key as a CSS identifier fragment, like CSS.escape: word characters and "-" as they are, the rest escaped. */
export function chartKey(k: string) {
  return Array.from(k)
    .map((ch) => (/[\w-]/.test(ch) && ch.charCodeAt(0) < 128 ? ch : `\\${ch.codePointAt(0)!.toString(16)} `))
    .join("");
}
/** A string as a quoted CSS string that can't close the string, the rule or the <style>. */
const cssString = (v: string) => `"${Array.from(v).map((ch) => (/["\\<>\n\r\f]/.test(ch) ? `\\${ch.codePointAt(0)!.toString(16)} ` : ch)).join("")}"`;
const safeValue = (v: string) => (/[;{}<>\\]/.test(v) ? "" : v.trim());

/** The CSS for a chart's colours: `--color-<key>` on `[data-chart=<id>]`, light by default and dark under `.dark` or `[data-theme="dark"]`. */
export function chartStyleCss(id: string, config: ChartConfig): string {
  const sel = `[data-chart=${cssString(id)}]`;
  const decls = (mode: "light" | "dark") =>
    Object.entries(config)
      .map(([k, c]) => {
        const v = c.theme ? c.theme[mode] : c.color;
        const s = v ? safeValue(v) : "";
        return s ? `--color-${chartKey(k)}:${s};` : "";
      })
      .join("");
  const light = decls("light");
  const dark = decls("dark");
  // Dark: shadcn's .dark class, or a data-theme="dark" attribute (next-themes' other strategy).
  return [light && `${sel}{${light}}`, dark && dark !== light && `:where(.dark,[data-theme="dark"]) ${sel}{${dark}}`].filter(Boolean).join("\n");
}

/** The colour of a series, as the variable ChartContainer sets: `var(--color-<key>)`. */
export const chartColor = (key: string) => `var(--color-${chartKey(key)})`;

/** A series' label from the config, else the key itself. */
export const chartLabel = (config: ChartConfig, key: string) => config[key]?.label ?? key;

/** "{n} of {total} markets" → "14 of 20 markets". Unknown placeholders stay as written. */
export function interpolate(template: string, values: Record<string, string | number>) {
  return template.replace(/\{(\w+)\}/g, (m, k: string) => (k in values ? String(values[k]) : m));
}

export type LongPoint = { x: string | number; series: string; value: number };

/** shadcn's rows (`{ month: "Jan", desktop: 186, mobile: 80 }`) to long points, skipping values that aren't numbers. */
export function fromRows(rows: readonly Record<string, unknown>[], xKey: string, keys?: readonly string[]): LongPoint[] {
  const out: LongPoint[] = [];
  for (const row of rows) {
    const x = row[xKey];
    if (typeof x !== "string" && typeof x !== "number") continue;
    for (const k of keys ?? Object.keys(row).filter((k) => k !== xKey)) {
      const v = row[k];
      if (typeof v === "number" && Number.isFinite(v)) out.push({ x, series: k, value: v });
    }
  }
  return out;
}

/** A matrix (rows × columns) to sparse cells, skipping nulls and values that aren't numbers. */
export function fromMatrix<R extends string | number, C extends string | number>(matrix: readonly (readonly (number | null | undefined)[])[], rows: readonly R[], columns: readonly C[]): { row: R; column: C; value: number }[] {
  const out: { row: R; column: C; value: number }[] = [];
  matrix.forEach((line, i) =>
    line.forEach((v, j) => {
      const row = rows[i];
      const column = columns[j];
      if (row !== undefined && column !== undefined && typeof v === "number" && Number.isFinite(v)) out.push({ row, column, value: v });
    }),
  );
  return out;
}

/** Up to this many cells, ChartTable is rendered for screen readers; above it, on demand. */
export const CHART_TABLE_LIMIT = 500;

const warned = new Set<string>();
/** Warns once per message in development; never throws. */
export function chartWarn(message: string) {
  // Read through globalThis: Vite apps have no Node types (and no process at runtime).
  const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.NODE_ENV;
  if (env === "production" || warned.has(message)) return;
  warned.add(message);
  console.warn(`[lumen-chart] ${message}`);
}

/** Finite numbers only (and non-negative where negatives are impossible); warns once about what it drops. */
export function cleanValues<T>(items: readonly T[], value: (item: T) => unknown, opts: { name: string; nonNegative?: boolean }): T[] {
  let dropped = 0;
  const out = items.filter((it) => {
    const v = value(it);
    const ok = typeof v === "number" && Number.isFinite(v) && (!opts.nonNegative || v >= 0);
    if (!ok) dropped++;
    return ok;
  });
  if (dropped) chartWarn(`${opts.name}: skipped ${dropped} invalid point${dropped === 1 ? "" : "s"}`);
  return out;
}
```

### 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/indicator.ts

```tsx
"use client";

import * as React from "react";

/*
 * Where the active item sits inside a list, as CSS variables on the list: --ind-x, --ind-y, --ind-w,
 * --ind-h (px, relative to the list's padding box). Lumen's sliding thumbs, lenses and filaments read
 * them, so the same markup animates on Base UI and Radix alike. `data-ind-still` is set for the first
 * measurement and on resize, so the indicator appears in place instead of sliding in from 0.
 */

const ACTIVE = '[aria-selected="true"],[data-active],[data-state="active"],[aria-pressed="true"],[data-pressed],[data-state="on"]';

// Measure before paint on the client (no frame without the thumb); a plain effect on the server.
const useIsoLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

// The item's box within the list. Offsets ignore transforms (a menu opening on a scale), but they are
// relative to the nearest positioned ancestor (a pagination or navigation <li>), so walk up to the list.
function offsetWithin(el: HTMLElement, list: HTMLElement) {
  let x = 0;
  let y = 0;
  for (let n: HTMLElement | null = el; n && n !== list; ) {
    x += n.offsetLeft;
    y += n.offsetTop;
    const parent = n.offsetParent as HTMLElement | null;
    if (!parent || parent === list || !list.contains(parent)) break;
    x += parent.clientLeft;
    y += parent.clientTop;
    n = parent;
  }
  return { x, y };
}

/**
 * `target` is a ref for lists that are always mounted (tabs, toggle groups), or the element itself
 * for popups (menus, select, combobox): a popup's element only exists while it is open, so pass it
 * through a callback ref (useState) and the indicator attaches each time it opens.
 */
export function useActiveIndicator(target: React.RefObject<HTMLElement | null> | HTMLElement | null, active = ACTIVE, enabled = true) {
  useIsoLayoutEffect(() => {
    const list = target && "current" in target ? target.current : target;
    if (!list || !enabled) return;
    let frame = 0;
    let measured = false;
    const place = (still: boolean) => {
      frame = 0;
      const el = list.querySelector<HTMLElement>(active);
      if (!el) {
        list.removeAttribute("data-ind-ready");
        return;
      }
      if (still || !measured) list.setAttribute("data-ind-still", "");
      const { x, y } = offsetWithin(el, list);
      list.style.setProperty("--ind-x", `${x}px`);
      list.style.setProperty("--ind-y", `${y}px`);
      list.style.setProperty("--ind-w", `${el.offsetWidth}px`);
      list.style.setProperty("--ind-h", `${el.offsetHeight}px`);
      list.setAttribute("data-ind-ready", "");
      measured = true;
      // Let the still position paint, then allow the next change to glide.
      requestAnimationFrame(() => requestAnimationFrame(() => list.removeAttribute("data-ind-still")));
    };
    const schedule = (still: boolean) => {
      if (frame) cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => place(still));
    };
    place(true);
    const ro = new ResizeObserver(() => schedule(true));
    const watchChildren = () => {
      ro.observe(list);
      for (const child of Array.from(list.children)) ro.observe(child);
    };
    watchChildren();
    // Selection changes glide; tabs added, removed or reordered, or a direction flip, re-place it.
    const mo = new MutationObserver((records) => {
      const structural = records.some((r) => r.type === "childList" || r.attributeName === "dir");
      if (structural) watchChildren();
      schedule(structural);
    });
    mo.observe(list, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected", "aria-checked", "aria-current", "data-active", "data-state", "aria-pressed", "data-pressed", "data-highlighted", "data-popup-open", "aria-expanded", "dir"] });
    const html = document.documentElement;
    const dirMo = new MutationObserver(() => schedule(true));
    // A direction flip anywhere above (a wrapper, the page) moves equal-sized items without resizing them.
    dirMo.observe(html, { attributes: true, subtree: true, attributeFilter: ["dir"] });
    return () => {
      mo.disconnect();
      dirMo.disconnect();
      ro.disconnect();
      if (frame) cancelAnimationFrame(frame);
    };
  }, [target, active, enabled]);
}
```

### 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 {
  --lc-body: linear-gradient(180deg,rgba(255,255,255,.94) 0%,rgba(250,251,253,.88) 44%,rgba(244,246,250,.92) 100%);
  --lc-shadow: 0 0 0 1px rgba(20,24,40,.07),0 1px 2px rgba(20,24,40,.06),0 16px 32px -16px rgba(20,24,40,.18),0 40px 80px -30px rgba(20,24,40,.28);
  --lc-rim: linear-gradient(180deg,rgba(255,255,255,.95),rgba(255,255,255,.4) 40%,rgba(255,255,255,.7));
  --lc-sheen: linear-gradient(180deg,rgba(255,255,255,.7) 0%,rgba(255,255,255,.2) 12%,transparent 26%);
}
.dark, [data-theme="dark"] {
  --lc-body: linear-gradient(180deg,rgba(34,36,41,.8) 0%,rgba(12,13,15,.86) 44%,rgba(10,11,13,.88) 100%);
  --lc-shadow: 0 0 0 1px rgba(0,0,0,.85),0 1px 1px rgba(0,0,0,.5),0 16px 32px -16px rgba(0,0,0,.8),0 40px 80px -30px rgba(0,0,0,.95);
  --lc-rim: linear-gradient(180deg,rgba(255,255,255,.24) 0%,rgba(255,255,255,.08) 8%,rgba(255,255,255,.03) 36%,rgba(255,255,255,.03) 86%,rgba(255,255,255,.08) 100%);
  --lc-sheen: linear-gradient(180deg,rgba(255,255,255,.06) 0%,rgba(255,255,255,.015) 10%,transparent 20%);
}
@layer components {
  .lumen-chart {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
    color: var(--gc-ink);
    font: 400 13px/1.45 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-surface {
    position: relative;
    isolation: isolate;
    display: flex;
    flex-direction: column;
    gap: 20px;
    box-sizing: border-box;
    min-width: 0;
    margin: 0;
    padding: 28px 30px 24px;
    border-radius: calc(30px * var(--lumen-radius-k, 1));
    background: var(--lc-body);
    -webkit-backdrop-filter: blur(26px) saturate(160%);
    backdrop-filter: blur(26px) saturate(160%);
    box-shadow: var(--lc-shadow);
    color: var(--gc-ink);
    font: 400 13px/1.45 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-surface::before {
    content: "";
    position: absolute;
    inset: 0;
    padding: 1px;
    border-radius: inherit;
    background: var(--lc-rim);
    -webkit-mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    pointer-events: none;
  }
  .lumen-chart-surface::after {
    content: "";
    position: absolute;
    inset: 1px;
    z-index: -1;
    border-radius: calc(29px * var(--lumen-radius-k, 1));
    background: var(--lc-sheen);
    pointer-events: none;
  }
  .lumen-chart-header {
    display: grid;
    gap: 6px;
    min-width: 0;
  }
  .lumen-chart-eyebrow {
    display: flex;
    align-items: center;
    gap: 8px;
    font: 500 10.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--gc-sec);
  }
  .lumen-chart-eyebrow[data-live]::before {
    content: "";
    width: 6px;
    height: 6px;
    flex: none;
    border-radius: calc(1.5px * var(--lumen-radius-k, 1));
    background: var(--gc-good);
    box-shadow: 0 0 0 3px color-mix(in srgb,var(--gc-good) 18%,transparent),0 0 8px color-mix(in srgb,var(--gc-good) 60%,transparent);
  }
  .lumen-chart-title {
    margin: 0;
    font: 600 26px/1.15 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: -.035em;
    color: var(--gc-ink);
    text-wrap: balance;
  }
  .lumen-chart-description {
    margin: 0;
    max-width: 62ch;
    font: 400 13.5px/1.5 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-sec);
  }
  .lumen-chart-readout {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 10px;
    min-width: 0;
    color: var(--gc-sec);
  }
  .lumen-chart-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
  }
  .lumen-chart-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font: 500 12px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-sec);
  }
  button.lumen-chart-legend-item {
    appearance: none;
    margin: -4px -8px;
    padding: 4px 8px;
    border: 0;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
    background: transparent;
    cursor: pointer;
    transition: background min(0.2s, var(--gcp-cm)) ease,color min(0.2s, var(--gcp-cm)) ease;
  }
  button.lumen-chart-legend-item:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  button.lumen-chart-legend-item[aria-pressed="false"] {
    color: var(--gc-m45);
  }
  .lumen-chart-legend-swatch {
    width: 10px;
    height: 10px;
    flex: none;
    border-radius: calc(2.5px * var(--lumen-radius-k, 1));
    background: var(--lc-c);
    transition: background min(0.2s, var(--gcp-cm)) ease,box-shadow min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-legend-swatch[data-indicator="line"] {
    width: 14px;
    height: 2px;
    border-radius: calc(1px * var(--lumen-radius-k, 1));
  }
  [aria-pressed="false"]>.lumen-chart-legend-swatch {
    background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--lc-c);
  }
  .lumen-chart-legend-value {
    font: 500 12px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-plot {
    position: relative;
    width: 100%;
    min-width: 0;
  }
  .lumen-chart-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 160px;
    font: 500 12px/1.4 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .04em;
    color: var(--gc-sec);
    text-align: center;
  }
  .lumen-chart-plot>.lumen-chart-state {
    position: absolute;
    inset: 0;
    min-height: 0;
  }
  .lumen-chart-empty-cells {
    display: flex;
    gap: 3px;
  }
  .lumen-chart-empty-cells>i {
    width: 6px;
    height: 6px;
    border-radius: calc(1.5px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
  }
  .lumen-chart-loading-ticker {
    font-size: 20px;
    color: var(--gc-acc-ui);
  }
  .lumen-chart-table-disclosure {
    max-width: 100%;
    overflow-x: auto;
  }
  .lumen-chart-table-disclosure>summary {
    width: fit-content;
    padding: 4px 0;
    font: 500 12px/1.4 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-sec);
    cursor: pointer;
  }
  .lumen-chart-table-disclosure>summary:focus-visible {
    outline: none;
    border-radius: calc(6px * var(--lumen-radius-k, 1));
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-table {
    width: 100%;
    border-collapse: collapse;
    font: 400 12px/1.4 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-table caption {
    padding: 8px 0;
    text-align: start;
    color: var(--gc-sec);
  }
  .lumen-chart-table :is(th,td) {
    padding: 6px 8px;
    border-bottom: 1px solid var(--gc-hair);
    text-align: end;
    white-space: nowrap;
  }
  .lumen-chart-table :is(thead th,tbody th) {
    text-align: start;
    font-weight: 500;
    color: var(--gc-sec);
  }
  .lumen-chart-tooltip {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 5;
    box-sizing: border-box;
    min-width: min(160px,calc(100% - 16px));
    max-width: min(240px,calc(100% - 16px));
    padding: 10px 12px;
    border-radius: calc(14px * var(--lumen-radius-k, 1));
    background: var(--gc-tipBg);
    box-shadow: var(--gc-tipSh);
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    color: var(--gc-ink);
    font: 400 12px/1.45 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--gc-t-fade) ease,visibility 0s linear var(--gc-t-fade);
  }
  .lumen-chart-tooltip[data-open] {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--gc-t-fade) ease;
  }
  .lumen-chart-tooltip-header {
    margin-bottom: 6px;
    font: 500 10.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--gc-sec);
  }
  .lumen-chart-tooltip-row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 20px;
  }
  .lumen-chart-tooltip-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--gc-sec);
  }
  .lumen-chart-tooltip-value {
    font: 500 12px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-modes {
    position: relative;
    display: inline-flex;
    align-items: center;
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
    padding: 3px;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    background: var(--gc-p-track);
    box-shadow: var(--gc-p-trackSh);
    overflow-x: auto;
    scrollbar-width: none;
  }
  .lumen-chart-modes::-webkit-scrollbar {
    display: none;
  }
  .lumen-chart-modes-thumb {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--ind-w,0);
    height: var(--ind-h,0);
    transform: translate(var(--ind-x,0),var(--ind-y,0));
    border-radius: calc(9px * var(--lumen-radius-k, 1));
    background: var(--gc-p-thumb);
    box-shadow: var(--gc-p-thumbSh);
    opacity: 0;
    pointer-events: none;
    transition: transform calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),width calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),opacity min(0.3s, var(--gcp-cm)) ease;
  }
  .lumen-chart-modes[data-ind-ready]>.lumen-chart-modes-thumb {
    opacity: 1;
  }
  .lumen-chart-modes[data-ind-still]>.lumen-chart-modes-thumb {
    transition: none;
  }
  .lumen-chart-mode {
    position: relative;
    z-index: 1;
    flex: none;
    appearance: none;
    height: 30px;
    margin: 0;
    padding: 0 12px;
    border: 0;
    border-radius: calc(9px * var(--lumen-radius-k, 1));
    background: transparent;
    font: 500 12.5px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-p-m62);
    white-space: nowrap;
    cursor: pointer;
    transition: color min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-chart-mode[aria-checked="true"] {
    color: var(--gc-p-thumbInk);
  }
  .lumen-chart-modes:not([data-ind-ready])>.lumen-chart-mode[aria-checked="true"] {
    background: var(--gc-p-thumb);
    box-shadow: var(--gc-p-thumbSh);
  }
  @media (hover:hover) {
    button.lumen-chart-legend-item:hover {
      background: var(--gc-p-hov);
      color: var(--gc-ink);
    }
    .lumen-chart-mode[aria-checked="false"]:not(:disabled):hover {
      color: var(--gc-ink);
    }
  }
  .lumen-chart-mode:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-mode:disabled {
    opacity: .4;
    cursor: not-allowed;
  }
  @media (max-width: 699px) {
    .lumen-chart-surface {
      padding: 22px 18px 20px;
      border-radius: calc(24px * var(--lumen-radius-k, 1));
    }
    .lumen-chart-surface::after {
      border-radius: calc(23px * var(--lumen-radius-k, 1));
    }
    .lumen-chart-title {
      font-size: 21px;
    }
    .lumen-chart-mode {
      height: 36px;
      padding: 0 14px;
    }
  }
}
```

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.
