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

- id: `lumen-radial-bar-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-radial-bar-chart.json`
- npm dependencies: recharts@^3
- registry dependencies: utils, @beautiful-ui/lumen-chart, @beautiful-ui/lumen-stat, https://beautiful-ui.dev/r/lumen-foundation.json
- docs: https://beautiful-ui.dev/components/lumen-radial-bar-chart
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Progress rings against targets, one ring per measure. Each ring closes at its own target and laps again, brighter, when it passes it; or put every ring on one scale with a tick at each target. The centre and the list beside it say the focused ring in words.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `recharts@^3`.
- Files: `components/ui/lumen/charts/radial-bar-chart.tsx`, `components/ui/lumen/charts/chart-frame.tsx`, `components/ui/lumen/charts/cartesian.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `lumen-chart`, `lumen-stat`, `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: `RadialBarChart`, 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-mono`, `--lumen-font-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { RadialBarChart } from "@/components/ui/lumen/charts/radial-bar-chart";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `RadialBarChart` | — | The rings, centre, list, states, summary and table. |

## 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/charts/radial-bar-chart.tsx`, `components/ui/lumen/charts/chart-frame.tsx`, `components/ui/lumen/charts/cartesian.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
- radial bar chart, progress rings, goal rings, activity rings, target attainment, quota, OKR progress, gauges
- A few measures against their own targets (quota, goals, budgets), where 'are we there' matters more than exact values

### Not when
- Comparing amounts precisely: use a Bar or Bullet Chart
- More than six rings: the inner ones get too short to read; use a Bullet Chart

## Mistakes
- Keep data and config stable (outside the component, or memoised)
- Rings on the target scale compare attainment, not size: a full small ring and a full large ring look the same. Use scale="shared" when the sizes matter

## Usage

```tsx
"use client";
import { RadialBarChart } from "@/components/ui/lumen/charts/radial-bar-chart";

const goals = [
  { goal: "revenue", actual: 1180000, target: 1000000 },
  { goal: "pipeline", actual: 2640000, target: 3200000 },
  { goal: "renewals", actual: 410000, target: 520000 },
];

const config = {
  revenue: { label: "New revenue", color: "var(--chart-1)" },
  pipeline: { label: "Pipeline", color: "var(--chart-2)" },
  renewals: { label: "Renewals", color: "var(--chart-3)" },
};

export function QuarterGoals() {
  return (
    <RadialBarChart
      data={goals}
      name="goal"
      value="actual"
      target="target"
      config={config}
      valueFormat={{ style: "currency", notation: "compact" }}
      label="Quarter goals against target"
    />
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `data / name / value / target` | `Row[] / field / field / field` |  | One row per ring: its name, the measured value and (optionally) its target. |
| `config` | `ChartConfig` |  | Colour and label per ring name. Unlisted rings take the series colours in order. |
| `scale` | `"target" \| "shared"` | `"target"` | target: each ring closes at its own target and overshoot laps again. shared: one scale (0 to max) with a tick at each target. |
| `max` | `number` |  | The full ring on the shared scale. Default: the largest value or target. |
| `valueFormat` | `StatFormat \| (n) => string` |  | Numbers in the list, centre, table and summary. |
| `legend` | `"list" \| "none"` | `"list"` | Each ring's value, target and share beside the rings (below on narrow cards). |
| `active / onActiveChange` | `string \| null / (name) => void` |  | The focused ring, controlled; omit for uncontrolled. |
| `onSelect` | `(name) => void` |  | A ring or list entry was clicked, or Enter pressed on a focused ring. |
| `labels` | `{ summary?, ofTarget?, noData? }` |  | Words for i18n: summary(met, total) under the centre figure. |
| `thickness / height` | `number / number` | `auto / 280` | Ring thickness (in the 200px drawing) and plot height. |
| `motion` | `"full" \| "subtle" \| "off"` | `"full"` | Rings draw round in turn; reduced motion always wins. |
| `loading / error / emptyText / label` | `boolean / string / string / string` |  | States in words with the height held, and the chart's accessible name. |

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

## Customising
- Motion: `motion="full"` (default) | `"subtle"` | `"off"`. prefers-reduced-motion is always respected.
- 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 |
|---|---|
| Tab | Focus the rings, then each list entry |
| ← / → / ↑ / ↓ | Move between rings; the centre and the live region say it |
| Home / End | Outermost or innermost ring |
| Enter / Space | Select the focused ring (onSelect) |
| Escape | Clear the focus |

## Performance

- Plain SVG, a few circles per ring; no chart library work at runtime

## Responsive

- The rings keep a square and fill the plot height; under 520px of card width the list moves below the rings

## Motion inventory

| Interaction | What moves |
|---|---|
| Draw | Each ring draws round from twelve o'clock in turn; an overshoot lap follows (motion="subtle" draws them together, faster) |
| Change | Rings ease to their new length |
| Focus | The focused ring glows and the rest dim |

## Accessibility contract (preserve when editing)
- A labelled figure; the rings are one tab stop (role application) and arrow keys move between them, said through a polite live region
- The list beside the rings is buttons; focusing one lights its ring
- Share of target is written as a percent, so colour is never the only cue; met targets also read in the good colour
- A one-sentence summary and the full table for screen readers; negative values are refused in words, missing values are named

## Install

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

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

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

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

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

## Credits
- Built on shadcn/ui charts conventions (https://ui.shadcn.com/charts)

## Source (Base UI build)

### components/ui/lumen/charts/radial-bar-chart.tsx

```tsx
"use client";
// Generated from src/registry/core/charts/radial-bar-chart.tsx by scripts/gen-systems.ts. Edit the core file, not this one.

import * as React from "react";
import { useChartKeyboard, type ChartConfig } from "@/components/ui/lumen/chart";
import { formatValue, type ChartMotion, type ChartRow, type ValueFormat } from "@/components/ui/lumen/charts/cartesian";
import { ChartFrame, categoryColor, fieldLabel, num } from "@/components/ui/lumen/charts/chart-frame";

/*
 * Radial Bar Chart: progress rings against targets, one ring per measure, outermost first. On the
 * default "target" scale every ring closes at its own target, so one glance says which are there;
 * past the target the ring keeps going as a brighter second lap. On a "shared" scale (one unit for
 * every ring) each ring carries a tick at its target. The centre says the focused ring in words;
 * arrow keys walk the rings and the legend beside them does the same with a pointer.
 */

export type RadialBarChartProps<R extends ChartRow> = {
  data: readonly R[];
  /** The field naming each ring (a config key when you want to set its colour and label). */
  name: keyof R & string;
  /** The measured value. */
  value: keyof R & string;
  /** The target for each ring. Without one, rings measure against `max`. */
  target?: (keyof R & string) | undefined;
  /** Colours and labels per ring name (shadcn's shape). Unlisted rings take the series colours in order. */
  config?: ChartConfig | undefined;
  valueFormat?: ValueFormat | undefined;
  /**
   * "target" (default): every ring closes at its own target, and overshoot laps again.
   * "shared": one scale for every ring (0 to `max`), with a tick at each target.
   */
  scale?: "target" | "shared" | undefined;
  /** The full ring's value on the shared scale. Default: the largest value or target. */
  max?: number | undefined;
  /** The words under the centre figure before a ring is focused. Default "{n} of {total} on target". */
  labels?: { summary?: ((met: number, total: number) => string) | undefined; ofTarget?: string | undefined; noData?: string | undefined } | undefined;
  /** Ring thickness in px of the 200px drawing. Default: fits the rings to the space. */
  thickness?: number | undefined;
  /** Plot height in px. Default 280. */
  height?: number | undefined;
  /** "list" (default): each ring's value, target and share beside the plot; "none". */
  legend?: "list" | "none" | undefined;
  motion?: ChartMotion | undefined;
  loading?: boolean | undefined;
  error?: string | null | undefined;
  emptyText?: string | undefined;
  label?: string | undefined;
  /** The focused ring (its name), controlled. */
  active?: string | null | undefined;
  onActiveChange?: ((name: string | null) => void) | undefined;
  onSelect?: ((name: string) => void) | undefined;
  className?: string | undefined;
};

export type RadialRing = { key: string; label: string; value: number | null; target: number | null; color: string; /** Share of the target (or of max on the shared scale). */ ratio: number | null };

