# Day Rings (Lumen Halo): prompt.md (v2.0.0)

- id: `lumen-day-rings` · version 2.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-day-rings.json`
- npm dependencies: none
- 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-day-rings
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Daily goals over about a month on one ring: a spoke per day stacks each goal's progress, and the pinned day's goals wrap the ring as arcs that glow past 100%. Scrub by pointer or arrow keys, filter to one goal, log today in a tap and undo it. In parts, on Lumen's chart core.

## 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/day-rings.tsx`; shared code: `lib/beautiful-ui/lumen/day-rings-model.ts`, `lib/beautiful-ui/lumen/day-rings-sample.ts`, `lib/beautiful-ui/lumen/sound.ts`.
- 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: `DayRings`, `DayRingsRing`, `DayRingsGoals`, `DayRingsFilter`, `DayRingsSummary`, `DayRingsLog`, `DayRingsTable`, 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 { DayRings, DayRingsRing, DayRingsGoals, DayRingsFilter, DayRingsLog, DayRingsTable } from "@/components/ui/lumen/day-rings";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `DayRings` | — | Data, pinned day, filter, logs and undo. |
| `DayRingsRing` | `day-rings-ring` | The ring slider and its centre readout. |
| `DayRingsGoals` | `day-rings-goals` | Each goal on the pinned day, with a cell meter. |
| `DayRingsFilter` | — | All goals, or one. |
| `DayRingsSummary` | `day-rings-summary` | Days closed, a cell per day. |
| `DayRingsLog` | `day-rings-log` | Quick logs, undo, back to today. |
| `DayRingsTable` | — | Every day for screen readers. |

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/day-rings.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
- daily goals, habit tracker, team rituals, activity rings, streaks, 30-day progress, log today, Lumen
- Two to four daily goals over about a month, where people log today and look back
- Rituals, habits, health or productivity dashboards

### Not when
- Long histories (quarters, years): use a line or heatmap
- Values without a daily target: use Line or Stat

## Mistakes
- Keep goals and days stable (memoise)
- Persist logs in onLog; the ring only holds them for the session

## Usage

```tsx
import { DayRings, DayRingsRing, DayRingsGoals, DayRingsFilter, DayRingsLog, DayRingsTable } from "@/components/ui/lumen/day-rings";

<DayRings goals={goals} days={history} onLog={(goal, delta) => save(goal, delta)} onUndo={(goal, delta) => save(goal, delta)}>
  <DayRingsRing />
  <DayRingsGoals />
  <DayRingsFilter />
  <DayRingsLog />
  <DayRingsTable />
</DayRings>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `goals` | `{ id, label, unit, goal, step, chip? }[]` |  | Each goal (inside out): its id (the config key for its colour), target and the quick-log step. |
| `days` | `{ date: string; values: (number \| null)[] }[]` |  | Oldest first, ISO dates; the last day is today. Missing values stay missing, never zero. |
| `config` | `ChartConfig` |  | Colours by goal id (shadcn's shape). |
| `pinned / defaultPinned / onPinnedChange` | `string \| null` |  | The pinned day as an ISO date; null is today. |
| `filter / defaultFilter / onFilterChange` | `string \| null` |  | A goal id, or null for all goals. |
| `onLog / onUndo` | `(goalId, delta) => void` |  | A quick log for today, and the last log undone (with the reversed delta). The ring updates at once; persist here. |
| `readOnly` | `boolean` | `false` | Hide the quick logs. |
| `formatValue / locale` | `(value, goal) => string / string` |  | How values and dates read. |
| `labels` | `Partial<DayRingsLabels>` |  | Every word, for i18n. |
| `motion` | `"full" \| "subtle" \| "off"` | `"full"` | Reduced motion always wins. |
| `Parts` | `DayRingsRing · Goals · Filter · Summary · Log · Table · Card` |  | Arrange them yourself inside <DayRings>, or use DayRingsCard; useDayRings() reads the state. |

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

## 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 |
|---|---|
| ← / → | Previous or next day |
| Page Up / Down | A week back or forward |
| Home / End | First day, or today |
| Arrows (filter) | Move between All and each goal |

## Motion inventory

| Interaction | What moves |
|---|---|
| Scrub | The pinned spoke lights and its arcs wrap the ring; a soft tick per day |
| Log | Today's spoke and arc grow; the % rolls like an odometer |
| Past 100% | The goal's arc glows |

## Accessibility contract (preserve when editing)
- The ring is a slider: arrows (and Page Up/Down by a week, Home, End) move the pinned day; its value text reads the date and how many goals closed
- Goal filter is a radio group; logs and undo are labelled buttons, and each log is announced
- Missing values are marked as missing (dotted spokes, No data), never as zero
- Every day is in the data table for screen readers

## Install

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

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

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

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

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

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

## Source (Base UI build)

### components/ui/lumen/day-rings.tsx

```tsx
"use client";

