# Alert volume

> A security card of alerts triaged per day or week, stacked by source, with the escalated few drawn below the line.

The card a security team opens to see how many alerts were triaged, and by what. Switch between days and weeks, filter by date range, source and verdict, or drop the grouping; every bar stacks its alerts by source above the baseline and its escalations below it on a scale of their own. Hover a bar and a ring closes round it while a frosted note lists its top three sources, and four figures under the chart follow the filters.

A React component by Bruno Delorence: https://brunodelorence.com/components/alert-volume. MIT License, Copyright (c) 2026 Bruno Delorence.

Category: Data.

## Anatomy

1. **Day / Week switch**: A radio group: one bar per day over the last 30 days, or per week over the last 12. Each keeps a date range of its own.
2. **Filters**: Date range, source, grouping and verdict, each a small label over its value that opens a listbox. Two by two under 520 px.
3. **Total**: The alerts the bars show, with what they are (triaged, closed without a human, escalated) and over how long. The share closed by the product sits at the right.
4. **Legend**: A swatch a source, a touch paler than its bars, and Escalated while every verdict is shown. One grey entry when the bars are not grouped.
5. **Bars**: One SVG at the width it is given: alerts stacked by source above the baseline (the tallest 172 px), escalations below it on their own scale (the deepest 40 px).
6. **Ring**: Closes round the hovered bar, 3 px off its sides and 4 px over its top, down past its escalations. The pointer or the arrow keys move it.
7. **Note**: The bar’s date and total, its top three sources and its escalations, frosted over the chart on the roomier side, with a dotted line to the ring.
8. **Figures**: Auto-closed, escalated, median triage and analyst time saved, over the whole period. They follow the filters.

## When to use

- A count that comes in by day or week and splits into a few named sources, where the mix matters as much as the total.
- A small share that needs its own scale beside the whole: escalations under the line, read on their own axis.
- An overview card a team opens daily, where a handful of filters (period, source, grouping, verdict) cover most questions.
- Up to six or seven series; past that the stacks turn to noise and the legend wraps for lines.

## When not to use

- A trend over many months where only the line matters. Use a line or area chart, which reads change better than bars.
- Comparing sources against each other: stacked segments share no baseline. Use grouped bars or small multiples.
- One figure with no history behind it. Use a stat tile, or the four figures at the bottom on their own.
- Data whose readers must act on each row (a queue of alerts to triage). Use a table with filters, and link the card to it.
- Many more than 60 bars: they thin out to slivers. Sum them into weeks, or months, first.

## Installation

Install it with the shadcn CLI, into components/alert-volume of any React project:

```sh
npx shadcn@latest add https://brunodelorence.com/r/alert-volume.json
```

Or copy all the source files below into one folder of your project. Either way it needs React 19.2 and a bundler with CSS Modules (Next.js, Vite).

It has a dark colour scheme. Its colours are CSS light-dark() pairs, kept inside @supports so that builds which rewrite light-dark() for older browsers leave them alone: it follows your page’s color-scheme (light unless the page sets one) or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

## Usage

```tsx
import { AlertVolume } from './alert-volume';

export function SecurityOverview() {
  return <AlertVolume product="Halyard" defaultGranularity="week" autoplay={false} />;
}
```

## States

- **By day**: Where it opens: 30 bars stacked by six sources, the escalations under the line.
- **Bar hovered**: The ring closes round the 2 October phishing spike and the note lists its top sources.
- **By week**: Twelve wider bars, each a Monday-to-Sunday sum; the scale steps up to match.
- **Last 60 days**: The longest day range: the bars thin to fit and the x-axis labels space out.
- **One source**: Email only: one colour, one legend entry, and the figures for email alone.
- **Not grouped**: Group by None: a grey bar for all sources together.
- **Auto-closed only**: Alerts closed without a person: nothing is drawn under the line.
- **Escalated only**: The few that reached an analyst, now on the main scale above the line.
- **Filter open**: The source listbox open, its picked row lit and checked. It eases in over 160 ms.

## How it works

- The data is 26 invented weeks: the last 30 days follow a fixed outline (quiet weekends, a phishing spike on 2 October with 900 email alerts), the weeks before them come from a seeded generator, so every load draws the same chart. Weeks are summed Monday to Sunday from the days.
- The chart is one SVG drawn at the width it is given, read by a ResizeObserver, so its type and spacing keep their size from a phone to a desktop and only the bars’ width (65% of each slot, 24 px at most) follows the space.
- Both scales fill their room: the tallest bar is 172 px and the most escalations 40 px. Grid lines fall on a round step that leaves two or three under the tallest bar (1k, 2k; 5k, 10k), and one round label sits under the baseline.
- Stacked segments are drawn 2 px short of their share, which leaves the gaps between them without moving the top of the bar. Escalations are drawn at 87% opacity, and at full strength on the hovered bar.
- The pointer picks the bar nearest to it; the ring stands 3 px off the bar’s sides and 4 px over its top, down to 3 px under the bottom of its escalations. The note goes on the roomier side with a dotted line to the ring, or floats over the plot when neither side has room, and blurs what is behind it.
- A change of view (switch, range, source, grouping, verdict) gives the bars a new key, so they grow again from the baseline, 9 ms apart: transforms on their own groups, nothing laid out.
- The Day / Week switch keeps a range of its own for each (30 days, 12 weeks). The filters are listboxes the card holds open or shut, which lets the demo open one and move through it the way a reader would.
- While on screen and untouched, a demo pointer hovers the spike and the days around it, switches to weeks, filters to email and back, and returns to days. Any input of the reader’s stops it, and leaves what the reader is hovering in place.
- Every colour is a light-dark() token on the root: a near-black card on a dim stage at night, a white one on pale grey by day, with each source a step deeper so it holds on white.

## API reference

- **product** (`string`, default `'Halyard'`): The product that triages the alerts, named under the title.
- **defaultGranularity** (`'day' | 'week'`, default `'day'`): One bar per day (the last 30 days) or per week (the last 12 weeks), at first.
- **defaultSource** (`'all' | 'Identity' | 'Endpoint' | 'Cloud' | 'Email' | 'Network' | 'SaaS'`, default `'all'`): Only one source’s alerts, or all of them, at first.
- **defaultGroupBy** (`'source' | 'none'`, default `'source'`): Bars stacked by source, or in one colour, at first.
- **defaultVerdict** (`'all' | 'auto' | 'escalated'`, default `'all'`): All alerts (escalations drawn below the line), only those closed without a person, or only the escalated ones, at first.
- **defaultActive** (`number | null`, default `null`): The bar shown hovered at first, by its index from the left, until the pointer or the keys move on.
- **defaultRange** (`string`): The date range at first, by its id, one of the granularity’s: '30d', '14d' or '60d' by day, '12w' or '26w' by week (by default the first). Turns the demo off.
- **defaultMenu** (`'range' | 'source' | 'group' | 'verdict' | null`, default `null`): A filter’s menu open at first, its picked option lit, until the reader clicks. Turns the demo off.
- **autoplay** (`boolean`, default `true`): While on screen and untouched, a demo pointer hovers bars, switches to weeks and filters by a source.
- **colorScheme** (`'light' | 'dark' | 'system'`): Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
- **className** (`string`): Extra class for the root.

## Accessibility

- The card is a section named by its title. Day / Week is a radio group (arrow keys move between them); each filter is a button that opens a listbox, worked with the arrow keys, Home, End, Enter and Escape, and the focus returns to the button.
- The chart is a focusable group with a name that gives the total and the period. The arrow keys, Home and End move from bar to bar, showing the ring and the note, and a polite live region reads out each bar’s date, total, top sources and escalations.
- Changing the view announces the new total and dates. The legend is a list, the four figures a description list; the bars, the note and the demo pointer are hidden from screen readers.
- Under reduced motion the bars appear without growing, menus open without easing and the demo never plays.

## Source

### alert-volume/alert-volume.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import {
  useCallback,
  useEffect,
  useEffectEvent,
  useId,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from 'react';
import {
  ANALYST_SECONDS,
  MEDIAN_TRIAGE,
  MEDIAN_TRIAGE_ALL,
  RANGES,
  SOURCES,
  buckets,
  longDate,
  shortDate,
  span,
  type Granularity,
} from './data';
import { DemoPointer, runDemo, type Point } from './demo-pointer';
import { FilterMenu, type Option } from './filter-menu';
import { useAutoplay } from './use-autoplay';
import styles from './alert-volume.module.css';

/*
 * Alert volume: the card a security team opens to see how many alerts were triaged, and by what.
 * A Day / Week switch, four filters (date range, source, grouping, verdict), the total, a legend,
 * and a bar per day or week: alerts stacked by source above the baseline, the escalated few drawn
 * below it on a scale of their own. Hovering a bar rings it and opens a note with its top three
 * sources; four figures close the card.
 *
 * - The chart is one SVG drawn at the width it is given (a ResizeObserver), so its type and its
 *   bars keep their size from a phone to a desktop; only the bars' width follows the space.
 * - The scales fill the space: the tallest bar is 172 px, the most escalations 42 px, and the
 *   grid lines fall on round numbers under them.
 * - A change of view grows the new bars from the baseline, a few ms apart (transforms only).
 * - While on screen and untouched, a demo pointer hovers bars, switches to weeks, filters by a
 *   source and back. Any input of the reader's stops it.
 * - Every colour is a light-dark() token in the CSS: the page's color-scheme, or colorScheme's.
 */