/** The rings' model: values, targets and how far round each one goes. */
export function radialRings(data: readonly ChartRow[], name: string, value: string, target: string | undefined, config: ChartConfig, scale: "target" | "shared", max?: number): { rings: RadialRing[]; full: number } {
  const base = data.map((r, i) => {
    const key = String(r[name] ?? "");
    // A target of zero or less can't be reached or shared: treat it as no target, everywhere.
    const t = target ? num(r[target]) : null;
    return { key, label: config[key]?.label ?? key, value: num(r[value]), target: t != null && t > 0 ? t : null, color: categoryColor(config, key, i) };
  });
  const full = max ?? Math.max(0, ...base.flatMap((r) => [r.value ?? 0, r.target ?? 0]));
  const rings = base.map((r) => {
    const denom = scale === "target" && r.target != null && r.target > 0 ? r.target : full;
    return { ...r, ratio: r.value == null || !(denom > 0) ? null : Math.max(0, r.value) / denom };
  });
  return { rings, full };
}

const C = 100; // pathLength: arcs are measured in percent of the circle.

function RadialBarChart<R extends ChartRow>({
  data,
  name,
  value,
  target,
  config = {},
  valueFormat,
  scale = "target",
  max,
  labels,
  thickness,
  height = 280,
  legend = "list",
  motion,
  loading,
  error,
  emptyText,
  label,
  active: activeProp,
  onActiveChange,
  onSelect,
  className,
}: RadialBarChartProps<R>) {
  const { rings, full } = React.useMemo(() => radialRings(data, name, value, target, config, scale, max), [data, name, value, target, config, scale, max]);
  const [inner, setInner] = React.useState<string | null>(null);
  const activeKey = activeProp !== undefined ? activeProp : inner;
  const setActive = (k: string | null) => {
    if (activeProp === undefined) setInner(k);
    onActiveChange?.(k);
  };
  const index = rings.findIndex((r) => r.key === activeKey);
  const [live, setLive] = React.useState("");
  const negative = rings.some((r) => r.value != null && r.value < 0);
  const problem = error ?? (negative ? "Rings show progress, so values must be zero or more. Show signed values in a bar chart instead." : null);
  const name_ = label ?? `Radial bar chart of ${String(value)} by ${String(name)}`;
  const pct = (r: number | null) => (r == null ? "—" : `${Math.round(r * 100)}%`);
  const ofTarget = labels?.ofTarget ?? "of target";
  const noData = labels?.noData ?? "No data";
  const withTargets = rings.filter((r) => r.target != null);
  const met = withTargets.filter((r) => r.value != null && r.target != null && r.value >= r.target).length;
  const words = (r: RadialRing) => (r.value == null ? `${r.label}: ${noData.toLowerCase()}` : `${r.label}: ${formatValue(r.value, valueFormat)}${r.target != null ? ` of ${formatValue(r.target, valueFormat)} target, ${pct(r.value / (r.target || 1))}` : ""}`);

  // Geometry in a 200 × 200 drawing: rings from the outside in, leaving a hole for the centre words.
  const R = 96;
  const hole = 46;
  const n = Math.max(1, rings.length);
  const gap = n > 1 ? Math.max(2, Math.min(5, 24 / n)) : 0;
  const w = thickness ?? Math.max(5, Math.min(18, (R - hole - gap * (n - 1)) / n));
  const radiusOf = (i: number) => R - w / 2 - i * (w + gap);

  const kb = useChartKeyboard({
    count: rings.length,
    index,
    onIndexChange: (i) => {
      const r = rings[i];
      if (!r) return;
      setActive(r.key);
      setLive(words(r));
    },
    onPin: onSelect ? (i) => rings[i] && onSelect(rings[i].key) : undefined,
    onClear: () => setActive(null),
  });

  const shown = index >= 0 ? rings[index] : null;
  const summaryWords = labels?.summary ?? ((m: number, t: number) => `${m} of ${t} on target`);
  const summary = `${name_}: ${rings.map(words).join("; ")}.${withTargets.length ? ` ${summaryWords(met, withTargets.length)}.` : ""}`;

  return (
    <ChartFrame
      kind="radial"
      config={config}
      name={name_}
      height={height}
      fit
      loading={loading}
      error={problem}
      empty={rings.length === 0}
      emptyText={emptyText}
      motion={motion}
      className={className}
      summary={summary}
      live={live}
      columns={[fieldLabel(String(name)), fieldLabel(String(value)), ...(target ? [fieldLabel(String(target)), "Share"] : [])]}
      rows={rings.map((r) => [r.label, formatValue(r.value, valueFormat), ...(target ? [formatValue(r.target, valueFormat), r.value != null && r.target ? pct(r.value / r.target) : "—"] : [])])}
    >
      <div className={`lumen-chart-radial-body`} data-legend={legend}>
        <div className={`lumen-chart-radial-ring`} style={{ height }}>
          <svg
            viewBox="0 0 200 200"
            role="application"
            aria-roledescription="radial bar chart"
            aria-label={`${name_}. Arrow keys move between rings.`}
            tabIndex={0}
            className={`lumen-chart-radial-svg`}
            onKeyDown={kb}
            onBlur={() => setActive(null)}
          >
            {rings.map((r, i) => {
              const rad = radiusOf(i);
              const ratio = r.ratio ?? 0;
              const first = Math.min(1, ratio);
              const lap = Math.min(1, Math.max(0, ratio - 1));
              const on = r.key === activeKey;
              const tickAt = scale === "shared" && r.target != null && full > 0 ? (r.target / full) * 2 * Math.PI : null;
              return (
                <g
                  key={r.key}
                  className={`lumen-chart-radial-item`}
                  data-active={on || undefined}
                  data-dim={activeKey != null && !on ? "" : undefined}
                  data-missing={r.value == null || undefined}
                  data-over={ratio > 1 || undefined}
                  style={{ color: r.color, "--i": i } as React.CSSProperties}
                  transform="rotate(-90 100 100)"
                  onPointerEnter={() => setActive(r.key)}
                  onPointerLeave={() => setActive(null)}
                  onClick={() => onSelect?.(r.key)}
                >
                  <circle cx={100} cy={100} r={rad} strokeWidth={w} className={`lumen-chart-radial-track`} />
                  {first > 0 && <circle cx={100} cy={100} r={rad} strokeWidth={w} pathLength={C} strokeDasharray={`${first * C} ${C}`} className={`lumen-chart-radial-fill`} />}
                  {lap > 0 && <circle cx={100} cy={100} r={rad} strokeWidth={w} pathLength={C} strokeDasharray={`${lap * C} ${C}`} className={`lumen-chart-radial-lap`} />}
                  {tickAt != null && <line x1={100 + Math.cos(tickAt) * (rad - w / 2 - 1.5)} y1={100 + Math.sin(tickAt) * (rad - w / 2 - 1.5)} x2={100 + Math.cos(tickAt) * (rad + w / 2 + 1.5)} y2={100 + Math.sin(tickAt) * (rad + w / 2 + 1.5)} className={`lumen-chart-radial-target`} />}
                  {/* A wide invisible ring to point at. */}
                  <circle cx={100} cy={100} r={rad} strokeWidth={w + gap} className={`lumen-chart-radial-hit`} />
                </g>
              );
            })}
            {scale === "target" && withTargets.length > 0 && <line x1={100} y1={100 - R - 3} x2={100} y2={100 - radiusOf(n - 1) + w / 2 + 2} className={`lumen-chart-radial-goal`} />}
          </svg>
          <div className={`lumen-chart-radial-center`} aria-hidden>
            <span className={`lumen-chart-radial-value`}>{shown ? (shown.value == null ? "—" : shown.target != null ? pct(shown.value / (shown.target || 1)) : formatValue(shown.value, valueFormat)) : withTargets.length ? `${met}/${withTargets.length}` : formatValue(rings.reduce((s, r) => s + (r.value ?? 0), 0), valueFormat)}</span>
            <span className={`lumen-chart-radial-caption`}>{shown ? (shown.value == null ? noData : shown.target != null ? `${shown.label} · ${ofTarget}` : shown.label) : withTargets.length ? summaryWords(met, withTargets.length).replace(/^\d+ of \d+ /, "") : "Total"}</span>
          </div>
        </div>
        {legend === "list" && (
          <div role="list" data-slot="chart-legend" className={`lumen-chart-radial-legend`}>
            {rings.map((r) => (
              <span role="listitem" key={r.key} className="contents">
                <button
                  type="button"
                  className={`lumen-chart-radial-entry`}
                  data-active={r.key === activeKey || undefined}
                  style={{ "--lc-c": r.color } as React.CSSProperties}
                  onPointerEnter={() => setActive(r.key)}
                  onPointerLeave={() => setActive(null)}
                  onFocus={() => setActive(r.key)}
                  onBlur={() => setActive(null)}
                  onClick={() => onSelect?.(r.key)}
                >
                  <span aria-hidden className={`lumen-chart-legend-swatch`} />
                  <span className={`lumen-chart-radial-entry-label`}>{r.label}</span>
                  <span className={`lumen-chart-radial-entry-value`}>
                    {r.value == null ? noData : formatValue(r.value, valueFormat)}
                    {r.target != null && <span className={`lumen-chart-radial-entry-target`}> / {formatValue(r.target, valueFormat)}</span>}
                  </span>
                  {r.target != null && (
                    <span className={`lumen-chart-radial-entry-share`} data-met={r.value != null && r.value >= r.target ? "" : undefined}>
                      {r.value == null ? "—" : pct(r.value / (r.target || 1))}
                    </span>
                  )}
                </button>
              </span>
            ))}
          </div>
        )}
      </div>
    </ChartFrame>
  );
}