/**
 * Day Rings (Lumen Halo) v2.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/lumen-day-rings · Agent prompt: https://beautiful-ui.dev/md/lumen-day-rings.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 { ChartContainer, ChartDescription, ChartEyebrow, ChartHeader, ChartLive, ChartSurface, ChartTable, ChartTitle, chartColor, type ChartConfig } from "@/components/ui/lumen/chart";
import { ChartMode, ChartModes, useChartSelection } from "@/components/ui/lumen/chart-client";
import { StatValue } from "@/components/ui/lumen/stat";
import { applyLog, buildDayRings, dayRingsLabels, dayWords, vsAverage, type DayRingsDay, type DayRingsGoal, type DayRingsLabels, type DayRingsModel } from "@/lib/beautiful-ui/lumen/day-rings-model";
import { sampleDays, sampleGoals } from "@/lib/beautiful-ui/lumen/day-rings-sample";
import { emitSound } from "@/lib/beautiful-ui/lumen/sound";

/*
 * Lumen Day Rings. Daily goals over about a month on one ring: a spoke per day stacks each goal's
 * progress, and the pinned day's goals wrap the ring as arcs that glow past 100%. Scrub the ring by
 * pointer or arrow keys; filter to one goal; log today in a tap and undo it. In parts: DayRings holds
 * the data and state; Ring, Goals, Filter, Log, Summary and Table read it. DayRingsCard assembles
 * them on the sample month; its source is the recipe.
 */

type Motion = "full" | "subtle" | "off";

/** list[i] for an index that is in bounds by construction (see each use); the same value as list[i]. */
const inBounds = <T,>(list: readonly T[], i: number): T => list[i]!;

type Ctx = {
  model: DayRingsModel;
  L: DayRingsLabels;
  title: string;
  locale?: string | undefined;
  pinned: number;
  pin: (i: number, from?: Element | null) => void;
  filter: string | null;
  setFilter: (id: string | null) => void;
  log: (goal: number, from?: Element | null) => void;
  undo: (from?: Element | null) => void;
  canUndo: boolean;
  readOnly: boolean;
  format: (v: number | null | undefined, g: DayRingsGoal) => string;
  motion: Motion;
  say: (s: string) => void;
};

const DayRingsContext = React.createContext<Ctx | null>(null);

/** The chart's state, for parts of your own inside <DayRings>. */
export function useDayRings() {
  const c = React.useContext(DayRingsContext);
  if (!c) throw new Error("Day Rings parts must be inside <DayRings>.");
  return c;
}

const DEFAULT_CONFIG: ChartConfig = {
  focus: { label: "Focus", theme: { light: "#5b63e8", dark: "#8b93ff" } },
  reviews: { label: "Reviews", theme: { light: "#c97f06", dark: "#f4b73e" } },
  deploys: { label: "Deploys", theme: { light: "#0e87b8", dark: "#4fc7ee" } },
};

export type DayRingsProps = Omit<React.ComponentProps<"div">, "title"> & {
  goals?: DayRingsGoal[] | undefined;
  /** Oldest first; the last day is today. Designed for about 30. */
  days?: DayRingsDay[] | undefined;
  /** Colours by goal id (shadcn's ChartConfig). */
  config?: ChartConfig | undefined;
  /** Names the chart for screen readers. */
  title?: string | undefined;
  /** Pinned day as an ISO date; null is today. */
  pinned?: string | null | undefined;
  defaultPinned?: string | null | undefined;
  onPinnedChange?: ((date: string | null) => void) | undefined;
  /** A goal id, or null for all goals. */
  filter?: string | null | undefined;
  defaultFilter?: string | null | undefined;
  onFilterChange?: ((goalId: string | null) => void) | undefined;
  /** A quick log for today: persist it here. The ring updates at once. */
  onLog?: ((goalId: string, delta: number) => void) | undefined;
  /** The last log undone, with the reversed delta. */
  onUndo?: ((goalId: string, delta: number) => void) | undefined;
  /** Hide the quick logs. */
  readOnly?: boolean | undefined;
  /** How goal values read. Default: grouped whole numbers in the locale. */
  formatValue?: ((value: number, goal: DayRingsGoal) => string) | undefined;
  locale?: string | undefined;
  labels?: Partial<DayRingsLabels> | undefined;
  motion?: Motion | undefined;
};