type SourceFilter = 'all' | (typeof SOURCES)[number];
type Grouping = 'source' | 'none';
type Verdict = 'all' | 'auto' | 'escalated';
type MenuName = 'range' | 'source' | 'group' | 'verdict';

/** The chart's geometry, in px. */
const HEIGHT = 262;
/** The baseline's y. */
const BASE = 180;
/** The tallest positive bar, and the deepest escalation bar. */
const POS_MAX = 172;
const NEG_MAX = 40;
/** Room between the baseline and the bars on either side, and between stacked segments. */
const POS_GAP = 2;
const NEG_GAP = 2;
const SEG_GAP = 2;
/** The column of y-axis labels on the right, and where its labels start. */
const AXIS = 44;
const AXIS_INSET = 11.5;
/** The bars start a little in from the plot's left end; the last one stops short of its right end. */
const LEAD = 2;
const TAIL = 4;
/** The space between two bars (the bars are 65% of a slot, at most 24 px). */
const BAR_GAP = 6;
/** The x-axis labels' line. */
const X_LABELS = 246;
/** How far the ring around a hovered bar stands off it: its sides, its top, its bottom. */
const RING = 3;
const RING_TOP = 4;
const RING_BOTTOM = 3;

const KEYS = ['identity', 'endpoint', 'cloud', 'email', 'network', 'saas'];
const color = (source: number) => `var(--${KEYS[source]})`;

const fmt = (value: number) => Math.round(value).toLocaleString('en-US');
/** "2k", "2.5k", "500". */
const tick = (value: number) =>
  value >= 1000 ? `${+(value / 1000).toFixed(1)}k` : String(Math.round(value));

/** A round step that puts two or three grid lines under `max`. */
function niceStep(max: number) {
  if (max <= 0) return 1;
  const base = 10 ** Math.floor(Math.log10(max));
  for (const factor of [0.1, 0.2, 0.25, 0.5, 1, 2, 2.5, 5, 10]) {
    const step = base * factor;
    if (step >= 1 && max / step <= 3.5) return step;
  }
  return base * 10;
}

/** The largest round number at most `max`, for the one label under the baseline. */
function roundBelow(max: number) {
  if (max < 1) return 0;
  const base = 10 ** Math.floor(Math.log10(max));
  for (const factor of [5, 2, 1]) if (base * factor <= max) return base * factor;
  return base;
}

type Segment = { key: string; label: string; color: string; value: number };
type Bar = {
  start: number;
  end: number;
  segments: Segment[];
  total: number;
  escalated: number;
};

/** A menu's lit option, standing for the one picked (the menu defaultMenu opens). */
const PICKED = -2;

/** The demo keeps an even pace: each glide takes FLIGHT ms. */
const FLIGHT = 560;