export { RadialBarChart };
```

### components/ui/lumen/charts/chart-frame.tsx

```tsx
"use client";
// Generated from src/registry/core/charts/chart-frame.tsx by scripts/gen-systems.ts. Edit the core file, not this one.

import * as React from "react";
import { cn } from "@/lib/utils";
import { ChartContainer, ChartEmpty, ChartLive, ChartLoading, ChartPlot, ChartTable, chartColor, type ChartConfig } from "@/components/ui/lumen/chart";
import { useChartMotion, type ChartMotion } from "@/components/ui/lumen/charts/cartesian";

/*
 * The frame the hand-drawn chart types share (Radial Bar, Treemap, Sankey, Slope, Waffle, Bullet,
 * Candlestick): the container with its ChartConfig colours, a plot that holds its height through
 * loading, error and empty, an optional legend, a polite live region for what the keyboard finds,
 * and the one-sentence summary and data table screen readers get. Motion is one attribute the CSS
 * reads: "off" whenever reduced motion is asked for.
 */

export type ChartFrameProps = {
  /** The chart's class: "radial" → lumen-chart-radial. */
  kind: string;
  config: ChartConfig;
  /** The accessible name, and the table's caption. */
  name: string;
  /** Plot height in px: held through loading, error and empty. */
  height: number;
  /** Once drawn, let the plot size itself to its content (the height still holds the states). */
  fit?: boolean | undefined;
  loading?: boolean | undefined;
  error?: string | null | undefined;
  /** True when there's nothing to draw (and no loading or error). */
  empty: boolean;
  emptyText?: string | undefined;
  /** Above the plot: a breadcrumb, a mode switch. */
  before?: React.ReactNode;
  /** Below the plot: the legend. Hidden while loading, in error or empty. */
  legend?: React.ReactNode;
  /** One sentence about the data. */
  summary: string;
  columns: string[];
  rows: (string | number)[][];
  /** What the live region says (the focused point, in words). */
  live?: string | undefined;
  motion?: ChartMotion | undefined;
  className?: string | undefined;
  plotProps?: React.ComponentProps<"div"> | undefined;
  children: React.ReactNode;
};

export function ChartFrame({ kind, config, name, height, fit, loading, error, empty, emptyText, before, legend, summary, columns, rows, live, motion, className, plotProps, children }: ChartFrameProps) {
  const { animate } = useChartMotion(motion);
  const drawn = !loading && !error && !empty;
  return (
    <ChartContainer config={config} className={cn(`lumen-chart-frame`, `lumen-chart-${kind}`, className)} role="figure" aria-label={name} data-motion={animate ? (motion ?? "full") : "off"}>
      {drawn && before}
      <ChartPlot {...plotProps} style={{ ...(fit && drawn ? { minHeight: height } : { height }), ...plotProps?.style }} aria-busy={loading || undefined}>
        {loading ? (
          <ChartLoading />
        ) : error ? (
          <div role="alert" className={`lumen-chart-error`}>
            {error}
          </div>
        ) : empty ? (
          <ChartEmpty>{emptyText ?? "No data yet"}</ChartEmpty>
        ) : (
          children
        )}
      </ChartPlot>
      {drawn && legend}
      <ChartLive>{live ?? ""}</ChartLive>
      {drawn && (
        <>
          <p className="sr-only">{summary}</p>
          <ChartTable caption={name} columns={columns} rows={rows} />
        </>
      )}
    </ChartContainer>
  );
}

/**
 * A category's colour: its config entry's (--color-<key>) when it has one, else the theme's series
 * colours in order of first appearance (your --chart-N under lumen-inherit).
 */
export function categoryColor(config: ChartConfig, key: string, index: number): string {
  // Only an entry with a colour (or per-theme colours) emits --color-<key>; a label-only entry doesn't.
  const entry = config[key] as { color?: unknown; theme?: unknown } | undefined;
  return entry && (entry.color || entry.theme) ? chartColor(key) : `var(--lg-c-series-${(index % 5) + 1}, var(--chart-${(index % 5) + 1}, #8b93ff))`;
}

/** A field's name as a column header: "revenue" → "Revenue". */
export const fieldLabel = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

/** A finite number, or null. */
export const num = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) ? v : null);
```

### components/ui/lumen/charts/cartesian.tsx

```tsx
"use client";
// Generated from src/registry/core/charts/cartesian.tsx by scripts/gen-systems.ts. Edit the core file, not this one.

import * as React from "react";
import { CartesianGrid, ReferenceLine, Tooltip, XAxis, YAxis, type TooltipContentProps } from "recharts";
import { cn } from "@/lib/utils";
import { ChartContainer, ChartEmpty, ChartLegend, ChartLegendItem, ChartLoading, ChartPlot, ChartTable, ChartTooltipHeader, ChartTooltipRow, chartColor, chartLabel, type ChartConfig } from "@/components/ui/lumen/chart";
import { formatStatValue, type StatFormat } from "@/components/ui/lumen/stat";

/*
 * The shared core of the conventional cartesian charts (Line, Area, Bar, Composed) on Recharts. One
 * contract for all four: rows in (kept as they are, so your own Recharts children see every field),
 * a shadcn ChartConfig for series, formats through Intl, a legend that can hide series, Lumen's
 * tooltip, labelled reference lines, a table and a one-sentence summary for screen readers, the
 * loading/empty/error states, and one motion setting. Recharts' accessibility layer gives arrow-key
 * inspection. Everything that draws light (edges, glows, gradients) is scoped to the chart instance.
 */

export type ChartRow = Record<string, unknown>;
export type ValueFormat = StatFormat | ((value: number) => string);
export type ChartMotion = "full" | "subtle" | "off";

export type ChartReference = {
  /** A value on the value axis (a target, a limit). */
  y?: number | undefined;
  /** A category on the x axis (a launch, an incident). */
  x?: string | number | undefined;
  label: string;
  tone?: "neutral" | "good" | "bad" | "accent" | undefined;
};

export type CartesianBaseProps<R extends ChartRow> = {
  data: readonly R[];
  /** The field for the x axis (categories or dates as text). */
  x: keyof R & string;
  /** Series: one entry per value field, with its label and colour (shadcn's shape). */
  config: ChartConfig;
  /** Which config keys to draw, in order. Default: every key in config. */
  series?: string[] | undefined;
  /** Numbers on the value axis, tooltip and table: a Stat format, or your own function. */
  valueFormat?: ValueFormat | undefined;
  /** A different format for particular series (a percent beside currency). */
  formats?: Partial<Record<string, ValueFormat>> | undefined;
  /** How x values read (ticks, tooltip header, table). */
  xFormat?: ((value: unknown) => string) | undefined;
  /** The x axis's name in the table and summary. Default: the field name, capitalised. */
  xLabel?: string | undefined;
  /** Plot height in px. Default 280. */
  height?: number | undefined;
  grid?: "horizontal" | "vertical" | "both" | "none" | undefined;
  axes?: { x?: boolean | undefined; y?: boolean | undefined } | undefined;
  /**
   * The value axis's range: "zero" (default) starts at zero; "fit" hugs the data, for rates and
   * slow-moving levels where zero hides every change; or a fixed [min, max]. Bars always start at zero.
   */
  yDomain?: "zero" | "fit" | [number, number] | undefined;
  /** "toggle" (default with 2+ series): buttons that hide and show series. "static", or "none". */
  legend?: "toggle" | "static" | "none" | undefined;
  tooltip?: boolean | undefined;
  references?: ChartReference[] | undefined;
  /** "full" (default) · "subtle" · "off". Reduced motion always wins. */
  motion?: ChartMotion | undefined;
  loading?: boolean | undefined;
  /** An error: the plot shows it in words instead of a chart. */
  error?: string | null | undefined;
  /** Words for no data. */
  emptyText?: string | undefined;
  /** Names the chart for assistive tech (and its table caption). */
  label?: string | undefined;
  /** Extra Recharts elements (lines, reference areas, labels). They get the rows unchanged. */
  children?: React.ReactNode;
  className?: string | undefined;
};