export function DayRings({
  goals = sampleGoals,
  days: daysProp = sampleDays,
  config = DEFAULT_CONFIG,
  title = "Daily goals",
  pinned: pinnedProp,
  defaultPinned = null,
  onPinnedChange,
  filter: filterProp,
  defaultFilter = null,
  onFilterChange,
  onLog,
  onUndo,
  readOnly = false,
  formatValue,
  locale,
  labels,
  motion = "full",
  className,
  children,
  ...props
}: DayRingsProps) {
  // Logs apply at once on a local copy; a new days prop replaces it (adjusted during render).
  const [days, setDays] = React.useState(daysProp);
  const [source, setSource] = React.useState(daysProp);
  if (source !== daysProp) {
    setSource(daysProp);
    setDays(daysProp);
  }
  const model = React.useMemo(() => buildDayRings(goals, days), [goals, days]);
  const L = React.useMemo(() => ({ ...dayRingsLabels, ...labels }), [labels]);
  const last = model.days.length - 1;
  const [pinDate, setPinDate] = useChartSelection<string | null>({ value: pinnedProp, defaultValue: defaultPinned, onValueChange: onPinnedChange });
  const at = pinDate == null ? -1 : model.days.findIndex((d) => d.date === pinDate);
  const pinned = at >= 0 ? at : last;
  const [filter, setFilterRaw] = useChartSelection<string | null>({ value: filterProp, defaultValue: defaultFilter, onValueChange: onFilterChange });
  const [undoStack, setUndoStack] = React.useState<{ goal: number; delta: number }[]>([]);
  const [said, say] = React.useState("");
  const lastTick = React.useRef(0);
  const nf = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }), [locale]);
  const format = (v: number | null | undefined, g: DayRingsGoal) => (v == null ? "—" : formatValue ? formatValue(v, g) : nf.format(v));

  const ctx: Ctx = {
    model,
    L,
    title,
    locale,
    pinned,
    pin: (i, from) => {
      const next = Math.max(0, Math.min(last, i));
      if (next === pinned) return;
      // next is clamped to the days.
      setPinDate(next === last ? null : inBounds(model.days, next).date);
      const now = performance.now();
      if (from && now - lastTick.current > 70) {
        lastTick.current = now;
        emitSound(from, "tick");
      }
    },
    filter: filter && model.goals.some((g) => g.id === filter) ? filter : null,
    setFilter: (id) => setFilterRaw(id),
    log: (g, from) => {
      // g is one of the goals' own indices.
      const goal = inBounds(model.goals, g);
      setDays(applyLog(model, last, g, goal.step));
      setUndoStack((s) => [...s, { goal: g, delta: goal.step }]);
      setPinDate(null);
      onLog?.(goal.id, goal.step);
      emitSound(from ?? null, "toggleOn");
      say(L.logged(goal.chip ?? `+${goal.step} ${goal.unit}`, goal.label));
    },
    undo: (from) => {
      const top = undoStack[undoStack.length - 1];
      if (!top) return;
      const goal = inBounds(model.goals, top.goal);
      setDays(applyLog(model, last, top.goal, -top.delta));
      setUndoStack((s) => s.slice(0, -1));
      onUndo?.(goal.id, -top.delta);
      emitSound(from ?? null, "toggleOff");
      say(L.undone(goal.label));
    },
    canUndo: undoStack.length > 0,
    readOnly,
    format,
    motion,
    say,
  };
  return (
    <DayRingsContext.Provider value={ctx}>
      <ChartContainer config={config} data-motion={motion} className={cn(`lumen-day-rings lumen-control`, className)} {...props}>
        {children}
        <ChartLive>{said}</ChartLive>
      </ChartContainer>
    </DayRingsContext.Provider>
  );
}

// ---------------------------------------------------------------------------------------------
// The ring
// ---------------------------------------------------------------------------------------------

const C = 160;
const R0 = 56;
const R1 = 124;
const ARC0 = 136;
const ARC_GAP = 7;