export function AlertVolume({
  product = 'Halyard',
  defaultGranularity = 'day',
  defaultSource = 'all',
  defaultGroupBy = 'source',
  defaultVerdict = 'all',
  defaultActive = null,
  defaultRange,
  defaultMenu = null,
  autoplay = true,
  colorScheme,
  className = '',
}: {
  /** The name of the product that triages the alerts, under the title. */
  product?: string;
  /** One bar per day (the last 30 days) or per week (the last 12 weeks). */
  defaultGranularity?: Granularity;
  /** Only one source's alerts, or all of them. */
  defaultSource?: SourceFilter;
  /** Bars stacked by source, or one colour. */
  defaultGroupBy?: Grouping;
  /** All alerts, only those closed without a person, or only the escalated ones. */
  defaultVerdict?: Verdict;
  /** The bar shown hovered at first (its index from the left), until the pointer moves. */
  defaultActive?: number | null;
  /** The date range at first, by its id ('30d', '14d', '60d' by day; '12w', '26w' by week); turns the demo off. */
  defaultRange?: string;
  /** The filter shown open at first, its picked option lit, until the reader clicks; turns the demo off. */
  defaultMenu?: MenuName | null;
  /** While on screen and untouched, a demo pointer hovers bars and works the switch and filters. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  className?: string;
}) {
  const { ref: root, playing, take } = useAutoplay<HTMLDivElement>(8000);
  const [granularity, setGranularity] = useState<Granularity>(defaultGranularity);
  const [rangeId, setRangeId] = useState(defaultRange ?? RANGES[defaultGranularity][0].id);
  const [source, setSource] = useState<SourceFilter>(defaultSource);
  const [groupBy, setGroupBy] = useState<Grouping>(defaultGroupBy);
  const [verdict, setVerdict] = useState<Verdict>(defaultVerdict);
  const [active, setActive] = useState<number | null>(defaultActive);
  const [menu, setMenu] = useState<{ name: MenuName; active: number; focus: boolean } | null>(
    // defaultMenu opens with its picked option lit (PICKED, read when drawn).
    () => (defaultMenu ? { name: defaultMenu, active: PICKED, focus: false } : null),
  );
  const [width, setWidth] = useState(560);
  const [message, setMessage] = useState('');
  const [pressed, setPressed] = useState<string | null>(null);
  const chart = useRef<HTMLDivElement>(null);
  const pointer = useRef<HTMLSpanElement>(null);
  const segmented = useRef<HTMLDivElement>(null);
  const titleId = useId();
  /** The bar lit now was lit by the demo. */
  const demoHover = useRef(false);
  /** The menu open now was opened by the demo. */
  const demoMenu = useRef(false);

  const range = RANGES[granularity].find((r) => r.id === rangeId) ?? RANGES[granularity][0];

  // The chart is drawn at its box's width.
  useEffect(() => {
    const element = chart.current;
    if (!element) return;
    const observer = new ResizeObserver(([entry]) => {
      setWidth(Math.max(200, Math.round(entry.contentRect.width)));
    });
    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  const view = useMemo(() => {
    const list = buckets(granularity, range.count);
    const picked = source === 'all' ? SOURCES.map((_, s) => s) : [SOURCES.indexOf(source)];
    const bars: Bar[] = list.map((bucket) => {
      const value = (s: number) =>
        verdict === 'all'
          ? bucket.alerts[s]
          : verdict === 'auto'
            ? bucket.alerts[s] - bucket.escalated[s]
            : bucket.escalated[s];
      const bySource: Segment[] = picked.map((s) => ({
        key: KEYS[s],
        label: SOURCES[s],
        color: color(s),
        value: value(s),
      }));
      const total = bySource.reduce((sum, segment) => sum + segment.value, 0);
      return {
        start: bucket.start,
        end: bucket.end,
        segments:
          groupBy === 'source'
            ? bySource
            : [{ key: 'alerts', label: 'Alerts', color: 'var(--alerts)', value: total }],
        total,
        escalated: verdict === 'all' ? picked.reduce((sum, s) => sum + bucket.escalated[s], 0) : 0,
      };
    });
    const all = list.reduce((sum, b) => sum + picked.reduce((n, s) => n + b.alerts[s], 0), 0);
    const escalated = list.reduce(
      (sum, b) => sum + picked.reduce((n, s) => n + b.escalated[s], 0),
      0,
    );
    const auto = all - escalated;
    const shown = bars.reduce((sum, bar) => sum + bar.total, 0);
    const days = granularity === 'day' ? range.count : range.count * 7;
    return {
      bars,
      shown,
      all,
      auto,
      escalated,
      closed: all ? (auto / all) * 100 : 0,
      median:
        source === 'all'
          ? MEDIAN_TRIAGE_ALL + (days > 60 ? 1 : 0)
          : MEDIAN_TRIAGE[SOURCES.indexOf(source)],
      saved: (auto * ANALYST_SECONDS) / 3600,
      period: granularity === 'day' ? `${range.count} days` : `${range.count} weeks`,
      dates: span(list),
    };
  }, [granularity, range.count, source, groupBy, verdict]);

  // Geometry: the plot's width, each bar's slot, and the scales on either side of the baseline.
  const plot = width - AXIS;
  const count = view.bars.length;
  const slot = (plot - LEAD - TAIL) / count;
  const barWidth = Math.min(24, slot * 0.65);
  /** The space after each bar: BAR_GAP, or less on a narrow chart where the bars' share is smaller. */
  const gap = Math.min(BAR_GAP, slot - barWidth);
  const maxTotal = Math.max(1, ...view.bars.map((bar) => bar.total));
  const maxEscalated = Math.max(0, ...view.bars.map((bar) => bar.escalated));
  const posScale = POS_MAX / maxTotal;
  const negScale = maxEscalated ? NEG_MAX / maxEscalated : 0;
  const step = niceStep(maxTotal);
  const ticks: number[] = [];
  for (let value = step; value <= maxTotal * 1.02; value += step) ticks.push(value);
  const negTick = roundBelow(maxEscalated);
  /** A bar's left edge: its slot, less the space to the next bar, holds it in the middle. */
  const barX = (index: number) => LEAD + index * slot + (slot - gap - barWidth) / 2;

  // The x-axis: a label every week (or every few), farther apart on a narrow chart.
  let every =
    granularity === 'day' ? (count <= 14 ? 3 : count <= 31 ? 7 : 14) : count <= 12 ? 3 : 6;
  while (every * slot < 58) every *= 2;
  const xLabels: { index: number; text: string }[] = [];
  for (let index = 0; index < count; index += every)
    xLabels.push({ index, text: shortDate(view.bars[index].start) });

  const hovered = active !== null && active < count ? view.bars[active] : null;

  /** What the note on a bar says, for screen readers too. */
  const rows = (bar: Bar) => {
    const top = [...bar.segments]
      .filter((segment) => segment.value > 0)
      .sort((a, b) => b.value - a.value)
      .slice(0, 3);
    return top;
  };
  const dateOf = (bar: Bar) => (granularity === 'day' ? '' : 'Wk ') + shortDate(bar.start);
  const describe = (bar: Bar) => {
    const when = granularity === 'day' ? longDate(bar.start) : `Week of ${longDate(bar.start)}`;
    const parts = rows(bar).map((segment) => `${segment.label} ${fmt(segment.value)}`);
    const escalated = verdict === 'all' ? `, ${fmt(bar.escalated)} escalated` : '';
    return `${when}: ${fmt(bar.total)} alerts. ${parts.join(', ')}${escalated}.`;
  };

  /** Repeats are made to differ, so screen readers announce them again. */
  const announce = (text: string) => setMessage((now) => (now === text ? `${text} ` : text));

  // ——— The reader's pointer and keys on the chart ———
  const indexAt = (clientX: number) => {
    const element = chart.current;
    if (!element) return null;
    const rect = element.getBoundingClientRect();
    const scale = rect.width / element.offsetWidth || 1;
    const x = (clientX - rect.left) / scale;
    // Each bar answers for a slot's width around its own middle.
    const index = Math.round((x - LEAD - (slot - gap) / 2) / slot);
    return Math.min(count - 1, Math.max(0, index));
  };
  const onChartMove = (event: ReactPointerEvent) => {
    if (!event.isPrimary) return;
    demoHover.current = false;
    setActive(indexAt(event.clientX));
  };
  const onChartLeave = (event: ReactPointerEvent) => {
    if (event.pointerType === 'touch') return;
    if (!chart.current?.matches(':focus-visible')) setActive(null);
  };
  const onChartKey = (event: KeyboardEvent) => {
    const from = active ?? count;
    let to: number | null = null;
    if (event.key === 'ArrowLeft') to = Math.max(0, from - 1);
    else if (event.key === 'ArrowRight')
      to = active === null ? count - 1 : Math.min(count - 1, from + 1);
    else if (event.key === 'Home') to = 0;
    else if (event.key === 'End') to = count - 1;
    else if (event.key === 'Escape') {
      setActive(null);
      return;
    }
    if (to === null) return;
    event.preventDefault();
    demoHover.current = false;
    setActive(to);
    announce(describe(view.bars[to]));
  };

  // ——— Filters ———
  const sourceOptions: Option[] = [
    { value: 'all', label: 'All sources' },
    ...SOURCES.map((name, s) => ({ value: name, label: name, swatch: color(s) })),
  ];
  const menus: {
    name: MenuName;
    label: string;
    value: string;
    options: Option[];
    choose: (value: string) => void;
  }[] = [
    {
      name: 'range',
      label: 'Date range',
      value: range.id,
      options: RANGES[granularity].map((r) => {
        const list = buckets(granularity, r.count);
        return { value: r.id, label: r.label, hint: span(list) };
      }),
      choose: (value) => setRangeId(value),
    },
    {
      name: 'source',
      label: 'Source',
      value: source,
      options: sourceOptions,
      choose: (value) => setSource(value as SourceFilter),
    },
    {
      name: 'group',
      label: 'Group by',
      value: groupBy,
      options: [
        { value: 'source', label: 'Source' },
        { value: 'none', label: 'None' },
      ],
      choose: (value) => setGroupBy(value as Grouping),
    },
    {
      name: 'verdict',
      label: 'Verdict',
      value: verdict,
      options: [
        { value: 'all', label: 'All verdicts' },
        { value: 'auto', label: 'Auto-closed' },
        { value: 'escalated', label: 'Escalated' },
      ],
      choose: (value) => setVerdict(value as Verdict),
    },
  ];
  const closeMenu = useCallback(() => setMenu(null), []);

  const switchTo = (next: Granularity) => {
    if (next === granularity) return;
    setGranularity(next);
    setRangeId(RANGES[next][0].id);
    setActive(null);
  };
  const onSwitchKey = (event: KeyboardEvent) => {
    if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
    event.preventDefault();
    const next = granularity === 'day' ? 'week' : 'day';
    switchTo(next);
    segmented.current
      ?.querySelector<HTMLButtonElement>(`[data-value='${next}']`)
      ?.focus({ preventScroll: true });
  };

  const headline =
    verdict === 'escalated'
      ? 'alerts escalated'
      : verdict === 'auto'
        ? 'alerts closed without a human'
        : 'alerts triaged';

  // The view changed: a new key grows the bars again, and screen readers hear the new total.
  const viewKey = `${granularity}-${range.id}-${source}-${groupBy}-${verdict}`;
  const first = useRef(true);
  const announceView = useEffectEvent(() =>
    announce(`${fmt(view.shown)} ${headline} in ${view.period}, ${view.dates}.`),
  );
  useEffect(() => {
    if (first.current) {
      first.current = false;
      return;
    }
    announceView();
  }, [viewKey]);

  // ——— The demo ———
  const demo = autoplay && playing && defaultRange === undefined && !defaultMenu;
  const demoState = useEffectEvent(
    (
      action:
        | { type: 'hover'; index: number | null }
        | { type: 'granularity'; value: Granularity }
        | { type: 'open'; name: MenuName; active: number }
        | { type: 'light'; active: number }
        | { type: 'source'; value: SourceFilter }
        | { type: 'close' }
        | { type: 'press'; target: string | null },
    ) => {
      if (action.type === 'hover') {
        demoHover.current = action.index !== null;
        setActive(action.index);
      } else if (action.type === 'granularity') switchTo(action.value);
      else if (action.type === 'open') {
        demoMenu.current = true;
        setMenu({ name: action.name, active: action.active, focus: false });
      } else if (action.type === 'light')
        setMenu((now) => (now ? { ...now, active: action.active } : now));
      else if (action.type === 'source') setSource(action.value);
      else if (action.type === 'close') {
        demoMenu.current = false;
        setMenu(null);
      } else setPressed(action.target);
    },
  );
  const demoCount = useEffectEvent(() => count);

  useEffect(() => {
    const stage = root.current;
    const arrow = pointer.current;
    const plotBox = chart.current;
    if (!demo || !stage || !arrow || !plotBox) return;

    /** The middle of `element` (or a point in it, in shares of its box), in the stage's px. */
    const at = (element: Element | null | undefined, fx = 0.5, fy = 0.5): Point => {
      const frame = stage.getBoundingClientRect();
      const scale = frame.width / stage.offsetWidth || 1;
      const rect = element?.getBoundingClientRect() ?? frame;
      return {
        x: (rect.left + rect.width * fx - frame.left) / scale,
        y: (rect.top + rect.height * fy - frame.top) / scale,
      };
    };
    /** A bar's middle, a little above the baseline. */
    const barAt = (index: number): Point => {
      const box = at(plotBox, 0, 0);
      const n = demoCount();
      const s = (plotBox.offsetWidth - AXIS - LEAD - TAIL) / n;
      const g = Math.min(BAR_GAP, s - Math.min(24, s * 0.65));
      return { x: box.x + LEAD + index * s + (s - g) / 2, y: box.y + BASE - 60 };
    };
    const q = (selector: string) => stage.querySelector(selector);

    const stop = runDemo(stage, arrow, async ({ wait, glide, glyph }) => {
      const press = async (target: string, then: () => void) => {
        glyph('hand');
        demoState({ type: 'press', target });
        arrow.dataset.pressed = 'true';
        await wait(140);
        delete arrow.dataset.pressed;
        demoState({ type: 'press', target: null });
        then();
      };
      const pick = async (name: MenuName, index: number, after: () => void) => {
        await glide(at(q(`[data-filter='${name}'] [data-part='filter-value']`), 0.4), FLIGHT);
        await press(`menu-${name}`, () => demoState({ type: 'open', name, active: -1 }));
        await wait(380);
        glyph('arrow');
        const option = () => q(`[data-filter='${name}'] [role='option']:nth-child(${index + 1})`);
        await glide(at(option(), 0.3), FLIGHT);
        demoState({ type: 'light', active: index });
        await wait(320);
        await press(`option`, () => {
          after();
          demoState({ type: 'close' });
        });
        await wait(200);
        glyph('arrow');
      };

      await wait(500);
      for (;;) {
        // Hover the spike, then a few days either side of it.
        const n = demoCount();
        for (const index of [Math.min(n - 1, 20), Math.min(n - 1, 24), Math.max(0, 16)]) {
          await glide(barAt(index), FLIGHT);
          demoState({ type: 'hover', index });
          await wait(1100);
        }
        demoState({ type: 'hover', index: null });

        // Weeks.
        await glide(at(q("[data-value='week']")), FLIGHT);
        await press('week', () => demoState({ type: 'granularity', value: 'week' }));
        await wait(700);
        glyph('arrow');
        for (const index of [8, 10]) {
          await glide(barAt(index), FLIGHT);
          demoState({ type: 'hover', index });
          await wait(1100);
        }
        demoState({ type: 'hover', index: null });

        // Only Email, then all sources again.
        await pick('source', 4, () => demoState({ type: 'source', value: 'Email' }));
        await glide(barAt(7), FLIGHT);
        demoState({ type: 'hover', index: 7 });
        await wait(1300);
        demoState({ type: 'hover', index: null });
        await pick('source', 0, () => demoState({ type: 'source', value: 'all' }));

        // Back to days.
        await glide(at(q("[data-value='day']")), FLIGHT);
        await press('day', () => demoState({ type: 'granularity', value: 'day' }));
        await wait(900);
        glyph('arrow');
      }
    });
    return () => {
      stop();
      // The demo's hover goes with it; a hover of the reader's stays.
      if (demoHover.current) demoState({ type: 'hover', index: null });
      if (demoMenu.current) demoState({ type: 'close' });
      demoState({ type: 'press', target: null });
      delete arrow.dataset.pressed;
    };
  }, [demo, root]);

  // ——— Drawing ———
  const ring = hovered && active !== null ? ringOf(active) : null;
  function ringOf(index: number) {
    const bar = view.bars[index];
    const top = bar.total ? BASE - POS_GAP - bar.total * posScale : BASE;
    const bottom = bar.escalated ? BASE + NEG_GAP + bar.escalated * negScale : BASE;
    return {
      x: barX(index) - RING,
      y: top - RING_TOP,
      width: barWidth + RING * 2,
      height: bottom - top + RING_TOP + RING_BOTTOM,
    };
  }
  // The note sits beside the ring, on the roomier side; on a chart too narrow for that, it
  // floats over the top of the plot, clear of the ring.
  const NOTE = 156;
  const roomLeft = ring ? ring.x - 12 : 0;
  const roomRight = ring ? width - ring.x - ring.width - 12 : 0;
  const noteSide: 'left' | 'right' | 'over' =
    Math.max(roomLeft, roomRight) < NOTE ? 'over' : roomLeft > roomRight ? 'left' : 'right';
  const noteTop =
    noteSide === 'over' ? 0 : ring ? Math.min(Math.max(0, ring.y + RING_TOP), HEIGHT - 150) : 0;
  const noteStyle: CSSProperties = !ring
    ? {}
    : noteSide === 'left'
      ? { top: noteTop, right: width - ring.x + 12 }
      : noteSide === 'right'
        ? { top: noteTop, left: ring.x + ring.width + 12 }
        : roomLeft > roomRight
          ? { top: noteTop, left: 0 }
          : { top: noteTop, right: AXIS };

  const legend: { key: string; label: string; color: string }[] =
    groupBy === 'none'
      ? [{ key: 'alerts', label: 'Alerts', color: 'var(--alerts)' }]
      : (source === 'all' ? SOURCES.map((_, s) => s) : [SOURCES.indexOf(source)]).map((s) => ({
          key: KEYS[s],
          label: SOURCES[s],
          color: color(s),
        }));

  return (
    <div
      ref={root}
      className={`${styles.root} ${className}`}
      data-part="stage"
      data-color-scheme={colorScheme}
      onPointerDownCapture={take}
      onKeyDownCapture={take}
      onPointerMove={(event) => {
        if (event.pointerType !== 'touch') take();
      }}
    >
      <section className={styles.card} data-part="card" aria-labelledby={titleId}>
        <header className={styles.header} data-part="header">
          <span className={styles.icon} data-part="icon" aria-hidden="true">
            <svg viewBox="0 0 16 16" focusable="false">
              <path d="M4 8.5v4M6.67 4.5v8M9.33 6.5v6M12 9.5v3" />
            </svg>
          </span>
          <div className={styles.titles}>
            <h3 className={styles.title} id={titleId} data-part="title">
              Alert volume
            </h3>
            <p className={styles.subtitle} data-part="subtitle">
              Triaged by {product}
            </p>
          </div>
          <div
            ref={segmented}
            className={styles.switch}
            data-part="switch"
            role="radiogroup"
            aria-label="Bars per"
            onKeyDown={onSwitchKey}
          >
            {(['day', 'week'] as const).map((value) => (
              <button
                key={value}
                type="button"
                role="radio"
                className={styles.switchOption}
                data-part="switch-option"
                data-value={value}
                data-pressed={pressed === value || undefined}
                aria-checked={granularity === value}
                tabIndex={granularity === value ? 0 : -1}
                onClick={() => switchTo(value)}
              >
                {value === 'day' ? 'Day' : 'Week'}
              </button>
            ))}
          </div>
        </header>

        <div className={styles.filters} data-part="filters">
          {menus.map((item) => (
            <FilterMenu
              key={item.name}
              name={item.name}
              label={item.label}
              value={item.value}
              options={item.options}
              open={menu?.name === item.name}
              active={
                menu?.name !== item.name
                  ? -1
                  : menu.active === PICKED
                    ? item.options.findIndex((option) => option.value === item.value)
                    : menu.active
              }
              focusOnOpen={menu?.name === item.name && menu.focus}
              onOpen={(index) => {
                demoMenu.current = false;
                setMenu({ name: item.name, active: index, focus: true });
              }}
              onClose={closeMenu}
              onActive={(index) => setMenu((now) => (now ? { ...now, active: index } : now))}
              onChoose={(value) => {
                item.choose(value);
                setActive(null);
              }}
            />
          ))}
        </div>

        <div className={styles.body} data-part="body">
          <div className={styles.summary} data-part="summary">
            <p className={styles.total}>
              <span className={styles.totalValue} data-part="total">
                {fmt(view.shown)}
              </span>{' '}
              <span className={styles.totalLabel}>
                {headline} in {view.period}
              </span>
            </p>
            <p className={styles.closed} data-part="closed-share">
              <span className={styles.closedValue}>{view.closed.toFixed(1)}%</span>{' '}
              <span className={styles.closedLabel}>closed without a human</span>
            </p>
          </div>

          <ul className={styles.legend} data-part="legend" aria-label="Legend">
            {legend.map((item) => (
              <li key={item.key} className={styles.legendItem}>
                <span
                  className={styles.swatch}
                  style={{
                    background: `color-mix(in srgb, ${item.color} 82%, var(--swatch-tint))`,
                  }}
                  aria-hidden="true"
                />
                {item.label}
              </li>
            ))}
            {verdict === 'all' && (
              <li className={`${styles.legendItem} ${styles.legendEscalated}`}>
                <span
                  className={styles.swatch}
                  style={{
                    background: 'color-mix(in srgb, var(--escalated) 82%, var(--swatch-tint))',
                  }}
                  aria-hidden="true"
                />
                Escalated
              </li>
            )}
          </ul>

          <div
            ref={chart}
            className={styles.chart}
            data-part="chart"
            role="group"
            aria-roledescription="chart"
            aria-label={`${fmt(view.shown)} ${headline} in ${view.period}, ${view.dates}. Use the arrow keys to read each ${granularity}.`}
            tabIndex={0}
            onPointerMove={onChartMove}
            onPointerDown={onChartMove}
            onPointerLeave={onChartLeave}
            onKeyDown={onChartKey}
            onBlur={() => setActive(null)}
          >
            <svg
              className={styles.plot}
              width={width}
              height={HEIGHT}
              viewBox={`0 0 ${width} ${HEIGHT}`}
              aria-hidden="true"
              focusable="false"
            >
              {/* Grid lines and the y-axis. */}
              {ticks.map((value) => {
                const y = BASE - POS_GAP - value * posScale;
                return (
                  <g key={value}>
                    <line className={styles.grid} x1={0} x2={plot} y1={y} y2={y} />
                    <text className={styles.axisText} x={plot + AXIS_INSET} y={y}>
                      {tick(value)}
                    </text>
                  </g>
                );
              })}
              <line className={styles.baseline} x1={0} x2={plot} y1={BASE} y2={BASE} />
              <text className={styles.axisText} x={plot + AXIS_INSET} y={BASE - POS_GAP}>
                0
              </text>
              {negTick > 0 && (
                <text
                  className={styles.axisText}
                  x={plot + AXIS_INSET}
                  y={BASE + NEG_GAP + negTick * negScale}
                >
                  {negTick}
                </text>
              )}

              {/* The bars: a new view grows them anew. */}
              <g key={viewKey} className={styles.bars} data-part="bars">
                {view.bars.map((bar, index) => {
                  const x = barX(index);
                  let cursor = BASE - POS_GAP;
                  return (
                    <g
                      key={bar.start}
                      className={styles.bar}
                      data-active={index === active || undefined}
                      style={{ '--i': index } as CSSProperties}
                    >
                      <g className={styles.up}>
                        {bar.segments.map((segment) => {
                          const h = segment.value * posScale;
                          if (h <= 0) return null;
                          const y = cursor - h;
                          cursor = y;
                          const height = Math.max(0.75, h - SEG_GAP);
                          return (
                            <rect
                              key={segment.key}
                              x={x}
                              y={y + (h - height)}
                              width={barWidth}
                              height={height}
                              rx={Math.min(2, height / 2)}
                              style={{ fill: segment.color }}
                            />
                          );
                        })}
                      </g>
                      {bar.escalated > 0 && (
                        <g className={styles.down}>
                          <rect
                            className={styles.escalated}
                            x={x}
                            y={BASE + NEG_GAP}
                            width={barWidth}
                            height={bar.escalated * negScale}
                            rx={Math.min(2, (bar.escalated * negScale) / 2)}
                          />
                        </g>
                      )}
                    </g>
                  );
                })}
              </g>

              {ring && (
                <rect
                  className={styles.ring}
                  data-part="ring"
                  x={ring.x}
                  y={ring.y}
                  width={ring.width}
                  height={ring.height}
                  rx={4}
                />
              )}

              {xLabels.map(({ index, text }) => {
                const middle = barX(index) + barWidth / 2;
                const half = (text.length * 7.8) / 2; // 13 px mono: 0.6 em a character
                const x = Math.min(plot - half, Math.max(half - 1.5, middle));
                return (
                  <text
                    key={index}
                    className={`${styles.axisText} ${styles.xText}`}
                    x={x}
                    y={X_LABELS}
                  >
                    {text}
                  </text>
                );
              })}
            </svg>

            {hovered && ring && (
              <div
                className={styles.note}
                data-part="tooltip"
                data-side={noteSide}
                aria-hidden="true"
                style={noteStyle}
              >
                <div className={styles.noteHead}>
                  <span className={styles.noteDate}>{dateOf(hovered)}</span>
                  <span className={styles.noteNumber}>{fmt(hovered.total)}</span>
                </div>
                {rows(hovered).map((segment) => (
                  <div key={segment.key} className={styles.noteRow}>
                    <span
                      className={styles.swatch}
                      style={{
                        background: `color-mix(in srgb, ${segment.color} 82%, var(--swatch-tint))`,
                      }}
                      aria-hidden="true"
                    />
                    <span className={styles.noteLabel}>{segment.label}</span>
                    <span className={styles.noteNumber}>{fmt(segment.value)}</span>
                  </div>
                ))}
                {verdict === 'all' && (
                  <div className={styles.noteRow}>
                    <span
                      className={styles.swatch}
                      style={{
                        background: 'color-mix(in srgb, var(--escalated) 82%, var(--swatch-tint))',
                      }}
                      aria-hidden="true"
                    />
                    <span className={styles.noteLabel}>Escalated</span>
                    <span className={styles.noteNumber}>{fmt(hovered.escalated)}</span>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>

        <dl className={styles.stats} data-part="stats">
          <div className={styles.stat}>
            <dt>Auto-closed</dt>
            <dd>{fmt(view.auto)}</dd>
          </div>
          <div className={styles.stat}>
            <dt>Escalated</dt>
            <dd className={styles.statAlert}>{fmt(view.escalated)}</dd>
          </div>
          <div className={styles.stat}>
            <dt>Median triage</dt>
            <dd>{view.median} s</dd>
          </div>
          <div className={styles.stat}>
            <dt>Analyst time saved</dt>
            <dd>{fmt(view.saved)} h</dd>
          </div>
        </dl>
      </section>

      <div className={styles.pointerLayer} aria-hidden="true">
        <DemoPointer
          ref={pointer}
          className={styles.pointer}
          arrowClass={styles.arrow}
          handClass={styles.hand}
        />
      </div>
      <p className={styles.hidden} role="status">
        {message}
      </p>
    </div>
  );
}
```

### alert-volume/alert-volume.module.css

```css
/* Alert volume (alert-volume.tsx): a near-black card on a dim stage, or by day a white one on a
   pale grey stage. The card is 600 px at most and lays itself out from its own width (a
   container query): filters and figures go two by two on a phone.

   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes
   the color-scheme it inherits from the page (light unless the page says otherwise), or the one
   its colorScheme prop sets (data-color-scheme).

   Memory. The bars grow by transforms on their own groups, the demo pointer moves by transforms,
   and the note over the chart is the only thing that blurs what is behind it. */

.root {
  /* The stage around the card, its frame, and the card. */
  --stage: #ececec;
  --stage-2: #f6f6f6;
  --frame: #e2e2e2;
  --card: #fff;
  /* Lines between the card's bands, and the boxes in it (icon, switch, menus). */
  --line: #ececec;
  --box: #f6f6f6;
  --box-line: #e6e6e6;
  --box-on: #fff;
  --box-on-line: #e0e0e0;
  /* Words: the strongest, the softer, the muted. */
  --ink: #141414;
  --ink-2: #4a4a4a;
  --muted: #8a8a8a;
  --axis: #9a9a9a;
  /* The chart's lines, and the ring around a hovered bar. */
  --grid: #dcdcdc;
  --base: #cfcfcf;
  --ring: #1c1c1c;
  /* The note over a bar. */
  --note: rgb(255 255 255 / 0.88);
  --note-line: #e3e3e3;
  --note-shadow: rgb(0 0 0 / 0.08);
  --connector: #b5b5b5;
  /* The sources, single-colour bars, and the escalated. */
  --identity: #7d55f6;
  --endpoint: #3f7cf4;
  --cloud: #62a8ef;
  --email: #f3b937;
  --network: #2fd08a;
  --saas: #ec4fae;
  --alerts: #9a9aa2;
  --escalated: #e5486a;
  --escalated-ink: #d93a5c;
  /* The swatches of the legend and the note are a touch paler than the bars. */
  --swatch-tint: #fff;
  /* The same on both schemes: the focus ring, and the demo's pointer. */
  --focus: #387bff;
  --pointer: #1c1c1e;
  --pointer-edge: #fff;
  --pointer-shadow: rgb(0 0 0 / 0.25);
  --sans: Satoshi, Inter, ui-sans-serif, system-ui, sans-serif;
  --mono: JetBrains, 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  min-width: 0;
  padding: clamp(14px, 6vw, 56px) clamp(10px, 4vw, 48px);
  box-sizing: border-box;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  background: linear-gradient(180deg, var(--stage) 0%, var(--stage-2) 100%);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --stage: light-dark(#ececec, #1f1d1d);
    --stage-2: light-dark(#f6f6f6, #2d2b2b);
    --frame: light-dark(#e2e2e2, #1c1a1a);
    --card: light-dark(#fff, #0b0b0b);
    --line: light-dark(#ececec, #1a1a1a);
    --box: light-dark(#f6f6f6, #141414);
    --box-line: light-dark(#e6e6e6, #232323);
    --box-on: light-dark(#fff, #2a2a2a);
    --box-on-line: light-dark(#e0e0e0, #2a2a2a);
    --ink: light-dark(#141414, #f2f2f2);
    --ink-2: light-dark(#4a4a4a, #a8a8a8);
    --muted: light-dark(#8a8a8a, #6e6e6e);
    --axis: light-dark(#9a9a9a, #6a6a6a);
    --grid: light-dark(#dcdcdc, #2a2a2a);
    --base: light-dark(#cfcfcf, #2f2f2f);
    --ring: light-dark(#1c1c1c, #c9c9c9);
    --note: light-dark(rgb(255 255 255 / 0.84), rgb(29 29 29 / 0.82));
    --note-line: light-dark(#e3e3e3, #2e2e2e);
    --note-shadow: light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.45));
    --connector: light-dark(#b5b5b5, #555);
    --identity: light-dark(#7d55f6, #855dfe);
    --endpoint: light-dark(#3f7cf4, #5a8cff);
    --cloud: light-dark(#62a8ef, #84bdf6);
    --email: light-dark(#f3b937, #ffd56b);
    --network: light-dark(#2fd08a, #76f5b6);
    --saas: light-dark(#ec4fae, #f866bd);
    --alerts: light-dark(#9a9aa2, #8d8d96);
    --escalated: light-dark(#e5486a, #ec617d);
    --escalated-ink: light-dark(#d93a5c, #f0607a);
    --swatch-tint: light-dark(#fff, #b4b4b4);
  }
}
/* The colorScheme prop. Without it, nothing is set: color-scheme is inherited from the page. */
.root[data-color-scheme='light'] {
  color-scheme: light;
}
.root[data-color-scheme='dark'] {
  color-scheme: dark;
}
.root[data-color-scheme='system'] {
  color-scheme: light dark;
}

.title,
.subtitle,
.total,
.closed,
.stat dd {
  margin: 0;
}

/* ——— The card ——— */
.card {
  container: card / inline-size;
  position: relative;
  width: min(100%, 600px);
  min-width: 0;
  border-radius: 14px;
  background: var(--card);
  box-shadow: 0 0 0 4px var(--frame);
  font-size: 14px;
  line-height: 1.3;
}

.header {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 68px;
  padding: 0 20px;
  border-bottom: 1px solid var(--line);
}
.icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 28px;
  height: 28px;
  box-sizing: border-box;
  border: 1px solid var(--box-line);
  border-radius: 7px;
  background: var(--box);
}
.icon svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-linecap: round;
}
.titles {
  min-width: 0;
  flex: 1;
  /* The two lines sit a touch below the icon's middle. */
  padding-top: 4px;
}
.title {
  font-size: 14.25px;
  font-weight: 500;
  line-height: 18px;
  letter-spacing: -0.005em;
}
.subtitle {
  overflow: hidden;
  color: var(--muted);
  font-size: 12.25px;
  font-weight: 500;
  line-height: 18px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The Day / Week switch. */
.switch {
  display: flex;
  flex: none;
  gap: 2px;
  padding: 4px;
  border-radius: 9px;
  background: var(--box);
  box-shadow: inset 0 0 0 1px var(--line);
}
.switchOption {
  height: 24px;
  padding: 0 13.5px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  line-height: 24px;
  cursor: pointer;
  transition:
    background-color 0.18s ease,
    color 0.18s ease,
    box-shadow 0.18s ease,
    transform 0.12s ease;
}
.switchOption:first-child {
  min-width: 44px;
  padding: 0 11px;
}
.switchOption:hover {
  color: var(--ink);
}
.switchOption[aria-checked='true'] {
  background: var(--box-on);
  box-shadow: inset 0 0 0 1px var(--box-on-line);
  color: var(--ink);
}
.switchOption[data-pressed] {
  transform: scale(0.95);
}
.switchOption:focus-visible,
.filterValue:focus-visible,
.chart:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ——— Filters ——— */
.filters {
  display: flex;
  align-items: center;
  height: 72px;
  padding: 0 20px;
  border-bottom: 1px solid var(--line);
}
.filter {
  position: relative;
  display: grid;
  gap: 4px;
  padding: 0 12.5px 0 15px;
  border-left: 1px solid var(--line);
}
.filter:first-child {
  padding-left: 0;
  border-left: 0;
}
.filterLabel {
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 16px;
}
.filterValue {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  justify-self: start;
  margin: 0 -4px;
  padding: 0 4px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 14.5px;
  font-weight: 500;
  line-height: 20px;
  white-space: nowrap;
  cursor: pointer;
}
.chevron {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: var(--ink-2);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.2s ease;
}
.filterValue[aria-expanded='true'] .chevron {
  transform: rotate(180deg);
}

/* A filter's menu. */
.menu {
  margin: 0;
  position: absolute;
  z-index: 3;
  top: calc(100% + 8px);
  left: -6px;
  min-width: 176px;
  padding: 4px;
  list-style: none;
  border: 1px solid var(--note-line);
  border-radius: 10px;
  background: var(--card);
  box-shadow:
    0 12px 32px var(--note-shadow),
    0 2px 6px var(--note-shadow);
  outline: none;
  animation: menuIn 0.16s cubic-bezier(0.2, 0.8, 0.3, 1);
  transform-origin: top left;
}
.filter:first-child .menu {
  left: -6px;
}
.filter:last-child .menu,
.filter:nth-child(3) .menu {
  min-width: 156px;
}
@keyframes menuIn {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}
.option {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 8px 0 10px;
  border-radius: 6px;
  color: var(--ink-2);
  font-size: 14px;
  white-space: nowrap;
  cursor: pointer;
}
.option[data-active='true'] {
  background: var(--box);
  color: var(--ink);
}
.option[aria-selected='true'] {
  color: var(--ink);
}
.optionLabel {
  flex: 1;
}
.optionHint {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 12px;
}
.check {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  visibility: hidden;
}
.option[aria-selected='true'] .check {
  visibility: visible;
}

/* ——— The chart's band ——— */
.body {
  padding: 18px 20px 8px;
  border-bottom: 1px solid var(--line);
}
.summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
}
.total,
.closed {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.totalValue {
  font-family: var(--mono);
  font-size: 22px;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 28px;
  font-variant-numeric: tabular-nums;
}
.totalLabel {
  color: var(--ink-2);
  font-size: 12.5px;
  font-weight: 500;
}
.closed {
  gap: 8px;
}
.closedValue {
  font-family: var(--mono);
  font-size: 13.5px;
  font-weight: 500;
}
.closedLabel {
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 500;
}

.legend {
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: 8.5px;
  list-style: none;
}
.legendItem {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-2);
  font-size: 12.75px;
  font-weight: 500;
  line-height: 18px;
}
.legendEscalated {
  margin-left: auto;
}
.swatch {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 2px;
}

.chart {
  position: relative;
  margin-top: 10px;
  border-radius: 4px;
  touch-action: pan-y;
  cursor: default;
}
.plot {
  display: block;
  width: 100%;
  height: 262px;
  overflow: visible;
}
.grid {
  stroke: var(--grid);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}
.baseline {
  stroke: var(--base);
  stroke-width: 1;
}
.axisText {
  fill: var(--axis);
  font-family: var(--mono);
  font-size: 12.5px;
  dominant-baseline: central;
}
.xText {
  font-size: 13px;
  text-anchor: middle;
}
.ring {
  fill: none;
  stroke: var(--ring);
  stroke-width: 1.25;
}
.escalated {
  fill: var(--escalated);
  opacity: 0.87;
}
.bar[data-active] .escalated {
  opacity: 1;
}
.up,
.down {
  transform-box: fill-box;
  animation: grow 0.46s cubic-bezier(0.2, 0.8, 0.25, 1) both;
  animation-delay: calc(min(var(--i), 40) * 9ms);
}
.up {
  transform-origin: 50% 100%;
}
.down {
  transform-origin: 50% 0%;
  animation-delay: calc(min(var(--i), 40) * 9ms + 80ms);
}
@keyframes grow {
  from {
    transform: scaleY(0);
  }
}

/* The note on a hovered bar, with a dotted line to its ring. */
.note {
  position: absolute;
  z-index: 2;
  min-width: 110px;
  padding: 4.5px 11px;
  box-sizing: border-box;
  border: 1px solid var(--note-line);
  border-radius: 6px;
  background: var(--note);
  box-shadow: 0 8px 24px var(--note-shadow);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  pointer-events: none;
  white-space: nowrap;
}
.note::after {
  position: absolute;
  top: 16px;
  width: 12px;
  border-top: 1px dotted var(--connector);
  content: '';
}
.note[data-side='left']::after {
  left: 100%;
}
.note[data-side='right']::after {
  right: 100%;
}
.noteHead,
.noteRow {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 20px;
}
.noteHead {
  justify-content: space-between;
  gap: 24px;
}
.noteDate {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 13px;
}
.noteLabel {
  flex: 1;
  margin-right: 16px;
  color: var(--ink-2);
  font-size: 12.75px;
  font-weight: 500;
}
.noteNumber {
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  text-align: right;
}

/* ——— The four figures ——— */
.stats {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  padding: 14.5px 20px 16.5px 4px;
}
.stat {
  display: grid;
  gap: 7px;
  min-width: 0;
  padding-left: 16px;
  border-left: 1px solid var(--line);
}
.stat:first-child {
  border-left: 0;
}
.stat dt {
  overflow: hidden;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 14px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stat dd {
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 500;
  line-height: 19px;
}
.stat .statAlert {
  color: var(--escalated-ink);
}

/* ——— Narrow cards ——— */
@container card (max-width: 520px) {
  .filters {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px 0;
    height: auto;
    padding: 14px 16px;
  }
  .filter,
  .filter:first-child {
    padding: 0 12px;
    border-left: 1px solid var(--line);
  }
  .filter:nth-child(odd) {
    padding-left: 0;
    border-left: 0;
  }
  .filter:nth-child(even) .menu {
    right: -6px;
    left: auto;
    transform-origin: top right;
  }
  .header,
  .body {
    padding-right: 16px;
    padding-left: 16px;
  }
  .stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 0;
    padding: 14px 16px;
  }
  .stat:nth-child(odd) {
    padding-left: 0;
    border-left: 0;
  }
  .legend {
    gap: 6px 14px;
  }
  .legendEscalated {
    margin-left: 0;
  }
  .subtitle,
  .stat dt {
    white-space: normal;
  }
}
@container card (max-width: 360px) {
  .switchOption {
    padding: 0 9px;
  }
  .subtitle {
    font-size: 12px;
  }
}

/* The demo pointer, over everything; one of its glyphs shows at a time. */
.pointerLayer {
  position: absolute;
  z-index: 4;
  inset: 0;
  pointer-events: none;
}
.pointer {
  width: 22px;
  height: 22px;
}
.arrow,
.hand {
  transition: transform 0.12s ease;
}
.pointer[data-pressed] .arrow,
.pointer[data-pressed] .hand {
  transform: scale(0.86);
}
.hand {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .arrow {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .hand {
  visibility: visible;
}

.hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .up,
  .down,
  .menu {
    animation: none;
  }
  .switchOption,
  .chevron,
  .arrow,
  .hand {
    transition: none;
  }
}
```

### alert-volume/data.ts

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

/*
 * The alerts behind the card: 26 weeks of invented days, Monday 13 April to Sunday 11 October,
 * each with its alerts by source and how many of them were escalated to a person. The last 30
 * days follow a fixed outline (quiet weekends, a spike of phishing on 2 October); the weeks
 * before them come from a seeded generator, so every load draws the same chart.
 */

export const SOURCES = ['Identity', 'Endpoint', 'Cloud', 'Email', 'Network', 'SaaS'] as const;
export type Source = (typeof SOURCES)[number];

export type Day = {
  /** Midnight UTC of the day, in ms. */
  time: number;
  /** Alerts per source, in the order of SOURCES; escalated ones included. */
  alerts: number[];
  /** Of those, how many went to a person, per source. */
  escalated: number[];
};

const DAY = 86_400_000;
/** The last day drawn: Sunday 11 October 2026. */
const LAST = Date.UTC(2026, 9, 11);
const COUNT = 182;

/** The share of each source on an ordinary day. */
const MIX = [0.31, 0.22, 0.14, 0.14, 0.11, 0.08];

/** The last 30 days, Saturday 12 September to Sunday 11 October: alerts and escalations a day. */
const OUTLINE = [
  1016, 976, 1688, 1712, 1696, 1632, 1720, 1008, 1064, 1696, 1784, 1800, 1784, 1780, 1064, 1064,
  1750, 1760, 1720, 1696, 2682, 1520, 1040, 1776, 1800, 1792, 1824, 1792, 1176, 1160,
];
const OUTLINE_ESCALATED = [
  5, 7.5, 6, 7.5, 6.5, 8, 8, 7.5, 6, 9, 9, 10.5, 6.5, 6, 5, 6, 10.5, 10.5, 8, 7.5, 22, 9, 5, 6, 6,
  7.5, 10.5, 8, 6, 6.5,
];
/** The 30 days add up to these. */
const OUTLINE_TOTAL = 43_969;
const OUTLINE_ESCALATED_TOTAL = 250;
/** 2 October, the phishing spike, exactly. */
const SPIKE = 20;
const SPIKE_ALERTS = [820, 409, 280, 900, 160, 113];
const SPIKE_ESCALATED = [8, 3, 1, 9, 1, 0];

/** A small seeded generator (mulberry32). */
function seeded(seed: number) {
  let state = seed >>> 0;
  return () => {
    state = (state + 0x6d2b79f5) >>> 0;
    let t = state;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** Whole numbers in proportion to `weights`, adding up to exactly `total`. */
function apportion(total: number, weights: number[]): number[] {
  const sum = weights.reduce((a, b) => a + b, 0) || 1;
  const exact = weights.map((weight) => (weight / sum) * total);
  const whole = exact.map(Math.floor);
  let left = total - whole.reduce((a, b) => a + b, 0);
  const order = exact
    .map((value, index) => ({ index, rest: value - Math.floor(value) }))
    .sort((a, b) => b.rest - a.rest);
  for (let i = 0; left > 0; i = (i + 1) % order.length, left -= 1) whole[order[i].index] += 1;
  return whole;
}

function build(): Day[] {
  const random = seeded(2041);
  const outline = [...OUTLINE];
  const escalations = [...OUTLINE_ESCALATED];
  outline.splice(SPIKE, 1);
  escalations.splice(SPIKE, 1);
  const totals = apportion(OUTLINE_TOTAL - 2682, outline);
  const escalated = apportion(OUTLINE_ESCALATED_TOTAL - 22, escalations);
  totals.splice(SPIKE, 0, 2682);
  escalated.splice(SPIKE, 0, 22);

  const days: Day[] = [];
  for (let i = 0; i < COUNT; i += 1) {
    const time = LAST - (COUNT - 1 - i) * DAY;
    const recent = i - (COUNT - 30);
    const weekday = new Date(time).getUTCDay();
    const weekend = weekday === 0 || weekday === 6;
    // A jittered mix, so no two bars stack alike.
    const mix = MIX.map((share) => share * (0.82 + random() * 0.36));
    let total: number;
    let escalations: number;
    if (recent >= 0) {
      total = totals[recent];
      escalations = escalated[recent];
    } else {
      // Older days grow slowly towards today's volume, as sources were connected.
      const growth = 0.78 + 0.22 * (i / (COUNT - 30));
      total = Math.round((weekend ? 1040 : 1720) * growth * (0.9 + random() * 0.2));
      escalations = Math.max(2, Math.round(total * (0.0045 + random() * 0.0025)));
    }
    if (recent === SPIKE) {
      days.push({ time, alerts: [...SPIKE_ALERTS], escalated: [...SPIKE_ESCALATED] });
      continue;
    }
    const alerts = apportion(total, mix);
    days.push({
      time,
      alerts,
      escalated: apportion(
        escalations,
        mix.map((m, s) => m * alerts[s]),
      ),
    });
  }
  return days;
}

export const DAYS: Day[] = build();

export type Granularity = 'day' | 'week';

/** The date ranges each view offers, the first of each being where the switch lands. */
export const RANGES: Record<Granularity, { id: string; label: string; count: number }[]> = {
  day: [
    { id: '30d', label: 'Last 30 days', count: 30 },
    { id: '14d', label: 'Last 14 days', count: 14 },
    { id: '60d', label: 'Last 60 days', count: 60 },
  ],
  week: [
    { id: '12w', label: 'Last 12 weeks', count: 12 },
    { id: '26w', label: 'Last 26 weeks', count: 26 },
  ],
};

/** One bar: a day or a week, with its alerts and escalations by source. */
export type Bucket = { start: number; end: number; alerts: number[]; escalated: number[] };

/** The last `count` days or Monday-to-Sunday weeks. */
export function buckets(granularity: Granularity, count: number): Bucket[] {
  const size = granularity === 'day' ? 1 : 7;
  const first = DAYS.length - count * size;
  const result: Bucket[] = [];
  for (let b = 0; b < count; b += 1) {
    const slice = DAYS.slice(first + b * size, first + (b + 1) * size);
    result.push({
      start: slice[0].time,
      end: slice[slice.length - 1].time,
      alerts: SOURCES.map((_, s) => slice.reduce((sum, day) => sum + day.alerts[s], 0)),
      escalated: SOURCES.map((_, s) => slice.reduce((sum, day) => sum + day.escalated[s], 0)),
    });
  }
  return result;
}

const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

/** "Oct 2". */
export function shortDate(time: number) {
  const date = new Date(time);
  return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
}

/** "October 2", for screen readers. */
export function longDate(time: number) {
  return new Date(time).toLocaleDateString('en-US', {
    month: 'long',
    day: 'numeric',
    timeZone: 'UTC',
  });
}

/** "Sep 12 - Oct 11". */
export function span(list: Bucket[]) {
  return `${shortDate(list[0].start)} - ${shortDate(list[list.length - 1].end)}`;
}

/** Median seconds to a verdict, per source, and for all of them. */
export const MEDIAN_TRIAGE = [34, 41, 45, 36, 39, 31];
export const MEDIAN_TRIAGE_ALL = 38;
/** What a person would have spent on each alert closed without them, in seconds. */
export const ANALYST_SECONDS = 97.2;
```

### alert-volume/filter-menu.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import { useEffect, useId, useRef, type KeyboardEvent } from 'react';
import styles from './alert-volume.module.css';

/*
 * One of the card's filters: a small label over its value, which opens a listbox under it. The
 * card owns whether it is open and which option is lit, so its demo can open a menu and move
 * through it like a reader would; the menu itself only reports what the reader does.
 */

export type Option = {
  value: string;
  label: string;
  /** Muted words at the end of the row (a date range's dates). */
  hint?: string;
  /** A colour swatch before the label (a source's colour). */
  swatch?: string;
};

export function FilterMenu({
  name,
  label,
  value,
  options,
  open,
  active,
  focusOnOpen,
  onOpen,
  onClose,
  onActive,
  onChoose,
}: {
  name: string;
  label: string;
  value: string;
  options: Option[];
  open: boolean;
  /** The lit option's index, while open. */
  active: number;
  /** The reader opened it: the listbox takes the focus. The demo's menus leave it alone. */
  focusOnOpen: boolean;
  onOpen: (active: number) => void;
  onClose: (refocus: boolean) => void;
  onActive: (index: number) => void;
  onChoose: (value: string) => void;
}) {
  const id = useId();
  const trigger = useRef<HTMLButtonElement>(null);
  const list = useRef<HTMLUListElement>(null);
  const wrap = useRef<HTMLDivElement>(null);
  const selected = Math.max(
    0,
    options.findIndex((option) => option.value === value),
  );
  const current = options[selected];

  useEffect(() => {
    if (open && focusOnOpen) list.current?.focus({ preventScroll: true });
  }, [open, focusOnOpen]);

  // A press anywhere else closes it.
  useEffect(() => {
    if (!open) return;
    const away = (event: PointerEvent) => {
      if (!wrap.current?.contains(event.target as Node)) onClose(false);
    };
    document.addEventListener('pointerdown', away);
    return () => document.removeEventListener('pointerdown', away);
  }, [open, onClose]);

  const choose = (index: number) => {
    onChoose(options[index].value);
    onClose(true);
    trigger.current?.focus({ preventScroll: true });
  };

  const onTriggerKey = (event: KeyboardEvent) => {
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
      event.preventDefault();
      onOpen(selected);
    }
  };

  const onListKey = (event: KeyboardEvent) => {
    const last = options.length - 1;
    const move = (to: number) => {
      event.preventDefault();
      onActive(Math.min(last, Math.max(0, to)));
    };
    switch (event.key) {
      case 'ArrowDown':
        return move(active + 1);
      case 'ArrowUp':
        return move(active - 1);
      case 'Home':
        return move(0);
      case 'End':
        return move(last);
      case 'Enter':
      case ' ':
        event.preventDefault();
        return choose(active);
      case 'Escape':
        event.preventDefault();
        onClose(true);
        trigger.current?.focus({ preventScroll: true });
        return;
      case 'Tab':
        onClose(false);
        return;
    }
  };

  return (
    <div ref={wrap} className={styles.filter} data-part="filter" data-filter={name}>
      <span className={styles.filterLabel} id={`${id}-label`}>
        {label}
      </span>
      <button
        ref={trigger}
        type="button"
        className={styles.filterValue}
        data-part="filter-value"
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-controls={open ? `${id}-list` : undefined}
        aria-labelledby={`${id}-label ${id}-value`}
        onClick={() => (open ? onClose(false) : onOpen(selected))}
        onKeyDown={onTriggerKey}
      >
        <span id={`${id}-value`}>
          {current.hint && name === 'range' ? current.hint : current.label}
        </span>
        <svg className={styles.chevron} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
          <path d="M4.5 6.25 8 9.75l3.5-3.5" />
        </svg>
      </button>
      {open && (
        <ul
          ref={list}
          id={`${id}-list`}
          className={styles.menu}
          data-part="menu"
          role="listbox"
          tabIndex={-1}
          aria-labelledby={`${id}-label`}
          aria-activedescendant={active >= 0 ? `${id}-option-${active}` : undefined}
          onKeyDown={onListKey}
        >
          {options.map((option, index) => (
            <li
              key={option.value}
              id={`${id}-option-${index}`}
              className={styles.option}
              data-part="menu-option"
              role="option"
              aria-selected={index === selected}
              data-active={index === active}
              onPointerMove={() => index !== active && onActive(index)}
              onClick={() => choose(index)}
            >
              {option.swatch && (
                <span
                  className={styles.swatch}
                  style={{ background: option.swatch }}
                  aria-hidden="true"
                />
              )}
              <span className={styles.optionLabel}>{option.label}</span>
              {option.hint && <span className={styles.optionHint}>{option.hint}</span>}
              <svg
                className={styles.check}
                viewBox="0 0 16 16"
                aria-hidden="true"
                focusable="false"
              >
                <path d="M3.5 8.5 6.5 11.5 12.5 4.5" />
              </svg>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

### alert-volume/demo-pointer.tsx

```tsx
'use client';

import type { CSSProperties, Ref } from 'react';

/*
 * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or
 * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends
 * the step in progress at once: its promise rejects, which ends the script right there, and the
 * pointer fades out.
 *
 * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame.
 * The points of the flight are handed to whoever wants to move along with it (the eye of the
 * badge, which plays its own animation on the same clock). The arrow and the hand are HTML boxes
 * that swap by their visibility: a change of style inside an SVG would lay the SVG out again.
 */

export type Point = { x: number; y: number };

/** A glide's path and timing, for an animation that plays along with it. */
export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };

export type Demo = {
  /** Resolves after `ms`. */
  wait: (ms: number) => Promise<void>;
  /** Glides the pointer to `to`, in the stage's own px, in `ms`; `along` gets the flight. */
  glide: (to: Point, ms: number, along?: (flight: Flight) => void) => Promise<void>;
  /** Shows the arrow, or the hand a link shows. */
  glyph: (name: 'arrow' | 'hand') => void;
};

/** What a stopped demo's pending step rejects with. */
class Stopped extends Error {}

/** Where the arrow's tip is, as shares of its box: the point the pointer stands on. */
const TIP: Point = { x: 0.15, y: 0.1 };

/** How many points a flight is drawn through. */
const SAMPLES = 24;

/** Plays `script` with the pointer over `stage`; returns the function that stops it. */
export function runDemo(
  stage: HTMLElement,
  pointer: HTMLElement,
  script: (demo: Demo) => Promise<void>,
): () => void {
  let alive = true;
  let flight: Animation | null = null;
  // The stylesheet sizes the pointer from the stage's width; its tip is where it stands.
  const tip = { x: pointer.offsetWidth * TIP.x, y: pointer.offsetHeight * TIP.y };
  const place = ({ x, y }: Point) => `translate(${x - tip.x}px, ${y - tip.y}px)`;
  const timers = new Set<number>();
  const pending = new Set<() => void>();
  // The pointer comes in from the lower right.
  let at: Point = { x: stage.offsetWidth * 0.8, y: stage.offsetHeight * 0.9 };

  /** Settles like `promise`, or rejects as soon as the demo stops. */
  const step = (promise: Promise<unknown>) =>
    new Promise<void>((resolve, reject) => {
      if (!alive) return reject(new Stopped());
      const cancel = () => reject(new Stopped());
      pending.add(cancel);
      promise.then(
        () => {
          pending.delete(cancel);
          if (alive) resolve();
        },
        () => {
          pending.delete(cancel);
          reject(new Stopped());
        },
      );
    });

  const wait = (ms: number) =>
    step(
      new Promise<void>((resolve) => {
        const timer = window.setTimeout(() => {
          timers.delete(timer);
          resolve();
        }, ms);
        timers.add(timer);
      }),
    );

  const glide: Demo['glide'] = (to, ms, along) => {
    const from = at;
    at = to;
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    // A gentle curve, as a hand moves: the control point sits off the straight line.
    const bend = { x: (from.x + to.x) / 2 - dy * 0.18, y: (from.y + to.y) / 2 + dx * 0.18 };
    const points: Point[] = [];
    for (let i = 0; i <= SAMPLES; i += 1) {
      const t = i / SAMPLES;
      const u = 1 - t;
      points.push({
        x: u * u * from.x + 2 * u * t * bend.x + t * t * to.x,
        y: u * u * from.y + 2 * u * t * bend.y + t * t * to.y,
      });
    }
    const timing: KeyframeAnimationOptions = {
      duration: ms,
      easing: 'cubic-bezier(0.45, 0, 0.25, 1)',
    };
    flight?.cancel();
    // Where it rests once the flight is over.
    pointer.style.transform = place(to);
    flight = pointer.animate(
      points.map((point) => ({ transform: place(point) })),
      timing,
    );
    along?.({ points, timing });
    return step(flight.finished);
  };

  const glyph: Demo['glyph'] = (name) => {
    pointer.dataset.glyph = name;
  };

  pointer.dataset.glyph = 'arrow';
  pointer.style.transform = place(at);
  pointer.style.opacity = '1';
  script({ wait, glide, glyph }).catch((error: unknown) => {
    if (!(error instanceof Stopped)) throw error;
  });

  return () => {
    if (!alive) return;
    alive = false;
    timers.forEach((timer) => window.clearTimeout(timer));
    flight?.cancel();
    pending.forEach((cancel) => cancel());
    pointer.style.opacity = '0';
  };
}

/**
 * The pointer's box: it moves, on a layer of its own. Its size comes from its class, its colours
 * from the stage's tokens (nda-badge.module.css).
 */
const POINTER: CSSProperties = {
  position: 'absolute',
  top: 0,
  left: 0,
  zIndex: 2,
  display: 'block',
  pointerEvents: 'none',
  opacity: 0,
  transition: 'opacity 0.3s ease',
  filter: 'drop-shadow(0 1px 1.5px var(--pointer-shadow))',
  willChange: 'transform',
};

/** One glyph over the other, each filling the box. */
const GLYPH: CSSProperties = { position: 'absolute', inset: 0, display: 'block' };
const DRAWING: CSSProperties = {
  display: 'block',
  width: '100%',
  height: '100%',
  overflow: 'visible',
};
/** The arrow is dark on a white edge, the hand white on a dark one, as the system draws them. */
const ARROW: CSSProperties = { fill: 'var(--pointer)', stroke: 'var(--pointer-edge)' };
const HAND: CSSProperties = { fill: 'var(--pointer-edge)', stroke: 'var(--pointer)' };

/**
 * The pointer the demo moves around: an arrow, or a pointing hand over the badge. It is
 * decorative: what it does shows on the badge itself. The stylesheet shows one glyph at a time,
 * from the box's data-glyph.
 */
export function DemoPointer({
  ref,
  className,
  arrowClass,
  handClass,
}: {
  ref: Ref<HTMLSpanElement>;
  className?: string;
  arrowClass?: string;
  handClass?: string;
}) {
  return (
    <span ref={ref} className={className} style={POINTER} aria-hidden="true">
      <span className={arrowClass} style={GLYPH}>
        <svg viewBox="0 0 20 20" style={DRAWING}>
          <path
            d="M3 2v14.5l3.6-3.3 2.6 5.6 2.4-1.1-2.6-5.5h5.2z"
            style={ARROW}
            strokeWidth="1.3"
            strokeLinejoin="round"
          />
        </svg>
      </span>
      <span className={handClass} style={GLYPH}>
        <svg viewBox="0 0 20 20" style={DRAWING}>
          {/* The tip of the finger sits where the arrow's tip is. */}
          <path
            d="M6.4 3.3c0-.8.6-1.4 1.3-1.4S9 2.5 9 3.3v5.5c.2-.5.6-.8 1.2-.8.7 0 1.2.5 1.2 1.2v.5c.2-.5.6-.8 1.2-.8.7 0 1.2.5 1.2 1.2v.7c.2-.4.6-.7 1.1-.7.7 0 1.2.5 1.2 1.2v3.5c0 2.8-1.9 4.9-4.6 4.9h-1.2c-1.8 0-3.1-.8-4.1-2.3l-2.6-3.9c-.4-.6-.2-1.3.4-1.7.6-.3 1.3-.2 1.7.4l.6.9z"
            style={HAND}
            strokeWidth="1.1"
            strokeLinejoin="round"
            transform="translate(-4.7 0.1)"
          />
        </svg>
      </span>
    </span>
  );
}
```

### alert-volume/use-autoplay.ts

```tsx
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';

/*
 * When a demo may play on its own: while part of it is on screen and the tab is visible, never
 * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it
 * takes back over `idle` ms after their last input.
 */
export function useAutoplay<T extends HTMLElement>(idle = 6000) {
  const ref = useRef<T>(null);
  const [visible, setVisible] = useState(false);
  const [reduced, setReduced] = useState(true);
  const [taken, setTaken] = useState(false);
  const timer = useRef(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    let onScreen = false;
    const update = () => setVisible(onScreen && !document.hidden);
    const observer = new IntersectionObserver(
      ([entry]) => {
        onScreen = entry.isIntersecting;
        update();
      },
      { threshold: 0.3 },
    );
    observer.observe(element);
    document.addEventListener('visibilitychange', update);
    const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const readMotion = () => setReduced(motion.matches);
    readMotion();
    motion.addEventListener('change', readMotion);
    return () => {
      observer.disconnect();
      document.removeEventListener('visibilitychange', update);
      motion.removeEventListener('change', readMotion);
    };
  }, []);

  const take = useCallback(() => {
    setTaken(true);
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => setTaken(false), idle);
  }, [idle]);
  useEffect(() => () => window.clearTimeout(timer.current), []);

  return { ref, take, playing: visible && !reduced && !taken };
}
```