export function formatValue(v: unknown, f?: ValueFormat): string {
  if (typeof v !== "number" || !Number.isFinite(v)) return "—";
  return typeof f === "function" ? f(v) : formatStatValue(v, f);
}

const noopSubscribe = () => () => {};
/** Motion as Recharts wants it: whether to animate, and for how long. Reduced motion turns it off. */
export function useChartMotion(motion: ChartMotion = "full") {
  const reduced = React.useSyncExternalStore(
    (cb) => {
      if (typeof window === "undefined") return noopSubscribe();
      const m = window.matchMedia("(prefers-reduced-motion: reduce)");
      m.addEventListener("change", cb);
      return () => m.removeEventListener("change", cb);
    },
    () => window.matchMedia("(prefers-reduced-motion: reduce)").matches,
    () => false,
  );
  const off = reduced || motion === "off";
  return { animate: !off, duration: motion === "subtle" ? 450 : 900 };
}

/** Series keys to draw, and which are hidden by the legend. */
export function useSeries(config: ChartConfig, series?: string[]) {
  const keys = React.useMemo(() => series ?? Object.keys(config), [series, config]);
  const [hidden, setHidden] = React.useState<string[]>([]);
  const toggle = (k: string) => setHidden((h) => (h.includes(k) ? h.filter((x) => x !== k) : [...h, k]));
  return { keys, shown: keys.filter((k) => !hidden.includes(k)), hidden, toggle };
}

/**
 * Percent stacking needs values of zero or more (a negative share has no meaning). Returns the words
 * for the error state when that isn't so, instead of drawing segments outside the plot.
 */
export function percentProblem(data: readonly ChartRow[], keys: string[]): string | null {
  for (const row of data) for (const k of keys) if (typeof row[k] === "number" && (row[k] as number) < 0) return "Percent stacking needs values of zero or more. Show this as stacked or grouped instead.";
  return null;
}

/** One sentence about the data for screen readers: range, highest and lowest per series. */
export function chartSummary(kind: string, data: readonly ChartRow[], x: string, keys: string[], config: ChartConfig, f?: ValueFormat, xf?: (v: unknown) => string, formats?: Partial<Record<string, ValueFormat>>): string {
  const xs = (v: unknown) => (xf ? xf(v) : String(v));
  const parts = keys.map((k) => {
    const pts = data.map((r) => ({ x: r[x], v: r[k] })).filter((p): p is { x: unknown; v: number } => typeof p.v === "number" && Number.isFinite(p.v));
    if (!pts.length) return `${chartLabel(config, k)} has no values`;
    const hi = pts.reduce((a, b) => (b.v > a.v ? b : a));
    const lo = pts.reduce((a, b) => (b.v < a.v ? b : a));
    const fk = formats?.[k] ?? f;
    // pts is non-empty (checked above), so its first and last points exist.
    const first = pts[0]!;
    const lastPt = pts[pts.length - 1]!;
    return `${chartLabel(config, k)} ${formatValue(first.v, fk)} at ${xs(first.x)} and ${formatValue(lastPt.v, fk)} at ${xs(lastPt.x)}; lowest ${formatValue(lo.v, fk)} (${xs(lo.x)}), highest ${formatValue(hi.v, fk)} (${xs(hi.x)})`;
  });
  const span = data.length ? `${xs(data[0]?.[x])} to ${xs(data[data.length - 1]?.[x])}` : "no data";
  return `${kind}, ${span}: ${parts.join("; ")}.`;
}

const TONE: Record<NonNullable<ChartReference["tone"]>, string> = { neutral: "var(--lumen-chart-ref, currentColor)", good: "var(--lumen-good, #1fa36a)", bad: "var(--lumen-bad, #e5484d)", accent: "var(--lumen-accent, #8b93ff)" };

/** Grid, axes and reference lines, drawn the Lumen way. Recharts reads these as direct children. */
export function cartesianFrame({ grid = "horizontal", axes = {}, x, xFormat, valueFormat, references = [], layout = "horizontal", rightAxis = false, rightFormat, yDomain = "zero" }: { grid?: CartesianBaseProps<ChartRow>["grid"] | undefined; axes?: CartesianBaseProps<ChartRow>["axes"] | undefined; yDomain?: CartesianBaseProps<ChartRow>["yDomain"] | undefined; x: string; xFormat?: ((v: unknown) => string) | undefined; valueFormat?: ValueFormat | undefined; references?: ChartReference[] | undefined; layout?: "horizontal" | "vertical" | undefined; rightAxis?: boolean | undefined; rightFormat?: ValueFormat | undefined }) {
  const tick = { className: `lumen-chart-tick` };
  const cat = layout === "horizontal";
  const tickFor = (f?: ValueFormat) => (v: number) => formatValue(v, typeof f === "function" ? f : { ...f, notation: f?.style === "percent" ? undefined : "compact" });
  const valueTick = tickFor(valueFormat);
  // "fit": Recharts' auto domain with nice ticks, which leaves zero out when the data sits far from it.
  const domain = yDomain === "fit" ? (["auto", "auto"] as const) : Array.isArray(yDomain) ? yDomain : ([0, "auto"] as const);
  return [
    grid !== "none" && <CartesianGrid key="grid" className={`lumen-chart-grid`} horizontal={grid === "horizontal" || grid === "both"} vertical={grid === "vertical" || grid === "both"} strokeDasharray="2 4" />,
    <XAxis
      key="x"
      {...(cat ? { dataKey: x, type: "category" as const, ...(xFormat && { tickFormatter: xFormat }) } : { type: "number" as const, tickFormatter: valueTick })}
      hide={axes.x === false}
      tick={tick}
      tickLine={false}
      axisLine={false}
      tickMargin={10}
      minTickGap={12}
    />,
    <YAxis
      key="y"
      yAxisId="left"
      {...(cat ? { type: "number" as const, tickFormatter: valueTick, domain: [...domain] } : { dataKey: x, type: "category" as const, ...(xFormat && { tickFormatter: xFormat }), width: 96 })}
      hide={axes.y === false}
      tick={tick}
      tickLine={false}
      axisLine={false}
      tickMargin={8}
      width={cat ? 52 : 96}
    />,
    rightAxis && <YAxis key="y2" yAxisId="right" orientation="right" type="number" tickFormatter={tickFor(rightFormat ?? valueFormat)} tick={tick} tickLine={false} axisLine={false} tickMargin={8} width={52} />,
    ...references.map((r, i) => (
      <ReferenceLine
        key={`ref-${i}`}
        yAxisId="left"
        {...(r.y != null ? (cat ? { y: r.y } : { x: r.y }) : r.x === undefined ? {} : cat ? { x: r.x } : { y: r.x })}
        stroke={TONE[r.tone ?? "neutral"]}
        strokeDasharray="4 4"
        strokeOpacity={0.8}
        ifOverflow="extendDomain"
        label={{ value: r.label, position: r.y != null ? "insideTopLeft" : "insideTop", className: `lumen-chart-ref-label`, fill: TONE[r.tone ?? "neutral"] }}
      />
    )),
  ];
}