const polar = (r: number, a: number) => [C + r * Math.sin(a), C - r * Math.cos(a)] as const;
function arcPath(r: number, from: number, to: number) {
  const [x0, y0] = polar(r, from);
  const [x1, y1] = polar(r, to);
  const large = to - from > Math.PI ? 1 : 0;
  return `M${x0.toFixed(2)} ${y0.toFixed(2)} A${r} ${r} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;
}

/** The ring: spokes per day, the pinned day's arcs, the centre readout. A keyboard slider. */
export function DayRingsRing({ className, size = 320, ...props }: React.ComponentProps<"div"> & { /** Largest diameter in px; it shrinks to fit. Default 320. */ size?: number | undefined }) {
  const { model, L, pinned, pin, filter, format, locale } = useDayRings();
  const n = model.days.length;
  const goals = model.goals;
  const shownGoals = filter ? goals.filter((g) => g.id === filter) : goals;
  const day = model.days[pinned];
  const svg = React.useRef<SVGSVGElement>(null);
  const angleOf = (i: number) => (n ? (i / n) * Math.PI * 2 : 0);
  const step = n ? (Math.PI * 2) / n : 0;
  const fromPointer = (e: React.PointerEvent) => {
    const el = svg.current;
    if (!el || !n) return;
    const r = el.getBoundingClientRect();
    const x = ((e.clientX - r.left) / r.width) * 320 - C;
    const y = ((e.clientY - r.top) / r.height) * 320 - C;
    if (Math.hypot(x, y) < R0 * 0.6) return;
    const a = (Math.atan2(x, -y) + Math.PI * 2) % (Math.PI * 2);
    pin(Math.round(a / step) % n, e.currentTarget);
  };
  if (!day) return null;
  const dw = dayWords(day.date, locale);
  const isToday = pinned === n - 1;
  const fg = filter ? goals.findIndex((g) => g.id === filter) : -1;
  const pct = fg >= 0 ? day.ratios[fg] : day.score;
  // fg, when found, is one of the goals' own indices.
  const fgGoal = fg >= 0 ? inBounds(goals, fg) : undefined;
  const valueText = `${isToday ? L.today : dw.long}: ${fgGoal ? (day.values[fg] == null ? `${fgGoal.label}, ${L.noValue.toLowerCase()}` : `${format(day.values[fg], fgGoal)} of ${format(fgGoal.goal, fgGoal)} ${fgGoal.unit}`) : L.goalsClosed(day.metCount, goals.length)}`;
  const band = (R1 - R0) / shownGoals.length;

  return (
    <div data-slot="day-rings-ring" className={cn(`lumen-day-rings-ring`, className)} style={{ maxWidth: size }} {...props}>
      <svg
        ref={svg}
        viewBox="0 0 320 320"
        role="slider"
        tabIndex={0}
        aria-label={L.ring}
        aria-valuemin={1}
        aria-valuemax={n}
        aria-valuenow={pinned + 1}
        aria-valuetext={valueText}
        onPointerMove={(e) => e.pointerType === "mouse" && fromPointer(e)}
        onPointerDown={fromPointer}
        onKeyDown={(e) => {
          const d = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1, PageUp: 7, PageDown: -7 }[e.key];
          if (d != null) pin(pinned + d, e.currentTarget);
          else if (e.key === "Home") pin(0, e.currentTarget);
          else if (e.key === "End") pin(n - 1, e.currentTarget);
          else return;
          e.preventDefault();
        }}
      >
        <circle cx={C} cy={C} r={R1 + 4} className={`lumen-day-rings-well`} />
        {model.days.map((d, i) => {
          const a = angleOf(i);
          const on = i === pinned;
          return (
            <g key={d.date} className={`lumen-day-rings-spoke`} data-on={on || undefined} data-closed={d.closed || undefined}>
              {shownGoals.map((g) => {
                const gi = goals.indexOf(g);
                const r = d.ratios[gi];
                const lo = R0 + shownGoals.indexOf(g) * band + 1.5;
                if (r == null) {
                  const [x0, y0] = polar(lo, a);
                  const [x1, y1] = polar(lo + band - 3, a);
                  return <line key={g.id} x1={x0} y1={y0} x2={x1} y2={y1} className={`lumen-day-rings-missing`} />;
                }
                const len = Math.max(1.2, (band - 3) * Math.min(1, r));
                const [x0, y0] = polar(lo, a);
                const [x1, y1] = polar(lo + len, a);
                return <line key={g.id} x1={x0} y1={y0} x2={x1} y2={y1} stroke={chartColor(g.id)} className={`lumen-day-rings-seg`} data-met={r >= 1 || undefined} />;
              })}
            </g>
          );
        })}
        {shownGoals.map((g, k) => {
          const gi = goals.indexOf(g);
          const r = day.ratios[gi] ?? 0;
          const rad = ARC0 + k * ARC_GAP;
          const sweep = Math.min(0.9999, Math.max(0.001, r)) * Math.PI * 2;
          return (
            <g key={g.id} className={`lumen-day-rings-arc`} data-over={r > 1 || undefined} style={{ color: chartColor(g.id) } as React.CSSProperties}>
              <circle cx={C} cy={C} r={rad} className={`lumen-day-rings-track`} />
              {r > 0 && <path d={arcPath(rad, 0, sweep)} className={`lumen-day-rings-fill`} />}
            </g>
          );
        })}
        {(() => {
          const [px, py] = polar(R1 + 4, angleOf(pinned));
          return <circle cx={px} cy={py} r={4.5} className={`lumen-day-rings-pin`} />;
        })()}
      </svg>
      <div className={`lumen-day-rings-center`} aria-hidden>
        <span className={`lumen-day-rings-when`}>{isToday ? L.today : dw.short}</span>
        <span className={`lumen-day-rings-pct`}>
          <StatValue value={pct == null ? null : Math.round(Math.min(pct, 9.99) * 100)} format={{ notation: "standard" }} />
          {pct != null && <span className={`lumen-day-rings-pct-unit`}>%</span>}
        </span>
        <span className={`lumen-day-rings-met`}>
          {goals.map((g, gi) => (
            <i key={g.id} data-on={(day.ratios[gi] ?? 0) >= 1 || undefined} style={{ color: chartColor(g.id) } as React.CSSProperties} />
          ))}
        </span>
        <span className={`lumen-day-rings-closed`}>{fgGoal ? `${format(day.values[fg], fgGoal)} / ${format(fgGoal.goal, fgGoal)} ${fgGoal.unit}` : L.closedOf(day.metCount, goals.length)}</span>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------------------------
// Goals, filter, summary, logs, table
// ---------------------------------------------------------------------------------------------

/** Each goal on the pinned day: value against target, % and a meter of cells, vs its average. */
export function DayRingsGoals({ className, ...props }: React.ComponentProps<"div">) {
  const { model, L, pinned, filter, format } = useDayRings();
  const day = model.days[pinned];
  if (!day) return null;
  return (
    <div data-slot="day-rings-goals" className={cn(`lumen-day-rings-goals`, className)} {...props}>
      {model.goals.map((g, gi) => {
        const v = day.values[gi];
        const r = day.ratios[gi];
        // The model keeps one stats entry per goal.
        const d = vsAverage(v, inBounds(model.stats, gi).average);
        const lit = r == null ? 0 : Math.round(Math.min(1, r) * 30);
        return (
          <div key={g.id} className={`lumen-day-rings-goal`} data-dim={(filter && filter !== g.id) || undefined} data-met={(r ?? 0) >= 1 || undefined} style={{ color: chartColor(g.id) } as React.CSSProperties}>
            <div className={`lumen-day-rings-goal-head`}>
              <span className={`lumen-day-rings-goal-name`}>{g.label}</span>
              <span className={`lumen-day-rings-goal-pct`}>{r == null ? L.noValue : `${Math.round(r * 100)}%`}</span>
            </div>
            <div className={`lumen-day-rings-goal-sub`}>
              <span>
                {format(v, g)} / {format(g.goal, g)} {g.unit}
              </span>
              {d != null && <span>{L.vsAvg(`${d >= 0 ? "+" : "−"}${Math.abs(Math.round(d * 100))}%`)}</span>}
            </div>
            <span aria-hidden className={`lumen-day-rings-meter`}>
              {Array.from({ length: 30 }, (_, i) => (
                <i key={i} data-on={i < lit || undefined} />
              ))}
            </span>
          </div>
        );
      })}
    </div>
  );
}

/** All goals or one: each tab says the goal's average (All says how many days closed). */
export function DayRingsFilter({ className, ...props }: Omit<React.ComponentProps<typeof ChartModes>, "value" | "onValueChange" | "defaultValue">) {
  const { model, L, filter, setFilter, format } = useDayRings();
  return (
    <ChartModes value={filter ?? "__all"} onValueChange={(v: string) => setFilter(v === "__all" ? null : v)} aria-label="Goals" className={cn(`lumen-day-rings-filter`, className)} {...props}>
      <ChartMode value="__all">
        <span className={`lumen-day-rings-tab`}>
          <span className={`lumen-day-rings-tab-name`}>{L.all}</span>
          <span className={`lumen-day-rings-tab-sub`}>{L.closedCount(model.closedDays)}</span>
        </span>
      </ChartMode>
      {model.goals.map((g, gi) => (
        <ChartMode key={g.id} value={g.id}>
          <span className={`lumen-day-rings-tab`} style={{ "--dr-c": chartColor(g.id) } as React.CSSProperties}>
            <span className={`lumen-day-rings-tab-name`}>{g.label}</span>
            <span className={`lumen-day-rings-tab-sub`}>{L.average(`${format(inBounds(model.stats, gi).average == null ? null : Math.round(inBounds(model.stats, gi).average!), g)} ${g.unit}`)}</span>
          </span>
        </ChartMode>
      ))}
    </ChartModes>
  );
}

/** "12 of 30 days closed", with a cell per day lit when it closed. */
export function DayRingsSummary({ className, ...props }: React.ComponentProps<"div">) {
  const { model, L } = useDayRings();
  return (
    <div data-slot="day-rings-summary" className={cn(`lumen-day-rings-summary`, className)} {...props}>
      <span aria-hidden className={`lumen-day-rings-summary-cells`}>
        {model.days.map((d) => (
          <i key={d.date} data-on={d.closed || undefined} />
        ))}
      </span>
      {L.daysClosed(model.closedDays, model.days.length)}
    </div>
  );
}

/** Quick logs for today, undo, and the way back to today. */
export function DayRingsLog({ className, ...props }: React.ComponentProps<"div">) {
  const { model, L, log, undo, canUndo, readOnly, pinned, pin } = useDayRings();
  const today = model.days.length - 1;
  return (
    <div data-slot="day-rings-log" className={cn(`lumen-day-rings-log`, className)} {...props}>
      {!readOnly && (
        <>
          {model.goals.map((g, gi) => (
            <button key={g.id} type="button" data-sound="none" className={`lumen-day-rings-chip`} style={{ "--dr-c": chartColor(g.id) } as React.CSSProperties} onClick={(e) => log(gi, e.currentTarget)}>
              <i aria-hidden />
              {g.chip ?? `+${g.step} ${g.unit}`}
            </button>
          ))}
          <button type="button" data-sound="none" className={`lumen-day-rings-undo`} aria-label={L.undo} title={L.undo} disabled={!canUndo} onClick={(e) => undo(e.currentTarget)}>
            <svg aria-hidden viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M6 4 3 7l3 3M3 7h6.5a3.5 3.5 0 0 1 0 7H8" />
            </svg>
          </button>
        </>
      )}
      <span className={`lumen-day-rings-hint`}>
        {pinned !== today ? (
          <button type="button" className={`lumen-day-rings-today`} onClick={(e) => pin(today, e.currentTarget)}>
            {L.todayButton}
          </button>
        ) : (
          L.hint
        )}
      </span>
    </div>
  );
}

/** Every day as a table, for screen readers (and on demand when large). */
export function DayRingsTable(props: Omit<React.ComponentProps<typeof ChartTable>, "caption" | "columns" | "rows">) {
  const { model, L, format, locale } = useDayRings();
  return (
    <ChartTable
      caption={L.tableCaption}
      columns={[L.dayHeader, ...model.goals.map((g) => `${g.label} (${g.unit})`), L.closedHeader]}
      rows={model.days.map((d) => [dayWords(d.date, locale).long, ...model.goals.map((g, gi) => format(d.values[gi], g)), d.closed ? "Yes" : "No"])}
      {...props}
    />
  );
}

/** The whole card on the sample month (or your data): header, ring, goals, filter and logs. */
export function DayRingsCard({ eyebrow = "Team rituals", heading = "Thirty days, at a glance", description, ...props }: DayRingsProps & { eyebrow?: string | undefined; heading?: string | undefined; description?: string | undefined }) {
  return (
    <DayRings {...props}>
      <DayRingsCardBody eyebrow={eyebrow} heading={heading} description={description} />
    </DayRings>
  );
}

function DayRingsCardBody({ eyebrow, heading, description }: { eyebrow: string; heading: string; description?: string | undefined }) {
  const { model, locale } = useDayRings();
  const first = model.days[0];
  const last = model.days[model.days.length - 1];
  return (
    <ChartSurface className={`lumen-day-rings-card`}>
      <ChartHeader className={`lumen-day-rings-header`}>
        <div>
          <ChartEyebrow live>
            {eyebrow}
            {first && last ? ` · ${dayWords(first.date, locale).short} – ${dayWords(last.date, locale).short}` : ""}
          </ChartEyebrow>
          <ChartTitle>{heading}</ChartTitle>
          {description && <ChartDescription>{description}</ChartDescription>}
        </div>
        <DayRingsSummary />
      </ChartHeader>
      <div className={`lumen-day-rings-body`}>
        <DayRingsRing />
        <DayRingsGoals />
      </div>
      <DayRingsFilter />
      <DayRingsLog />
      <DayRingsTable />
    </ChartSurface>
  );
}

export { buildDayRings, dayRingsLabels, type DayRingsDay, type DayRingsGoal, type DayRingsLabels, type DayRingsModel };
```

### lib/beautiful-ui/lumen/day-rings-model.ts

```tsx
/*
 * Lumen Day Rings' model: plain data in, plain data out (no React), so a Server Component can build
 * it. Days are ISO calendar dates ("2026-09-26"), oldest first, the last one is today. Each day holds
 * one value per goal; missing values stay missing (never zero). A day is "closed" when every goal
 * with a value met its target.
 */

export type DayRingsGoal = {
  /** A stable id: the config key for the goal's colour. */
  id: string;
  label: string;
  /** The unit in words ("min", "reviews"). */
  unit: string;
  /** The daily target. */
  goal: number;
  /** How much one quick log adds. */
  step: number;
  /** The quick-log chip's words, e.g. "+30 min". Default "+{step} {unit}". */
  chip?: string;
};

export type DayRingsDay = { date: string; values: readonly (number | null | undefined)[] };

export type DayRingsGoalStat = {
  /** Average over days with a value. */
  average: number | null;
  /** Days this goal met its target. */
  met: number;
};

export type DayRingsModel = {
  goals: DayRingsGoal[];
  days: { date: string; values: (number | null)[]; ratios: (number | null)[]; closed: boolean; metCount: number; score: number | null }[];
  stats: DayRingsGoalStat[];
  closedDays: number;
};

const ISO = /^\d{4}-\d{2}-\d{2}$/;
const clean = (v: unknown) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null);

export function buildDayRings(goals: readonly DayRingsGoal[], days: readonly DayRingsDay[]): DayRingsModel {
  const gs = goals.filter((g) => g.goal > 0);
  const ds = days
    .filter((d) => ISO.test(d.date))
    .map((d) => {
      const values = gs.map((_, i) => clean(d.values[i]));
      const ratios = gs.map((g, i) => {
        const v = values[i];
        return v == null ? null : v / g.goal;
      });
      const known = ratios.filter((r): r is number => r != null);
      const metCount = known.filter((r) => r >= 1).length;
      return {
        date: d.date,
        values,
        ratios,
        metCount,
        // Closed: every goal has a value and met it. A day with a missing goal can't be closed.
        closed: known.length === gs.length && metCount === gs.length,
        // The day's score: average progress over known goals, each capped at 100%.
        score: known.length ? known.reduce((s, r) => s + Math.min(1, r), 0) / known.length : null,
      };
    });
  const stats = gs.map((_, i) => {
    const vs = ds.map((d) => d.values[i]).filter((v): v is number => v != null);
    return { average: vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null, met: ds.filter((d) => (d.ratios[i] ?? 0) >= 1).length };
  });
  return { goals: gs, days: ds, stats, closedDays: ds.filter((d) => d.closed).length };
}

/** A goal's value today against its average: "+12% vs avg", or null without both. */
export function vsAverage(value: number | null | undefined, average: number | null | undefined): number | null {
  if (value == null || average == null || average === 0) return null;
  return value / average - 1;
}

/** A logged delta applied to one day's value (never below zero). */
export function applyLog(model: DayRingsModel, dayIndex: number, goalIndex: number, delta: number): DayRingsDay[] {
  return model.days.map((d, i) => ({
    date: d.date,
    values: i === dayIndex ? d.values.map((v, g) => (g === goalIndex ? Math.max(0, (v ?? 0) + delta) : v)) : d.values,
  }));
}

/** Short and long date words for an ISO day, without time-zone shifts. */
export function dayWords(iso: string, locale?: string) {
  const d = new Date(`${iso}T00:00:00Z`);
  return {
    short: new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", timeZone: "UTC" }).format(d),
    long: new Intl.DateTimeFormat(locale, { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" }).format(d),
  };
}

export type DayRingsLabels = {
  all: string;
  today: string;
  closedOf: (n: number, total: number) => string;
  daysClosed: (n: number, total: number) => string;
  closedCount: (n: number) => string;
  goalsClosed: (n: number, total: number) => string;
  average: (v: string) => string;
  vsAvg: (pct: string) => string;
  noValue: string;
  undo: string;
  hint: string;
  todayButton: string;
  ring: string;
  logged: (chip: string, goal: string) => string;
  undone: (goal: string) => string;
  tableCaption: string;
  dayHeader: string;
  closedHeader: string;
};

export const dayRingsLabels: DayRingsLabels = {
  all: "All",
  today: "Today",
  closedOf: (n, t) => `${n} of ${t} closed`,
  daysClosed: (n, t) => `${n} of ${t} days closed`,
  closedCount: (n) => `${n} ${n === 1 ? "day" : "days"} closed`,
  goalsClosed: (n, t) => `${n} of ${t} goals closed`,
  average: (v) => `avg ${v}`,
  vsAvg: (p) => `${p} vs avg`,
  noValue: "No data",
  undo: "Undo the last log",
  hint: "Point at the ring or use the arrow keys to scrub days",
  todayButton: "Today →",
  ring: "Days",
  logged: (c, g) => `Logged ${c} for ${g}`,
  undone: (g) => `Undid the last ${g} log`,
  tableCaption: "Goals by day",
  dayHeader: "Day",
  closedHeader: "All goals met",
};
```

### lib/beautiful-ui/lumen/day-rings-sample.ts

```tsx
import type { DayRingsDay, DayRingsGoal } from "@/lib/beautiful-ui/lumen/day-rings-model";

/* Lumen Day Rings' sample: a team's rituals over thirty days, fixed so server and client agree. */

export const sampleGoals: DayRingsGoal[] = [
  { id: "focus", label: "Focus", unit: "min", goal: 240, step: 30, chip: "+30 min" },
  { id: "reviews", label: "Reviews", unit: "reviews", goal: 8, step: 1, chip: "+1 review" },
  { id: "deploys", label: "Deploys", unit: "deploys", goal: 6, step: 1, chip: "+1 deploy" },
];

const START = Date.UTC(2026, 7, 28); // Aug 28, 2026

export const sampleDays: DayRingsDay[] = Array.from({ length: 30 }, (_, i) => {
  const date = new Date(START + i * 86_400_000).toISOString().slice(0, 10);
  const weekday = new Date(START + i * 86_400_000).getUTCDay();
  const weekend = weekday === 0 || weekday === 6;
  const w = (a: number, b: number) => Math.abs(Math.sin(i * a + b));
  return {
    date,
    values: [
      Math.round((weekend ? 70 : 150) + w(1.3, 0.4) * (weekend ? 90 : 140)),
      Math.round((weekend ? 1 : 5) + w(0.9, 1.1) * (weekend ? 3 : 5)),
      // Two days lost their deploy counts (the pipeline's webhook was down).
      i === 11 || i === 12 ? null : Math.round((weekend ? 1 : 3) + w(1.7, 2.3) * (weekend ? 2 : 4)),
    ],
  };
});
```

### 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
@layer components {
  .lumen-day-rings {
    container-type: inline-size;
    color: var(--gc-ink);
    font: 400 13px/1.4 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-day-rings-card {
    gap: 22px;
  }
  .lumen-day-rings-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px 24px;
  }
  .lumen-day-rings-body {
    display: grid;
    grid-template-columns: minmax(0,1fr) minmax(0,1.1fr);
    align-items: center;
    gap: 28px;
  }
  @container (max-width:620px) {
    .lumen-day-rings-body {
      grid-template-columns: minmax(0,1fr);
      gap: 20px;
    }
  }
  .lumen-day-rings-ring {
    position: relative;
    width: 100%;
    margin: 0 auto;
    aspect-ratio: 1;
  }
  .lumen-day-rings-ring>svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
    border-radius: 50%;
    outline: none;
    touch-action: none;
    cursor: crosshair;
  }
  .lumen-day-rings-ring>svg:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-day-rings-well {
    fill: color-mix(in srgb,var(--gc-ink) 3%,transparent);
    stroke: var(--gc-hair);
  }
  .lumen-day-rings-spoke {
    opacity: .62;
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-day-rings-spoke[data-on] {
    opacity: 1;
  }
  .lumen-day-rings-seg {
    stroke-width: 4.2;
    stroke-linecap: round;
  }
  .lumen-day-rings-spoke[data-on] .lumen-day-rings-seg,.lumen-day-rings-seg[data-met] {
    filter: drop-shadow(0 0 3px currentColor);
  }
  .lumen-day-rings-spoke[data-on] .lumen-day-rings-seg {
    stroke-width: 5.4;
  }
  .lumen-day-rings-missing {
    stroke: var(--gc-hair);
    stroke-width: 1.5;
    stroke-dasharray: 1.5 2.5;
  }
  .lumen-day-rings-track {
    fill: none;
    stroke: color-mix(in srgb,currentColor 14%,transparent);
    stroke-width: 4.5;
  }
  .lumen-day-rings-fill {
    fill: none;
    stroke: currentColor;
    stroke-width: 4.5;
    stroke-linecap: round;
    transition: d min(0.4s, var(--gcp-cm)) ease;
  }
  .lumen-day-rings-arc[data-over] .lumen-day-rings-fill {
    filter: drop-shadow(0 0 6px currentColor);
  }
  .lumen-day-rings-pin {
    fill: var(--gc-ink);
    stroke: var(--background,#000);
    stroke-width: 2;
    filter: drop-shadow(0 0 5px var(--gc-ink));
    transition: cx calc(0.25s * var(--gcp-k)) cubic-bezier(.22,1,.36,1),cy calc(0.25s * var(--gcp-k)) cubic-bezier(.22,1,.36,1);
  }
  .lumen-day-rings-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-day-rings-when {
    font: 500 10.5px/1 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-day-rings-pct {
    display: flex;
    align-items: flex-start;
    gap: 2px;
  }
  .lumen-day-rings-pct .lumen-stat-value {
    --st-fs: clamp(34px,11cqi,52px);
    font-weight: 600;
  }
  .lumen-day-rings-pct-unit {
    margin-top: 6px;
    font: 500 15px/1 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-sec);
  }
  .lumen-day-rings-met {
    display: flex;
    gap: 4px;
  }
  .lumen-day-rings-met>i {
    width: 18px;
    height: 4px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
  }
  .lumen-day-rings-met>i[data-on] {
    background: currentColor;
    box-shadow: 0 0 6px currentColor;
  }
  .lumen-day-rings-closed {
    font: 500 11px/1.2 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: var(--gc-sec);
  }
  .lumen-day-rings-goals {
    display: flex;
    flex-direction: column;
    gap: 20px;
    min-width: 0;
  }
  .lumen-day-rings-goal {
    transition: opacity min(0.25s, var(--gcp-cm)) ease;
  }
  .lumen-day-rings-goal[data-dim] {
    opacity: .4;
  }
  .lumen-day-rings-goal-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
  }
  .lumen-day-rings-goal-name {
    font: 600 16px/1.2 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: var(--gc-ink);
  }
  .lumen-day-rings-goal-pct {
    font: 600 16px/1.2 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-day-rings-goal-sub {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 4px 12px;
    margin: 4px 0 10px;
    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-sec);
  }
  .lumen-day-rings-meter {
    display: grid;
    grid-template-columns: repeat(30,minmax(0,1fr));
    gap: 3px;
  }
  .lumen-day-rings-meter>i {
    height: 8px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
  }
  .lumen-day-rings-meter>i[data-on] {
    background: currentColor;
  }
  .lumen-day-rings-goal[data-met] .lumen-day-rings-meter>i[data-on] {
    box-shadow: 0 0 5px color-mix(in srgb,currentColor 60%,transparent);
  }
  .lumen-day-rings-filter {
    width: 100%;
  }
  .lumen-day-rings-filter [role=radio] {
    flex: 1;
    min-width: 0;
    height: auto;
    padding: 10px 14px;
  }
  .lumen-day-rings-tab {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    min-width: 0;
    text-align: start;
  }
  .lumen-day-rings-tab-name {
    font: 500 14px/1.2 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
    color: inherit;
  }
  .lumen-day-rings-tab-name::before {
    content: "";
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-inline-end: 7px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: var(--dr-c,currentColor);
    vertical-align: 1px;
  }
  .lumen-day-rings-tab-sub {
    overflow: hidden;
    max-width: 100%;
    font: 500 11px/1.2 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: inherit;
    opacity: .68;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lumen-day-rings-summary {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    height: 34px;
    padding: 0 14px;
    border-radius: calc(999px * var(--lumen-radius-k, 1));
    box-shadow: inset 0 0 0 1px var(--gc-hair);
    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-day-rings-summary-cells {
    display: flex;
    gap: 1.5px;
  }
  .lumen-day-rings-summary-cells>i {
    width: 3px;
    height: 10px;
    border-radius: calc(1px * var(--lumen-radius-k, 1));
    background: var(--gc-cellOff);
  }
  .lumen-day-rings-summary-cells>i[data-on] {
    background: var(--gc-good);
    box-shadow: 0 0 4px var(--gc-good);
  }
  .lumen-day-rings-log {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding-top: 16px;
    border-top: 1px solid var(--gc-hair);
  }
  .lumen-day-rings-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 38px;
    margin: 0;
    padding: 0 16px;
    border: 0;
    border-radius: calc(999px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--dr-c) 9%,transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb,var(--dr-c) 30%,transparent);
    color: var(--gc-ink);
    font: 500 13px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    cursor: pointer;
    outline: none;
    transition: transform calc(0.2s * var(--gcp-k)) var(--gc-sp),background-color min(0.2s, var(--gcp-cm)) ease;
  }
  .lumen-day-rings-chip>i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--dr-c);
    box-shadow: 0 0 6px var(--dr-c);
  }
  @media (hover:hover) {
    .lumen-day-rings-chip:hover {
      background: color-mix(in srgb,var(--dr-c) 16%,transparent);
    }
  }
  .lumen-day-rings-chip:active {
    transform: scale(.96);
  }
  .lumen-day-rings-chip:focus-visible,.lumen-day-rings-undo:focus-visible,.lumen-day-rings-today:focus-visible {
    box-shadow: 0 0 0 2px var(--gc-gap), 0 0 0 4px var(--gc-p-focus), 0 0 #0000;
  }
  .lumen-day-rings-undo {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    box-shadow: inset 0 0 0 1px var(--gc-hair);
    color: var(--gc-sec);
    cursor: pointer;
    outline: none;
  }
  .lumen-day-rings-undo:disabled {
    opacity: .35;
    cursor: default;
  }
  .lumen-day-rings-undo svg {
    width: 15px;
    height: 15px;
  }
  .lumen-day-rings-hint {
    margin-inline-start: auto;
    font: 500 11.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    color: var(--gc-sec);
  }
  .lumen-day-rings-today {
    height: 32px;
    margin: 0;
    padding: 0 12px;
    border: 0;
    border-radius: calc(999px * var(--lumen-radius-k, 1));
    background: color-mix(in srgb,var(--gc-acc) 12%,transparent);
    color: var(--gc-ink);
    font: 500 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    cursor: pointer;
    outline: none;
  }
  @container (max-width:520px) {
    .lumen-day-rings-tab-sub {
      display: none;
    }
    .lumen-day-rings-filter [role=radio] {
      padding: 8px 6px;
    }
    .lumen-day-rings .lumen-day-rings-hint {
      flex-basis: 100%;
      margin-inline-start: 0;
    }
    .lumen-day-rings .lumen-day-rings-summary-cells {
      display: none;
    }
  }
  @media (prefers-reduced-motion:reduce) {
    .lumen-day-rings-pin,.lumen-day-rings-fill,.lumen-day-rings-chip {
      transition: none;
    }
  }
}
```

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.