/** Lumen's tooltip inside Recharts: header, then one row per shown series. */
export function cartesianTooltip({ config, valueFormat, formats, xFormat, keys }: { config: ChartConfig; valueFormat?: ValueFormat | undefined; formats?: Partial<Record<string, ValueFormat>> | undefined; xFormat?: ((v: unknown) => string) | undefined; keys: string[] }) {
  return (
    <Tooltip
      cursor={{ className: `lumen-chart-cursor` }}
      isAnimationActive={false}
      content={({ active, payload, label }: TooltipContentProps) =>
        active && payload?.length ? (
          <div className={`lumen-chart-tooltip`} data-static="" data-open="">
            <ChartTooltipHeader>{xFormat ? xFormat(label) : String(label ?? "")}</ChartTooltipHeader>
            {payload
              .filter((p) => keys.includes(String(p.dataKey)))
              // In the legend's order, not the drawing order.
              .sort((a, b) => keys.indexOf(String(a.dataKey)) - keys.indexOf(String(b.dataKey)))
              .map((p) => (
                <ChartTooltipRow key={String(p.dataKey)} name={String(p.dataKey)} config={config} value={formatValue(p.value, formats?.[String(p.dataKey)] ?? valueFormat)} />
              ))}
          </div>
        ) : null
      }
    />
  );
}

/** The frame around a cartesian chart: states, plot, legend and the screen-reader table. */
export function CartesianShell<R extends ChartRow>({
  kind,
  props,
  keys,
  shown,
  hidden,
  toggle,
  problem,
  onLegendPoint,
  children,
}: {
  kind: string;
  props: CartesianBaseProps<R>;
  keys: string[];
  shown: string[];
  hidden: string[];
  toggle: (k: string) => void;
  /** A reason the data can't be drawn (e.g. negative values under percent stacking). */
  problem?: string | null | undefined;
  /** A legend entry was pointed at or focused (its key), or left (null): for charts that bring a series forward. */
  onLegendPoint?: ((key: string | null) => void) | undefined;
  children: React.ReactNode;
}) {
  const { data, x, config, valueFormat, formats, xFormat, xLabel, height = 280, legend, loading, error, emptyText, label, className } = props;
  const legendMode = legend ?? (keys.length > 1 ? "toggle" : "none");
  const message = error ?? problem;
  const empty = !loading && !message && data.length === 0;
  const name = label ?? `${kind} of ${keys.map((k) => chartLabel(config, k)).join(", ")}`;
  return (
    <ChartContainer config={config} className={cn(`lumen-chart-cartesian`, className)} role="figure" aria-label={name}>
      <ChartPlot style={{ height }} aria-busy={loading || undefined}>
        {loading ? (
          <ChartLoading />
        ) : message ? (
          <div role="alert" className={`lumen-chart-error`}>
            {message}
          </div>
        ) : empty ? (
          <ChartEmpty>{emptyText ?? "No data yet"}</ChartEmpty>
        ) : (
          children
        )}
      </ChartPlot>
      {legendMode !== "none" && keys.length > 0 && (
        <ChartLegend>
          {keys.map((k) =>
            legendMode === "toggle" ? (
              <ChartLegendItem
                key={k}
                name={k}
                config={config}
                pressed={!hidden.includes(k)}
                onClick={() => toggle(k)}
                disabled={!hidden.includes(k) && shown.length === 1}
                {...(onLegendPoint && { onPointerEnter: () => onLegendPoint(hidden.includes(k) ? null : k), onPointerLeave: () => onLegendPoint(null), onFocus: () => onLegendPoint(hidden.includes(k) ? null : k), onBlur: () => onLegendPoint(null) })}
              />
            ) : (
              <ChartLegendItem key={k} name={k} config={config} />
            ),
          )}
        </ChartLegend>
      )}
      {!loading && !message && data.length > 0 && (
        <>
          <p className="sr-only">{chartSummary(kind, data, x, keys, config, valueFormat, xFormat, formats)}</p>
          <ChartTable
            caption={name}
            columns={[xLabel ?? x.charAt(0).toUpperCase() + x.slice(1), ...keys.map((k) => chartLabel(config, k))]}
            rows={data.map((r) => [xFormat ? xFormat(r[x]) : String(r[x] ?? ""), ...keys.map((k) => formatValue(r[k], formats?.[k] ?? valueFormat))])}
          />
        </>
      )}
    </ChartContainer>
  );
}

/** A per-instance id prefix for SVG defs (gradients, filters), so two charts never share them. */
export function useDefsId() {
  return `c${React.useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
}

/** The series colour as Recharts needs it. */
export const seriesColor = (k: string) => chartColor(k);
```

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

```css
@layer components {
  .lumen-chart-cartesian {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
  }
  .lumen-chart-cartesian .recharts-surface {
    overflow: visible;
  }
  .lumen-chart-cartesian :focus-visible {
    outline: none;
  }
  .lumen-chart-cartesian .recharts-wrapper:focus-visible {
    border-radius: calc(10px * 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-tick {
    fill: var(--gc-sec);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
  }
  .lumen-chart-grid line {
    stroke: var(--gc-hair);
  }
  .lumen-chart-cursor {
    fill: color-mix(in srgb,var(--gc-ink) 5%,transparent);
    stroke: color-mix(in srgb,var(--gc-ink) 25%,transparent);
    stroke-dasharray: 3 3;
  }
  .lumen-chart-ref-label {
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .04em;
  }
  .lumen-chart-tooltip[data-static] {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    min-width: 150px;
    max-width: 260px;
  }
  .lumen-chart-error {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    padding: 16px;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--gc-bad) 6%,transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb,var(--gc-bad) 25%,transparent);
    color: var(--gc-ink);
    font: 400 13px/1.5 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-align: center;
    text-wrap: balance;
  }
  .lumen-chart-line .recharts-line-curve,.lumen-chart-area .recharts-area-curve {
    filter: drop-shadow(0 0 5px color-mix(in srgb,var(--lc-c) 55%,transparent));
  }
  .lumen-chart-active-dot {
    filter: drop-shadow(0 0 6px var(--lc-c,currentColor));
  }
  .lumen-chart-last-dot {
    filter: drop-shadow(0 0 6px currentColor);
  }
  .lumen-chart-bar {
    transition: opacity min(0.3s, var(--gcp-cm)) ease;
  }
  .lumen-chart-bar[data-dim] {
    opacity: .38;
  }
  .lumen-chart-bar[data-lit] .lumen-chart-bar-edge,.lumen-chart-bar:hover .lumen-chart-bar-edge {
    filter: drop-shadow(0 0 6px currentColor);
  }
  .lumen-chart-bar-edge {
    color: inherit;
  }
  .lumen-chart-value {
    fill: var(--gc-ink);
    font: 500 11px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
  }
  .lumen-chart-axis-label {
    fill: var(--gc-sec);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .06em;
    text-transform: uppercase;
  }
  .lumen-chart-donut {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
  }
  .lumen-chart-donut .lumen-chart-plot {
    position: relative;
  }
  .lumen-chart-slice {
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-chart-slice[data-dim] {
    opacity: .35;
  }
  .lumen-chart-slice[data-active] path {
    filter: drop-shadow(0 0 8px currentColor);
  }
  .lumen-chart-donut-center {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    pointer-events: none;
    text-align: center;
  }
  .lumen-chart-donut-value {
    font: 600 24px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-donut-label {
    max-width: 60%;
    font: 500 10.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gc-sec);
  }
  .lumen-chart-donut-legend {
    display: grid;
    grid-template-columns: repeat(auto-fill,minmax(180px,1fr));
    gap: 2px 16px;
  }
  .lumen-chart-donut-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 32px;
    margin: 0;
    padding: 0 8px;
    border: 0;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
    background: none;
    color: var(--gc-sec);
    font: 500 12.5px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-align: start;
    cursor: pointer;
    outline: none;
    transition: background-color min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-donut-item[data-active] {
    background: color-mix(in srgb,var(--gc-ink) 5%,transparent);
    color: var(--gc-ink);
  }
  .lumen-chart-donut-item:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-donut-item-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lumen-chart-donut-item-value {
    font: 500 12px/1 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-donut-item-share {
    min-width: 3.2ch;
    font: 500 11px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-sec);
    text-align: end;
  }
  .lumen-chart-points .recharts-symbols {
    fill-opacity: .78;
    stroke: currentColor;
    stroke-width: 1.2;
    transition: fill-opacity min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-points .recharts-symbols:hover {
    fill-opacity: 1;
    filter: drop-shadow(0 0 6px currentColor);
  }
  .lumen-heatmap {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
    margin: 0;
    color: var(--gc-ink);
    font: 400 12px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-heatmap-grid {
    display: grid;
    gap: 3px;
    min-width: 0;
    outline: none;
  }
  .lumen-heatmap-row {
    display: contents;
  }
  .lumen-heatmap-col {
    padding-bottom: 4px;
    overflow: hidden;
    font: 500 10px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: var(--gc-sec);
    text-align: center;
    text-overflow: clip;
    white-space: nowrap;
  }
  .lumen-heatmap-rowhead {
    display: flex;
    align-items: center;
    padding-inline-end: 8px;
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: var(--gc-sec);
    white-space: nowrap;
  }
  .lumen-heatmap-cell {
    min-width: 0;
    border-radius: calc(3px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--hm-c) calc(var(--hm-t) * 1%),var(--gc-cellOff));
    outline: none;
    transition: background-color min(0.4s, var(--gcp-cm)) ease,box-shadow min(0.2s, var(--gcp-cm)) ease;
    animation: lumen-cell-drop calc(.45s * var(--gcp-k,1)) var(--gc-sp) both;
    animation-delay: calc(var(--d,0) * 22ms * var(--gcp-k,1));
  }
  @keyframes lumen-cell-drop {
    0% {
      opacity: 0;
      transform: translateY(-16px);
    }
    70% {
      opacity: 1;
      transform: translateY(1.5px);
    }
    100% {
      transform: none;
    }
  }
  .lumen-heatmap:is([data-motion="off"],[data-motion="subtle"]) .lumen-heatmap-cell {
    animation: none;
  }
  .lumen-heatmap-cell[data-hot] {
    box-shadow: 0 0 8px -1px color-mix(in srgb,var(--hm-c) 70%,transparent);
  }
  .lumen-heatmap-cell[data-missing] {
    background: repeating-linear-gradient(135deg,var(--gc-cellOff) 0 3px,transparent 3px 6px);
    box-shadow: inset 0 0 0 1px var(--gc-hair);
  }
  .lumen-heatmap-cell:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap,transparent),0 0 0 4px var(--gc-p-focus);
  }
  .lumen-heatmap-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px 16px;
  }
  .lumen-heatmap-readout {
    min-height: 1.3em;
    margin: 0;
    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-heatmap-scale {
    display: flex;
    align-items: center;
    gap: 8px;
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-sec);
  }
  .lumen-heatmap-ramp {
    width: 96px;
    height: 8px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: linear-gradient(90deg,color-mix(in srgb,var(--hm-c) 8%,var(--gc-cellOff)),var(--hm-c));
  }
  .lumen-heatmap-ramp[data-diverging] {
    background: linear-gradient(90deg,var(--hm-n),var(--gc-cellOff) 50%,var(--hm-c));
  }
  .lumen-heatmap-missing-key {
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }
  .lumen-heatmap-missing-key::before {
    content: "";
    width: 12px;
    height: 8px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: repeating-linear-gradient(135deg,var(--gc-cellOff) 0 3px,transparent 3px 6px);
    box-shadow: inset 0 0 0 1px var(--gc-hair);
  }
  .lumen-chart-radar .recharts-polar-grid :is(line,path,polygon,circle) {
    stroke: var(--gc-hair);
    fill: none;
  }
  .lumen-chart-radar-axis {
    fill: var(--gc-sec);
    font: 500 11px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-radar-tick {
    font-size: 9.5px;
    opacity: .75;
  }
  .lumen-chart-radar-shape .recharts-radar-polygon>path {
    filter: drop-shadow(0 0 4px color-mix(in srgb,var(--lc-c) 45%,transparent));
    transition: fill-opacity min(0.25s, var(--gcp-cm)) ease,stroke-opacity min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-chart-radar-cursor {
    fill: none;
    stroke: color-mix(in srgb,var(--gc-ink) 30%,transparent);
    stroke-dasharray: 3 3;
  }
  .lumen-chart-radar-lead {
    margin-top: 6px;
    padding-top: 6px;
    border-top: 1px solid var(--gc-hair);
    font: 500 11px/1.35 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: var(--gc-ink);
  }
  .lumen-chart-svg-focus:focus-visible,.lumen-chart-radial-svg:focus-visible {
    outline: none;
    border-radius: calc(12px * 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-frame[data-motion="off"] * {
    animation: none!important;
    transition: none!important;
  }
  .lumen-chart-radial-body {
    display: grid;
    grid-template-columns: minmax(0,1fr) minmax(0,1fr);
    align-items: center;
    gap: 20px 28px;
  }
  .lumen-chart-radial-body[data-legend="none"] {
    grid-template-columns: minmax(0,1fr);
  }
  @container (max-width:520px) {
    .lumen-chart-radial-body {
      grid-template-columns: minmax(0,1fr);
    }
  }
  .lumen-chart-radial {
    container-type: inline-size;
  }
  .lumen-chart-radial-ring {
    position: relative;
    display: grid;
    place-items: center;
    min-width: 0;
  }
  .lumen-chart-radial-svg {
    display: block;
    width: auto;
    height: 100%;
    max-width: 100%;
    aspect-ratio: 1;
    overflow: visible;
    outline: none;
    border-radius: 50%;
  }
  .lumen-chart-radial-item {
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
    cursor: default;
  }
  .lumen-chart-radial-item[data-dim] {
    opacity: .4;
  }
  .lumen-chart-radial-track {
    fill: none;
    stroke: color-mix(in srgb,currentColor 13%,var(--gc-cellOff));
  }
  .lumen-chart-radial-item[data-missing] .lumen-chart-radial-track {
    stroke: var(--gc-cellOff);
    stroke-dasharray: 1.5 2.5;
  }
  .lumen-chart-radial-fill,.lumen-chart-radial-lap {
    fill: none;
    stroke: currentColor;
    stroke-linecap: round;
    transition: stroke-dasharray calc(0.6s * var(--gcp-k)) cubic-bezier(.22,1,.36,1);
  }
  .lumen-chart-radial-fill {
    filter: drop-shadow(0 0 3px color-mix(in srgb,currentColor 45%,transparent));
    animation: lumen-ring-draw calc(.9s * var(--gcp-k,1)) cubic-bezier(.22,1,.36,1) both;
    animation-delay: calc(var(--i,0) * 70ms * var(--gcp-k,1));
  }
  .lumen-chart-radial-lap {
    stroke: color-mix(in srgb,currentColor 70%,var(--gc-ink));
    filter: drop-shadow(0 0 2px rgba(0,0,0,.35)) drop-shadow(0 0 6px currentColor);
    animation: lumen-ring-draw calc(.6s * var(--gcp-k,1)) ease-out both;
    animation-delay: calc(.75s * var(--gcp-k,1) + var(--i,0) * 70ms);
  }
  .lumen-chart-radial-item[data-active] .lumen-chart-radial-fill {
    filter: drop-shadow(0 0 7px currentColor);
  }
  @keyframes lumen-ring-draw {
    from {
      stroke-dasharray: 0 100;
    }
  }
  .lumen-chart-radial[data-motion="subtle"] :is(.lumen-chart-radial-fill,.lumen-chart-radial-lap) {
    animation-duration: calc(.45s * var(--gcp-k,1));
    animation-delay: 0s;
  }
  .lumen-chart-radial-hit {
    fill: none;
    stroke: transparent;
    pointer-events: stroke;
  }
  .lumen-chart-radial-target {
    stroke: var(--gc-ink);
    stroke-width: 1.5;
    stroke-linecap: round;
  }
  .lumen-chart-radial-goal {
    stroke: color-mix(in srgb,var(--gc-ink) 35%,transparent);
    stroke-width: 1;
    stroke-dasharray: 2 2.5;
  }
  .lumen-chart-radial-center {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    pointer-events: none;
    text-align: center;
  }
  .lumen-chart-radial-value {
    font: 600 22px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-radial-caption {
    max-width: 38%;
    font: 500 10px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gc-sec);
    text-wrap: balance;
  }
  .lumen-chart-radial-legend {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }
  .lumen-chart-radial-entry {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 34px;
    margin: 0;
    padding: 0 8px;
    border: 0;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
    background: none;
    color: var(--gc-sec);
    font: 500 12.5px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-align: start;
    cursor: pointer;
    outline: none;
    transition: background-color min(0.2s, var(--gcp-cm)) ease,color min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-radial-entry[data-active] {
    background: color-mix(in srgb,var(--gc-ink) 5%,transparent);
    color: var(--gc-ink);
  }
  .lumen-chart-radial-entry:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-radial-entry-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lumen-chart-radial-entry-value {
    font: 500 12px/1 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);
    white-space: nowrap;
  }
  .lumen-chart-radial-entry-target {
    color: var(--gc-sec);
  }
  .lumen-chart-radial-entry-share {
    min-width: 4.2ch;
    font: 500 11px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-sec);
    text-align: end;
  }
  .lumen-chart-radial-entry-share[data-met] {
    color: var(--gc-good-ui);
  }
  .lumen-chart-treemap-crumbs {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-width: 0;
  }
  .lumen-chart-treemap-crumbs>ol {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px;
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .lumen-chart-treemap-crumbs li {
    display: inline-flex;
    align-items: center;
    min-width: 0;
  }
  .lumen-chart-treemap-crumbs li+li::before {
    content: "/";
    padding: 0 4px;
    color: var(--gc-m45,var(--gc-sec));
    font: 400 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
  }
  .lumen-chart-treemap-crumb {
    max-width: 22ch;
    overflow: hidden;
    margin: 0;
    padding: 5px 7px;
    border: 0;
    border-radius: calc(7px * var(--lumen-radius-k, 1));
    background: none;
    color: var(--gc-sec);
    font: 500 12.5px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-overflow: ellipsis;
    white-space: nowrap;
    outline: none;
  }
  button.lumen-chart-treemap-crumb {
    cursor: pointer;
    transition: background-color min(0.2s, var(--gcp-cm)) ease,color min(0.2s, var(--gcp-cm)) ease;
  }
  button.lumen-chart-treemap-crumb:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-treemap-crumb[data-current] {
    color: var(--gc-ink);
  }
  .lumen-chart-treemap-total {
    flex: none;
    font: 500 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-sec);
  }
  .lumen-chart-treemap-level {
    position: absolute;
    inset: 0;
  }
  .lumen-chart-treemap-tile {
    position: absolute;
    box-sizing: border-box;
    display: flex;
    align-items: flex-start;
    margin: 0;
    padding: 0;
    overflow: hidden;
    border: 0;
    border-radius: calc(6px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--tm-c) calc(var(--tm-t) * 1%),var(--gc-cellOff));
    box-shadow: inset 0 2px 0 -0.5px var(--tm-c),inset 0 0 0 1px color-mix(in srgb,var(--tm-c) 30%,transparent);
    color: var(--gc-ink);
    text-align: start;
    cursor: default;
    outline: none;
    container-type: size;
    transition: left calc(0.5s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),top calc(0.5s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),width calc(0.5s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),height calc(0.5s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),opacity min(0.2s, var(--gcp-cm)) ease,box-shadow min(0.2s, var(--gcp-cm)) ease;
    animation: lumen-tile-in calc(.42s * var(--gcp-k,1)) cubic-bezier(.22,1,.36,1) both;
    animation-delay: calc(min(var(--i,0),12) * 24ms * var(--gcp-k,1));
  }
  .lumen-chart-treemap-tile[data-parent] {
    cursor: zoom-in;
  }
  .lumen-chart-treemap-tile[data-dim] {
    opacity: .72;
  }
  @media (hover:hover) {
    .lumen-heatmap-cell:hover {
      box-shadow: inset 0 0 0 1.5px color-mix(in srgb,var(--gc-ink) 55%,transparent);
    }
    button.lumen-chart-treemap-crumb:hover {
      background: color-mix(in srgb,var(--gc-ink) 5%,transparent);
      color: var(--gc-ink);
    }
    .lumen-chart-treemap-tile:hover {
      box-shadow: inset 0 2px 0 -0.5px var(--tm-c),inset 0 0 0 1.5px color-mix(in srgb,var(--gc-ink) 55%,transparent);
    }
  }
  .lumen-chart-treemap-tile:focus-visible {
    z-index: 1;
    box-shadow: inset 0 2px 0 -0.5px var(--tm-c),0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  @keyframes lumen-tile-in {
    from {
      opacity: 0;
      transform: scale(.94);
    }
  }
  .lumen-chart-treemap[data-motion="subtle"] .lumen-chart-treemap-tile {
    animation-duration: calc(.25s * var(--gcp-k,1));
    animation-delay: 0s;
  }
  .lumen-chart-treemap-inner {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  .lumen-chart-treemap-inner rect {
    fill: none;
    stroke: color-mix(in srgb,var(--gc-ink) 16%,transparent);
    stroke-width: 1;
  }
  .lumen-chart-treemap-text {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    max-width: 100%;
    padding: 8px 9px;
  }
  .lumen-chart-treemap-name {
    overflow: hidden;
    font: 500 12.5px/1.2 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lumen-chart-treemap-value {
    overflow: hidden;
    font: 500 11px/1.2 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: color-mix(in srgb,var(--gc-ink) 78%,transparent);
  }
  .lumen-chart-treemap-share {
    color: var(--gc-sec);
  }
  @container (max-height:46px) {
    .lumen-chart-treemap-value {
      display: none;
    }
  }
  @container (max-width:54px) or (max-height:26px) {
    .lumen-chart-treemap-text {
      display: none;
    }
  }
  .lumen-chart-sankey-svg {
    display: block;
    overflow: visible;
    outline: none;
  }
  .lumen-chart-sankey-flow {
    fill: none;
    stroke-opacity: .34;
    transition: stroke-opacity min(0.25s, var(--gcp-cm)) ease;
    cursor: default;
    animation: lumen-flow-draw calc(.8s * var(--gcp-k,1)) cubic-bezier(.22,1,.36,1) both;
    animation-delay: calc(.15s + min(var(--i,0),20) * 30ms * var(--gcp-k,1));
  }
  .lumen-chart-sankey-flow[data-lit] {
    stroke-opacity: .62;
  }
  .lumen-chart-sankey-flow[data-dim] {
    stroke-opacity: .07;
  }
  @keyframes lumen-flow-draw {
    from {
      stroke-dasharray: 0 1;
    }
    to {
      stroke-dasharray: 1 0;
    }
  }
  .lumen-chart-sankey[data-motion="subtle"] .lumen-chart-sankey-flow {
    animation-duration: calc(.4s * var(--gcp-k,1));
    animation-delay: 0s;
  }
  @media (prefers-reduced-motion:reduce) {
    .lumen-chart-bar {
      transition: none;
    }
    .lumen-heatmap-cell {
      animation: none;
    }
    .lumen-chart-radial-fill,.lumen-chart-radial-lap {
      animation: none;
      transition: none;
    }
    .lumen-chart-treemap-tile {
      animation: none;
      transition: none;
    }
    .lumen-chart-sankey-flow {
      animation: none;
    }
  }
  .lumen-chart-sankey-node {
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-chart-sankey-node[data-dim] {
    opacity: .35;
  }
  .lumen-chart-sankey-bar {
    fill: currentColor;
    filter: drop-shadow(0 0 4px color-mix(in srgb,currentColor 50%,transparent));
  }
  .lumen-chart-sankey-label,.lumen-chart-sankey-value {
    paint-order: stroke;
    stroke: color-mix(in srgb,var(--background,#fff) 85%,transparent);
    stroke-width: 3px;
    stroke-linejoin: round;
    pointer-events: none;
  }
  .lumen-chart-sankey-label {
    fill: var(--gc-ink);
    font: 500 12px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-sankey-value {
    fill: var(--gc-sec);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
  }
  .lumen-chart-bullet-rows {
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 100%;
    outline: none;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
  }
  .lumen-chart-bullet-row {
    display: grid;
    grid-template-columns: minmax(72px,24%) minmax(0,1fr) minmax(64px,auto);
    align-items: center;
    gap: 14px;
    padding: 0 6px;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
    transition: opacity min(0.25s, var(--gcp-cm)) ease,background-color min(0.2s, var(--gcp-cm)) ease;
    cursor: default;
  }
  .lumen-chart-bullet-row[data-active] {
    background: color-mix(in srgb,var(--gc-ink) 4%,transparent);
  }
  .lumen-chart-bullet-row[data-dim] {
    opacity: .45;
  }
  .lumen-chart-bullet-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--gc-sec);
    font: 500 12.5px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-bullet-row[data-active] .lumen-chart-bullet-label {
    color: var(--gc-ink);
  }
  .lumen-chart-bullet-track {
    position: relative;
    height: 18px;
    border-radius: calc(4px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
    box-shadow: inset 0 0 0 1px var(--gc-hair);
    overflow: hidden;
  }
  .lumen-chart-bullet-band {
    position: absolute;
    top: 0;
    bottom: 0;
    background: color-mix(in srgb,var(--gc-ink) calc(5% + var(--k) * 9%),transparent);
  }
  .lumen-chart-bullet-bar {
    position: absolute;
    left: 0;
    top: 50%;
    height: 6px;
    translate: 0 -50%;
    border-radius: 0 calc(3px * var(--lumen-radius-k, 1)) calc(3px * var(--lumen-radius-k, 1)) 0;
    background: var(--lc-c);
    box-shadow: 0 0 8px color-mix(in srgb,var(--lc-c) 55%,transparent);
    transform-origin: left;
    transition: width calc(0.5s * var(--gcp-k)) cubic-bezier(.22,1,.36,1);
  }
  .lumen-chart-frame[data-motion]:not([data-motion="off"]) .lumen-chart-bullet-bar {
    animation: lumen-bullet-in calc(.7s * var(--gcp-k)) cubic-bezier(.2,.8,.2,1) both;
    animation-delay: calc(var(--i) * 60ms);
  }
  @keyframes lumen-bullet-in {
    from {
      transform: scaleX(0);
    }
  }
  .lumen-chart-bullet-target {
    position: absolute;
    top: 2px;
    bottom: 2px;
    width: 2px;
    margin-left: -1px;
    border-radius: calc(1px * var(--lumen-radius-k, 1));
    background: var(--gc-ink);
  }
  .lumen-chart-bullet-value {
    color: var(--gc-ink);
    font: 500 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    text-align: end;
    white-space: nowrap;
  }
  .lumen-chart-bullet-share {
    color: var(--gc-sec);
  }
  .lumen-chart-bullet-row[data-met] .lumen-chart-bullet-share {
    color: var(--gc-good-ui);
  }
  .lumen-chart-bullet-row[data-missing] .lumen-chart-bullet-value {
    color: var(--gc-sec);
  }
  @container (max-width:420px) {
    .lumen-heatmap-col {
      font-size: 9px;
    }
    .lumen-chart-bullet-row {
      grid-template-columns: minmax(0,1fr) auto;
      grid-template-areas: "l v" "t t";
      row-gap: 6px;
      padding: 6px;
    }
    .lumen-chart-bullet-label {
      grid-area: l;
    }
    .lumen-chart-bullet-value {
      grid-area: v;
    }
    .lumen-chart-bullet-track {
      grid-area: t;
    }
  }
  .lumen-chart-bullet {
    container-type: inline-size;
  }
  .lumen-chart-slope-svg {
    display: block;
    outline: none;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    overflow: visible;
  }
  .lumen-chart-slope-head {
    fill: var(--gc-sec);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .08em;
    text-transform: uppercase;
  }
  .lumen-chart-slope-axis {
    stroke: var(--gc-hair);
    stroke-width: 1;
  }
  .lumen-chart-slope-line {
    color: var(--lc-c);
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
    cursor: default;
  }
  .lumen-chart-slope-line[data-dim] {
    opacity: .28;
  }
  .lumen-chart-slope-path {
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    fill: none;
    filter: drop-shadow(0 0 3px color-mix(in srgb,currentColor 45%,transparent));
    transition: stroke-width min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-slope-line[data-active] .lumen-chart-slope-path {
    stroke-width: 2.75;
  }
  .lumen-chart-frame[data-motion]:not([data-motion="off"]) .lumen-chart-slope-path {
    stroke-dasharray: 1;
    animation: lumen-slope-draw calc(.8s * var(--gcp-k)) cubic-bezier(.2,.8,.2,1) both;
    animation-delay: calc(var(--i) * 70ms);
  }
  @keyframes lumen-slope-draw {
    from {
      stroke-dashoffset: 1;
    }
    to {
      stroke-dashoffset: 0;
    }
  }
  .lumen-chart-slope-dot {
    fill: var(--gc-gap);
    stroke: currentColor;
    stroke-width: 1.75;
  }
  .lumen-chart-slope-hit {
    stroke: transparent;
    stroke-width: 14;
    pointer-events: stroke;
  }
  .lumen-chart-slope-label {
    font: 500 12px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-slope-name {
    fill: var(--gc-sec);
  }
  .lumen-chart-slope-line[data-active] .lumen-chart-slope-name {
    fill: var(--gc-ink);
  }
  .lumen-chart-slope-value {
    fill: var(--gc-ink);
    font-family: var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
  }
  .lumen-chart-slope-rank {
    font-family: var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-size: 10.5px;
  }
  .lumen-chart-slope-rank[data-dir="up"] {
    fill: var(--gc-good-ui);
  }
  .lumen-chart-slope-rank[data-dir="down"] {
    fill: var(--gc-bad-ui);
  }
  .lumen-chart-waffle {
    container-type: inline-size;
  }
  .lumen-chart-waffle-body {
    display: grid;
    grid-template-columns: auto minmax(0,1fr);
    align-items: center;
    gap: 20px 28px;
  }
  .lumen-chart-waffle-body[data-legend="none"] {
    grid-template-columns: auto;
    justify-content: center;
  }
  @container (max-width:480px) {
    .lumen-chart-waffle-body {
      grid-template-columns: minmax(0,1fr);
      justify-items: center;
    }
  }
  .lumen-chart-waffle-grid {
    display: grid;
    grid-template-columns: repeat(var(--n),minmax(0,1fr));
    gap: 3px;
    max-width: 100%;
    aspect-ratio: 1;
    padding: 4px;
    outline: none;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
  }
  .lumen-chart-waffle-cell {
    border-radius: calc(3px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
    box-shadow: inset 0 0 0 1px var(--gc-hair);
    transition: background-color min(0.45s, var(--gcp-cm)) ease,opacity min(0.25s, var(--gcp-cm)) ease,box-shadow min(0.3s, var(--gcp-cm)) ease;
  }
  .lumen-chart-waffle-cell[data-on] {
    background: var(--lc-c);
    box-shadow: 0 0 6px -1px color-mix(in srgb,var(--lc-c) 60%,transparent);
    cursor: default;
  }
  .lumen-chart-waffle-cell[data-dim] {
    opacity: .3;
  }
  .lumen-chart-waffle-cell[data-active] {
    box-shadow: 0 0 8px color-mix(in srgb,var(--lc-c) 75%,transparent);
  }
  .lumen-chart-frame[data-motion]:not([data-motion="off"]) .lumen-chart-waffle-cell[data-on] {
    animation: lumen-waffle-in calc(.35s * var(--gcp-k)) ease-out both;
    animation-delay: calc(var(--c) * 6ms * var(--gcp-k));
  }
  @keyframes lumen-waffle-in {
    from {
      opacity: 0;
      transform: scale(.4);
    }
  }
  .lumen-chart-waffle-legend {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    width: 100%;
  }
  .lumen-chart-waffle-entry {
    display: grid;
    grid-template-columns: auto minmax(0,1fr) auto auto;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 34px;
    margin: 0;
    padding: 0 8px;
    border: 0;
    border-radius: calc(8px * var(--lumen-radius-k, 1));
    background: none;
    color: var(--gc-sec);
    font: 500 12.5px/1.3 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    text-align: start;
    cursor: pointer;
    outline: none;
    transition: background-color min(0.2s, var(--gcp-cm)) ease,color min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-chart-waffle-entry:hover,.lumen-chart-waffle-entry[data-active] {
    background: color-mix(in srgb,var(--gc-ink) 5%,transparent);
    color: var(--gc-ink);
  }
  .lumen-chart-waffle-entry:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-chart-waffle-entry .lumen-chart-legend-swatch {
    background: var(--lc-c);
  }
  .lumen-chart-waffle-entry-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lumen-chart-waffle-entry-share {
    color: var(--gc-ink);
    font: 500 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
  }
  .lumen-chart-waffle-entry-value {
    min-width: 56px;
    color: var(--gc-sec);
    font: 500 11.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    text-align: end;
  }
}
```

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.
