# Menu bar onboarding

> The four-step welcome of a menu-bar capture app, each step acted out by a small desktop that loops above it.

A 448 × 600 panel over a warm desktop: welcome, shortcuts, selection and folders, with Back, Skip for now and Continue. Above each step a miniature desktop plays what it means: the app opens from the menu bar, a window is selected and captured, its thumbnail goes to the canvas to get a backdrop; the shortcut is pressed key by key; the pointer finds areas by itself; a capture drops into a folder. The choices made below (a recorded shortcut, the switches, the folders picked) change what the little desktop shows.

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

Category: Navigation.

## Anatomy

1. **Panel**: A 448 × 600 window over the desktop: the sheet with the step, over a grey footer. Under 480 px it takes the width and its content’s height.
2. **Illustration**: A small desktop that acts out the step on a loop, scaled to the panel’s width. It follows the choices below it.
3. **Back**: Over the illustration from the second step on. It keeps every choice made.
4. **Step label**: The step’s name and where it is, “Folders · 4 of 4”, in place of dots.
5. **Title**: One short line on what the step is for. The step’s words come in 70 ms after a change, over 90 ms.
6. **Folder chip**: A suggested folder, on or off. Off is dashed and greyed, and leaves the illustration’s popover.
7. **Skip for now**: Finishes with the choices made so far and calls `onFinish`, as Get started does. Hidden on the welcome.
8. **Primary button**: Continue, and Get started on the last step. Its press is the one the demo pointer clicks.

## When to use

- The first run of a desktop or menu-bar app, where a few settings matter before the first capture.
- Three to five steps, each one idea with a picture of it at work: what the app does, how to call it, how it behaves.
- Choices people can make in a glance (a shortcut, two switches, a few folders) and change later in the settings.
- A welcome that should teach by showing: each step’s illustration acts out what its words say, and follows the choices made.

## When not to use

- Settings people return to. Put them in a settings window with sections; the onboarding only starts them.
- Required account data, payment or permissions that block the app. Use a form with validation and clear errors.
- One tip about one feature. Use a popover or a coach mark pointing at it.
- More than five steps, or steps that branch on earlier answers: the panel moves in a straight line with Back and Continue.
- A web page’s sign-up flow: the panel is drawn as a native app’s window, at its fixed 448 × 600.

## Installation

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

```sh
npx shadcn@latest add https://brunodelorence.com/r/menubar-onboarding.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 { MenubarOnboarding } from './menubar-onboarding';

export function Welcome() {
  return (
    <MenubarOnboarding
      appName="Clipnest"
      onFinish={(choices) => saveSettings(choices)}
      autoplay={false}
    />
  );
}
```

## States

- **Welcome**: Three features with their icons, and only Continue: there is nothing to go back to or skip yet.
- **Shortcuts**: Two keycap buttons with the current combinations. The illustration presses the capture one key by key.
- **Recording a shortcut**: The keycap waits for a key with a modifier, outlined in blue; Escape cancels. Its words keep blinking.
- **Selection**: Both switches on: the illustration’s pointer finds areas by itself.
- **Auto selection off**: Enhanced detection greys out and can’t be changed, and the illustration drags a selection by hand.
- **Folders**: The three suggested folders on, AI sorting off. The last step’s button reads Get started.
- **Naming a folder**: The plus turns into a field (Enter adds, Escape drops it). Here Work is off and AI on. The folders stay on one line, which scrolls sideways while it is open.
- **Light and dark**: `colorScheme="light"` and `"dark"` side by side: at night the panel turns charcoal, the button light and the desktop a dusk.

## How it works

- The illustrations are one desktop drawn in its own px, 448 wide (wallpaper, menu bar, the window, the app’s popover, a thumbnail), seen through a camera that moves and zooms over it, plus the desktop app’s canvas laid on top in the first step. The whole is scaled to the panel’s width, so a narrow panel shows the same picture smaller.
- Each step has a timeline: for every moving part, what it looks like second by second, played by the Web Animations API as one looping animation per part, all started on the same frame. Only transforms and opacity move (a clip-path for the lit area and the canvas’s cut capture), so the compositor plays them.
- The first step runs 16 s: the camera zooms on the menu bar, the pointer opens the popover and clicks Capture, the screen dims and a selection is dragged around the window, a thumbnail slides up in the corner and Edit opens the canvas, where the capture gets a backdrop and then a layout. Step 2 loops every 5.6 s, step 3 every 5.4 s and the drop into a folder every 4.8 s.
- A selection is four edges and four handles, each moved by its own transform, so it can grow from a corner without being redrawn. The area found in step 3 is lit through the dimmed screen by a copy of the window clipped to it.
- Changing step swaps the content at once: the old content is gone in a frame and the new one fades in 70 ms later over 90 ms, as the illustration jumps to the new step’s framing and starts its loop.
- The shortcut buttons record like the system’s: press one, then a key with a modifier; Escape cancels. The capture shortcut is the one the keys of step 2 press. Turning automatic selection off makes the illustration’s pointer drag by hand, and without enhanced detection only bigger areas are found. Unticked folders leave the popover, and new ones can be named.
- While on screen and untouched, a demo pointer goes through the steps at the pace of the reference, clicking Continue (a ring marks each click) and at the end Get started, and the panel starts over. Any real input hands it to the reader; the illustrations keep looping while the panel is visible and pause when it is not.
- Every colour is a light-dark() token: at night the panel turns charcoal, the primary button light, and the desktop a dusk.

## API reference

- **appName** (`string`, default `'Clipnest'`): The app’s name, in the welcome’s title and text.
- **defaultStep** (`number`, default `0`): The step it opens on, from 0 (welcome) to 3 (folders). Any other step turns the demo off.
- **onFinish** (`(choices: OnboardingChoices) => void`): Called on Get started or Skip for now with the choices made: the two shortcuts, the selection switches, the folders picked and whether AI organizes. The panel then starts over.
- **autoplay** (`boolean`, default `true`): While on screen and untouched, a demo pointer goes through the steps and clicks Continue.
- **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.
- **defaultState** (`OnboardingState`): Opens with choices already made: shortcuts, autoSelection, enhancedDetection, the folders ticked by name (new names are added), organizeWithAI; and recording (a shortcut button waiting for keys) or naming (the new-folder field open). Turns the demo off.
- **still** (`boolean`, default `false`): Holds each step’s illustration on one still moment, as under reduced motion. Turns the demo off.
- **className** (`string`): Extra class for the root.

## Accessibility

- The panel is a labelled section (its title names it); each step’s change is announced in a polite live region (“Step 2 of 4: Choose your shortcuts”). The illustrations are decorative and hidden from screen readers.
- Back, Skip for now, Continue and Get started are buttons. The shortcut buttons are named with their row and their keys in words (“Capture image, Command Shift 4”) and say how to record; a new shortcut is announced.
- The switches are buttons with role="switch" and aria-checked, labelled by their title and described by their text; Enhanced detection is aria-disabled while automatic selection is off. The folders are toggle buttons with aria-pressed, and the new folder field has a label.
- Every control shows a focus ring. Under reduced motion the illustrations hold a still moment of each step, the content swaps without a fade and the demo never plays.

## Source

### menubar-onboarding/menubar-onboarding.tsx

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

'use client';

import {
  useEffect,
  useEffectEvent,
  useId,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactNode,
} from 'react';
import { FolderGlyph, type FolderKind, type HeroFolder } from './art';
import { DemoPointer, runDemo, type Point } from './demo-pointer';
import { Hero } from './hero';
import { CameraIcon, ChevronLeftIcon, EditIcon, FolderIcon, PlusIcon } from './icons';
import { useAutoplay } from './use-autoplay';
import styles from './menubar-onboarding.module.css';

/*
 * Menu bar onboarding: the four-step welcome panel of a menu-bar capture app, hanging over its
 * desktop. Welcome, shortcuts, selection, folders; Back, Skip for now, Continue, and Get started
 * on the last step.
 *
 * - Each step has an illustration at the top (hero.tsx): a small desktop that acts out what the
 *   step is about, on a loop. It follows the choices made below it: the shortcut recorded in step
 *   2 is the one its keys press, turning automatic selection off makes its pointer drag by hand,
 *   and the folders picked in step 4 are the ones in its popover.
 * - Changing step swaps the content at once: the old one goes in a frame, the new one fades in
 *   70 ms later over 90 ms, and the illustration starts its own loop from the top.
 * - The shortcuts are recorded like the system's: press the keycap button, then a combination
 *   with a modifier. Escape cancels.
 * - While on screen and untouched, a demo pointer goes through the steps at the pace of the
 *   illustrations, clicking Continue and, at the end, Get started; the panel then starts over. Any
 *   real input hands the panel to the reader.
 * - Light or dark: every colour is a light-dark() token, so the panel follows the page's
 *   color-scheme, or the one the colorScheme prop sets.
 */

export type OnboardingChoices = {
  /** The capture and record shortcuts, as their glyphs (⌘⇧4). */
  shortcuts: { image: string; video: string };
  autoSelection: boolean;
  enhancedDetection: boolean;
  /** The names of the folders picked. */
  folders: string[];
  organizeWithAI: boolean;
};

/** Where the panel opens, for a still or a prefilled welcome. Any of it turns the demo off. */
export type OnboardingState = {
  /** The shortcuts already set, as their glyphs (⌘⇧4). */
  shortcuts?: { image?: string; video?: string };
  autoSelection?: boolean;
  enhancedDetection?: boolean;
  /** The folders ticked, by name: suggested ones left out are unticked, new names are added. */
  folders?: string[];
  organizeWithAI?: boolean;
  /** A shortcut button waiting for keys. */
  recording?: 'image' | 'video';
  /** The new-folder field open. */
  naming?: boolean;
};

type Folder = HeroFolder & { on: boolean };

const SUGGESTED: Folder[] = [
  { id: 'ideas', name: 'Ideas', kind: 'ideas', files: 1, on: true },
  { id: 'inspiration', name: 'Inspiration', kind: 'inspiration', files: 2, on: true },
  { id: 'work', name: 'Work', kind: 'work', files: 1, on: true },
];

/** The suggested folders, ticked as `names` says, then any other name as a folder of its own. */
function foldersFrom(names?: string[]): Folder[] {
  if (!names) return SUGGESTED;
  const wanted = names.map((name) => name.trim()).filter(Boolean);
  const has = (name: string) => wanted.some((w) => w.toLowerCase() === name.toLowerCase());
  return [
    ...SUGGESTED.map((folder) => ({ ...folder, on: has(folder.name) })),
    ...wanted
      .filter(
        (name) => !SUGGESTED.some((folder) => folder.name.toLowerCase() === name.toLowerCase()),
      )
      .map((name, index) => ({
        id: `custom-${index}`,
        name: name.slice(0, 24),
        kind: 'custom' as FolderKind,
        files: 0,
        on: true,
      })),
  ];
}

const STEPS = ['Welcome', 'Shortcuts', 'Selection', 'Folders'];

/** The order the modifiers are written in. */
const MODIFIERS: [keyof KeyboardEvent & string, string][] = [
  ['metaKey', '⌘'],
  ['altKey', '⌥'],
  ['ctrlKey', '⌃'],
  ['shiftKey', '⇧'],
];

/** The glyphs of a combination, or null for a key that is only a modifier or has none. */
function glyphs(event: ReactKeyboardEvent): string | null {
  const mods = MODIFIERS.filter(([key]) => event[key as 'metaKey']).map(([, glyph]) => glyph);
  if (!mods.length || ['Meta', 'Alt', 'Control', 'Shift'].includes(event.key)) return null;
  const { code, key } = event;
  const main = code.startsWith('Key')
    ? code.slice(3)
    : code.startsWith('Digit')
      ? code.slice(5)
      : key.length === 1
        ? key.toUpperCase()
        : key;
  return mods.join('') + main;
}

/** A combination's glyphs, one key each: modifiers are single characters, the rest is the key. */
const splitKeys = (shortcut: string) => {
  const keys: string[] = [];
  let rest = shortcut;
  while (rest && MODIFIERS.some(([, glyph]) => rest.startsWith(glyph))) {
    keys.push(rest[0]);
    rest = rest.slice(1);
  }
  if (rest) keys.push(rest);
  return keys;
};

/** The demo keeps to the pace of the illustrations: how long it stays on each step, in ms. */
const STAY = [16550, 6750, 6900, 9200];

export function MenubarOnboarding({
  appName = 'Clipnest',
  defaultStep = 0,
  onFinish,
  autoplay = true,
  colorScheme,
  defaultState,
  still = false,
  className = '',
}: {
  /** The app's name, in the welcome. */
  appName?: string;
  /** The step it opens on, from 0 (welcome) to 3 (folders). Turns the demo off. */
  defaultStep?: number;
  /** Called with the choices made, on Get started or Skip for now. The panel then starts over. */
  onFinish?: (choices: OnboardingChoices) => void;
  /** While on screen and untouched, a demo pointer goes through the steps. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  /** Opens with these choices made (and a shortcut recording, or a folder being named). Turns the demo off. */
  defaultState?: OnboardingState;
  /** Holds each step's illustration on one still moment, as under reduced motion. Turns the demo off. */
  still?: boolean;
  className?: string;
}) {
  const { ref: root, playing, take, visible, reduced } = useAutoplay<HTMLDivElement>(8000);
  const [step, setStep] = useState(() => Math.max(0, Math.min(3, Math.round(defaultStep))));
  const [restart, setRestart] = useState(0);
  const [shortcuts, setShortcuts] = useState(() => ({
    image: defaultState?.shortcuts?.image ?? '⌘⇧4',
    video: defaultState?.shortcuts?.video ?? '⌘⇧5',
  }));
  const [recording, setRecording] = useState<'image' | 'video' | null>(
    defaultState?.recording ?? null,
  );
  const [auto, setAuto] = useState(defaultState?.autoSelection ?? true);
  const [enhanced, setEnhanced] = useState(defaultState?.enhancedDetection ?? true);
  const [folders, setFolders] = useState<Folder[]>(() => foldersFrom(defaultState?.folders));
  const [naming, setNaming] = useState(defaultState?.naming ?? false);
  const [ai, setAi] = useState(defaultState?.organizeWithAI ?? false);
  const [message, setMessage] = useState('');
  const pointer = useRef<HTMLSpanElement>(null);
  const ring = useRef<HTMLSpanElement>(null);
  const titleId = useId();
  const last = STEPS.length - 1;

  const announce = (text: string) => setMessage((now) => (now === text ? `${text} ` : text));

  const go = (next: number) => {
    const to = Math.max(0, Math.min(last, next));
    setStep(to);
    setRecording(null);
    setNaming(false);
    announce(`Step ${to + 1} of ${STEPS.length}: ${titleOf(to, appName)}`);
  };

  const finish = () => {
    onFinish?.({
      shortcuts,
      autoSelection: auto,
      enhancedDetection: auto && enhanced,
      folders: folders.filter((folder) => folder.on).map((folder) => folder.name),
      organizeWithAI: ai,
    });
    announce('All set. The welcome starts over.');
    setRecording(null);
    setNaming(false);
    setStep(0);
    setRestart((count) => count + 1);
  };

  const heroFolders = folders.filter((folder) => folder.on);

  // The demo: the pointer reads, hovers, and clicks Continue when the illustration has played.
  const demo = autoplay && playing && defaultStep === 0 && !defaultState && !still;
  const demoStep = useEffectEvent((next: number) => (next > last ? finish() : go(next)));
  const stepNow = useRef(step);
  stepNow.current = step;
  useEffect(() => {
    const stage = root.current;
    const arrow = pointer.current;
    const circle = ring.current;
    if (!demo || !stage || !arrow || !circle) return;
    /** A point of a part of the panel, in the stage's own px: (fx, fy) are shares of its box. */
    const spot = (part: string, fx = 0.5, fy = 0.5): Point => {
      const frame = stage.getBoundingClientRect();
      const scale = frame.width / stage.offsetWidth || 1;
      const box = stage.querySelector(`[data-part="${part}"]`)?.getBoundingClientRect();
      if (!box) return { x: stage.offsetWidth / 2, y: stage.offsetHeight / 2 };
      return {
        x: (box.left + box.width * fx - frame.left) / scale,
        y: (box.top + box.height * fy - frame.top) / scale,
      };
    };
    let pressed: HTMLElement | null = null;
    const stop = runDemo(stage, arrow, async ({ wait, glide }) => {
      /** Presses the primary button as a hand would: down, a beat, up. */
      const click = async () => {
        const at = spot('primary', 0.42, 0.55);
        circle.style.transform = `translate(${at.x}px, ${at.y}px)`;
        circle.getAnimations().forEach((animation) => animation.cancel());
        circle.animate(
          [
            { opacity: 0, scale: 0.62 },
            { opacity: 1, scale: 0.66, offset: 0.08 },
            { opacity: 1, scale: 0.94, offset: 0.78 },
            { opacity: 0, scale: 0.9 },
          ],
          { duration: 600, easing: 'ease-out' },
        );
        pressed = stage.querySelector<HTMLElement>('[data-part="primary"]');
        pressed?.setAttribute('data-pressed', '');
        await wait(420);
        pressed?.removeAttribute('data-pressed');
        await wait(110);
        demoStep(s + 1);
        s = s < last ? s + 1 : 0;
      };
      // The step the demo is on: it moves on before React has drawn the next one.
      let s = stepNow.current;
      for (;;) {
        const begin = performance.now();
        const left = (ms: number) => Math.max(0, begin + ms - performance.now());
        if (s === 0) {
          await wait(300);
          await glide(spot('features', 0.6, 0.12), 900);
          await wait(800);
          await glide(spot('features', 0.62, 0.02), 900);
          await wait(1600);
          await glide(spot('features', 0.58, 0.35), 1100);
          await wait(2200);
          await glide(spot('body', 0.62, 0.2), 1100);
          await wait(1600);
          await glide(spot('features', 0.66, 0.6), 1200);
          await wait(left(12400));
          await glide(spot('primary', 0.42, 0.55), 900);
          await wait(left(STAY[0] - 530));
        } else if (s === 1) {
          await wait(left(STAY[1] - 530));
        } else if (s === 2) {
          await wait(1300);
          await glide(spot('hero', 0.47, 0.72), 1000);
          await wait(900);
          await glide(spot('hero', 0.5, 0.8), 700);
          await wait(900);
          await glide(spot('body', 0.8, 0.35), 900);
          await wait(left(5900));
          await glide(spot('primary', 0.42, 0.55), 700);
          await wait(left(STAY[2] - 530));
        } else {
          await wait(700);
          await glide(spot('chips', 0.82, 0.95), 600);
          await glide(spot('hero', 0.55, 0.42), 900);
          await wait(900);
          await glide(spot('hero', 0.63, 0.6), 800);
          await wait(500);
          await glide(spot('body', 0.6, 0.9), 900);
          await wait(left(7900));
          await glide(spot('primary', 0.42, 0.55), 800);
          await wait(left(STAY[3] - 530));
        }
        await click();
      }
    });
    return () => {
      stop();
      pressed?.removeAttribute('data-pressed');
      circle.getAnimations().forEach((animation) => animation.cancel());
    };
  }, [demo, root]);

  // A shortcut is recorded from the next combination with a modifier; Escape cancels.
  const record = (which: 'image' | 'video', event: ReactKeyboardEvent<HTMLButtonElement>) => {
    if (recording !== which) return;
    if (event.key === 'Tab') {
      setRecording(null);
      return;
    }
    event.preventDefault();
    if (event.key === 'Escape') {
      setRecording(null);
      announce('Recording cancelled.');
      return;
    }
    const combo = glyphs(event);
    if (!combo) return;
    setShortcuts((now) => ({ ...now, [which]: combo }));
    setRecording(null);
    announce(`${which === 'image' ? 'Capture image' : 'Record video'} is now ${spoken(combo)}.`);
  };

  const toggleFolder = (id: string) =>
    setFolders((list) =>
      list.map((folder) => (folder.id === id ? { ...folder, on: !folder.on } : folder)),
    );

  const addFolder = (name: string) => {
    const clean = name.trim().slice(0, 24);
    setNaming(false);
    if (!clean) return;
    if (folders.some((folder) => folder.name.toLowerCase() === clean.toLowerCase())) {
      announce(`${clean} is already there.`);
      return;
    }
    setFolders((list) => [
      ...list,
      { id: `custom-${Date.now()}`, name: clean, kind: 'custom' as FolderKind, files: 0, on: true },
    ]);
    announce(`Folder ${clean} added.`);
  };

  const content: ReactNode[] = [
    <ul key="features" className={styles.features} data-part="features">
      {[
        [CameraIcon, 'Capture anything', 'Take screenshots or record part of your screen.'],
        [FolderIcon, 'Keep it organized', 'Collect captures in folders, with optional AI sorting.'],
        [
          EditIcon,
          'Continue in the desktop app',
          'Bring your captures into the canvas to edit and export.',
        ],
      ].map(([Glyph, title, text]) => {
        const Icon = Glyph as typeof CameraIcon;
        return (
          <li key={title as string} className={styles.feature} data-part="feature">
            <span className={styles.featureIcon}>
              <Icon className={styles.icon} />
            </span>
            <span className={styles.featureText}>
              <b>{title as string}</b>
              <span>{text as string}</span>
            </span>
          </li>
        );
      })}
    </ul>,
    <section key="shortcuts" className={styles.section} aria-labelledby={`${titleId}-s`}>
      <h3 id={`${titleId}-s`} className={styles.label}>
        Shortcuts
      </h3>
      <div className={styles.group} data-part="shortcuts">
        {(
          [
            ['image', 'Capture image'],
            ['video', 'Record video'],
          ] as const
        ).map(([which, label]) => (
          <div key={which} className={styles.shortcutRow} data-part="shortcut-row">
            <span id={`${titleId}-${which}`} className={styles.rowTitle}>
              {label}
            </span>
            <button
              type="button"
              className={styles.keycap}
              data-part="keycap"
              data-recording={recording === which || undefined}
              aria-labelledby={`${titleId}-${which} ${titleId}-${which}-k`}
              aria-describedby={`${titleId}-hint`}
              onClick={() => setRecording((now) => (now === which ? null : which))}
              onKeyDown={(event) => record(which, event)}
              onBlur={() => setRecording((now) => (now === which ? null : now))}
            >
              <span
                id={`${titleId}-${which}-k`}
                aria-label={recording === which ? 'Recording' : spoken(shortcuts[which])}
              >
                {recording === which ? 'Type keys…' : shortcuts[which]}
              </span>
            </button>
          </div>
        ))}
      </div>
      <span id={`${titleId}-hint`} className={styles.hidden}>
        Press to record a new shortcut: a key with Command, Option, Control or Shift. Escape
        cancels.
      </span>
    </section>,
    <div key="selection" className={styles.group} data-part="toggles">
      <Toggle
        id={`${titleId}-auto`}
        title="Auto selection"
        text="Detect areas as you move the pointer."
        on={auto}
        onChange={setAuto}
      />
      <Toggle
        id={`${titleId}-enh`}
        title="Enhanced detection"
        text="Use Accessibility to recognize more interface elements."
        on={auto && enhanced}
        disabled={!auto}
        onChange={setEnhanced}
      />
    </div>,
    <div key="folders">
      <section className={styles.section} aria-labelledby={`${titleId}-f`}>
        <h3 id={`${titleId}-f`} className={styles.label}>
          Suggested folders
        </h3>
        <div className={styles.chips} data-part="chips">
          {naming ? (
            <input
              className={`${styles.chip} ${styles.chipInput}`}
              data-part="chip-input"
              aria-label="New folder name"
              placeholder="Folder name"
              maxLength={24}
              autoFocus
              onKeyDown={(event) => {
                if (event.key === 'Enter') addFolder(event.currentTarget.value);
                if (event.key === 'Escape') setNaming(false);
              }}
              onBlur={(event) => addFolder(event.currentTarget.value)}
            />
          ) : (
            <button
              type="button"
              className={`${styles.chip} ${styles.chipAdd}`}
              data-part="chip-add"
              aria-label="New folder"
              onClick={() => setNaming(true)}
            >
              <PlusIcon className={styles.chipIcon} />
            </button>
          )}
          {folders.map((folder) => (
            <button
              key={folder.id}
              type="button"
              className={styles.chip}
              data-part="chip"
              data-kind={folder.kind}
              aria-pressed={folder.on}
              onClick={() => toggleFolder(folder.id)}
            >
              <FolderGlyph kind={folder.kind} className={styles.chipIcon} />
              {folder.name}
            </button>
          ))}
        </div>
      </section>
      <div className={`${styles.group} ${styles.aiGroup}`} data-part="ai">
        <Toggle
          id={`${titleId}-ai`}
          title="Organize with AI"
          text="AI picks a folder based on each capture’s content."
          on={ai}
          onChange={setAi}
        />
      </div>
    </div>,
  ];

  const texts = TEXTS(appName)[step];

  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();
      }}
    >
      <div className={styles.wallpaper} aria-hidden="true" />
      <section
        key={restart}
        className={styles.panel}
        data-part="panel"
        data-restart={restart > 0 || undefined}
        aria-labelledby={titleId}
        aria-roledescription="onboarding"
      >
        <div className={styles.sheet} data-part="sheet">
          <Hero
            step={step}
            keys={splitKeys(shortcuts.image)}
            auto={auto}
            enhanced={enhanced}
            folders={heroFolders}
            ai={ai}
            running={visible}
            still={reduced || still}
          />
          {step > 0 && (
            <button
              key={`back-${step > 0}`}
              type="button"
              className={styles.back}
              data-part="back"
              aria-label="Back"
              onClick={() => go(step - 1)}
            >
              <ChevronLeftIcon className={styles.backIcon} />
            </button>
          )}
          <div key={step} className={styles.content} data-part="content" data-step={step + 1}>
            <p className={styles.eyebrow} data-part="eyebrow">
              {STEPS[step]} · {step + 1} of {STEPS.length}
            </p>
            <h2 id={titleId} className={styles.title} data-part="title">
              {texts.title}
            </h2>
            <p className={styles.body} data-part="body">
              {texts.body}
            </p>
            {content[step]}
          </div>
        </div>
        <div className={styles.footer} data-part="footer">
          {step > 0 && (
            <button
              key="skip"
              type="button"
              className={styles.skip}
              data-part="skip"
              onClick={finish}
            >
              Skip for now
            </button>
          )}
          <button
            type="button"
            className={styles.primary}
            data-part="primary"
            onClick={() => (step < last ? go(step + 1) : finish())}
          >
            {step < last ? 'Continue' : 'Get started'}
          </button>
        </div>
      </section>
      <div className={styles.pointerLayer} aria-hidden="true">
        <span ref={ring} className={styles.ring} />
        <DemoPointer
          ref={pointer}
          className={styles.pointer}
          arrowClass={styles.arrow}
          handClass={styles.hand}
        />
      </div>
      <p className={styles.hidden} role="status">
        {message}
      </p>
    </div>
  );
}

function Toggle({
  id,
  title,
  text,
  on,
  disabled,
  onChange,
}: {
  id: string;
  title: string;
  text: string;
  on: boolean;
  disabled?: boolean;
  onChange: (on: boolean) => void;
}) {
  return (
    <div className={styles.toggleRow} data-part="toggle-row" data-disabled={disabled || undefined}>
      <span className={styles.toggleText}>
        <span id={`${id}-t`} className={styles.rowTitle}>
          {title}
        </span>
        <span id={`${id}-d`} className={styles.rowText}>
          {text}
        </span>
      </span>
      <button
        type="button"
        role="switch"
        className={styles.switch}
        data-part="switch"
        aria-checked={on}
        aria-disabled={disabled || undefined}
        aria-labelledby={`${id}-t`}
        aria-describedby={`${id}-d`}
        onClick={() => !disabled && onChange(!on)}
      >
        <span className={styles.knob} />
      </button>
    </div>
  );
}

const TEXTS = (app: string) => [
  {
    title: `${app}, in your menu bar`,
    body: `Capture and organize here. Keep creating in the ${app} desktop app.`,
  },
  {
    title: 'Choose your shortcuts',
    body: 'Capture without opening the menu. Each shortcut works from any app and can be changed later.',
  },
  {
    title: 'Select with less effort',
    body: 'Hover to find an area, then click to capture it. You can always drag your own selection.',
  },
  {
    title: 'A place for your captures',
    body: 'Keep related files together. Pick a few folders to get started, or make your own.',
  },
];

const titleOf = (step: number, app: string) => TEXTS(app)[step].title;

/** A shortcut's glyphs in words, for screen readers. */
function spoken(shortcut: string) {
  const names: Record<string, string> = {
    '⌘': 'Command',
    '⌥': 'Option',
    '⌃': 'Control',
    '⇧': 'Shift',
  };
  return splitKeys(shortcut)
    .map((key) => names[key] ?? key)
    .join(' ');
}
```

### menubar-onboarding/menubar-onboarding.module.css

```css
/* Menu bar onboarding (menubar-onboarding.tsx): a 448 × 600 panel on a warm desktop wallpaper.
   The panel is a sheet (the illustration and the step's content) over a grey footer that holds
   Skip for now and the primary button. Sizes are the panel's real ones, in px; under 480 px the
   panel takes the stage's width, its illustration scales down and its height follows its content.

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

   Memory. The stage is one layout boundary sized from outside. Only transforms and opacity move:
   the content's fade-in on a step change, the knobs of the switches, the demo pointer and its
   click ring. The wallpaper is gradients, painted once. */

.root {
  --wall-1: #f8c84c;
  --wall-2: #f5a532;
  --wall-3: #ef8426;
  --wall-4: #e25a29;
  --wall-5: #cc3c1d;
  --wall-deep: #ac1d19;
  --wall-sun: #ffe08a;
  --wall-blue: #3a86d2;
  --sheet: #fdfdfd;
  --footer: #f5f5f5;
  --ink: #0d0d0d;
  --muted: #5d5d5d;
  --line: #e7e7e7;
  --icon: #3d3d3d;
  --tile: #fff;
  --tile-edge: rgb(0 0 0 / 0.08);
  --primary: #262626;
  --primary-hover: #3a3a3a;
  --primary-ink: #fff;
  --ghost-hover: rgb(0 0 0 / 0.05);
  --switch-on: #6e6e6e;
  --switch-off: #d6d6d6;
  --knob: #fff;
  --back: rgb(255 255 255 / 0.55);
  --back-ink: #4b4b4b;
  --chip-off: #9a9a9a;
  --ideas: #f2a31b;
  --inspiration: #9b55e0;
  --work: #3b7ff0;
  --custom: #6d6d72;
  --focus: #387bff;
  --ring: #f4a467;
  --pointer: #1c1c1e;
  --pointer-edge: #fff;
  --pointer-shadow: rgb(0 0 0 / 0.25);
  position: relative;
  container-type: inline-size;
  contain: layout;
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 720px;
  padding: 60px 16px;
  box-sizing: border-box;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  color: var(--ink);
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --wall-1: light-dark(#f8d44c, #b4712a);
    --wall-2: light-dark(#f6b434, #9c5220);
    --wall-3: light-dark(#f29428, #833a1c);
    --wall-4: light-dark(#e46c21, #63241f);
    --wall-5: light-dark(#cf3c20, #3f1820);
    --wall-deep: light-dark(#ac1d19, #2a1018);
    --wall-sun: light-dark(#ffe08a, #d99a45);
    --wall-blue: light-dark(#3a86d2, #22457c);
    --sheet: light-dark(#fdfdfd, #222225);
    --footer: light-dark(#f5f5f5, #1a1a1c);
    --ink: light-dark(#0d0d0d, #f3f3f4);
    --muted: light-dark(#5d5d5d, #a3a3a8);
    --line: light-dark(#e7e7e7, #38383c);
    --icon: light-dark(#3d3d3d, #d4d4d8);
    --tile: light-dark(#fff, #2c2c30);
    --tile-edge: light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.08));
    --primary: light-dark(#262626, #f1f1f2);
    --primary-hover: light-dark(#3a3a3a, #ffffff);
    --primary-ink: light-dark(#fff, #141414);
    --ghost-hover: light-dark(rgb(0 0 0 / 0.05), rgb(255 255 255 / 0.07));
    --switch-on: light-dark(#6e6e6e, #a8a8ad);
    --switch-off: light-dark(#d6d6d6, #46464b);
    --knob: light-dark(#fff, #fff);
    --back: light-dark(rgb(255 255 255 / 0.55), rgb(30 30 32 / 0.55));
    --back-ink: light-dark(#4b4b4b, #e6e6e6);
    --chip-off: light-dark(#9a9a9a, #77777c);
    --ideas: light-dark(#f2a31b, #f6b443);
    --inspiration: light-dark(#9b55e0, #b98af0);
    --work: light-dark(#3b7ff0, #6ea2f6);
    --custom: light-dark(#6d6d72, #a6a6ab);
  }
}
/* 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;
}

/* The desktop behind the panel: the same afternoon as the illustrations, larger. */
.wallpaper {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      130% 62% at -28% 104%,
      transparent 44%,
      color-mix(in srgb, var(--wall-sun) 45%, transparent) 48.5%,
      transparent 53%
    ),
    radial-gradient(
      110% 52% at -30% 118%,
      transparent 46%,
      color-mix(in srgb, var(--wall-2) 70%, transparent) 50%,
      transparent 55%
    ),
    radial-gradient(
      70% 55% at 128% 104%,
      transparent 42%,
      color-mix(in srgb, var(--wall-4) 80%, transparent) 46%,
      transparent 52%
    ),
    radial-gradient(
      60% 40% at 30% 4%,
      color-mix(in srgb, var(--wall-sun) 45%, transparent) 0%,
      transparent 70%
    ),
    linear-gradient(
      168deg,
      var(--wall-1) 0%,
      var(--wall-2) 18%,
      var(--wall-3) 36%,
      var(--wall-4) 54%,
      var(--wall-5) 72%,
      var(--wall-deep) 92%
    );
}

/* ─── The panel ─────────────────────────────────────────────────────────────────────────────── */

.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 448px;
  height: 600px;
  overflow: hidden;
  overflow: clip;
  border-radius: 22px;
  background: var(--footer);
  box-shadow:
    0 0 0 0.5px light-dark(rgb(0 0 0 / 0.12), rgb(255 255 255 / 0.1)),
    0 24px 60px rgb(70 20 0 / 0.28),
    0 6px 18px rgb(70 20 0 / 0.12);
}
.panel[data-restart] {
  animation: restart 420ms cubic-bezier(0.32, 0.72, 0, 1) both;
}
@keyframes restart {
  from {
    opacity: 0;
    transform: scale(0.97);
  }
}

.sheet {
  position: relative;
  z-index: 1;
  flex: 0 0 530.5px;
  overflow: hidden;
  overflow: clip;
  border-radius: 0 0 18px 18px;
  background: var(--sheet);
  box-shadow:
    0 1px 2px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.4)),
    0 3px 12px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.35));
  /* The hero's fade ends on this colour. */
  --sheet-bg: var(--sheet);
}

.back {
  position: absolute;
  top: 6px;
  left: 14.9px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 33px;
  height: 33px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: var(--back);
  box-shadow: 0 0 0 0.5px rgb(255 255 255 / 0.3);
  color: var(--back-ink);
  cursor: pointer;
  animation: enter 90ms 70ms ease-out both;
  transition:
    background-color 150ms ease,
    transform 150ms ease;
}
.back:hover {
  background: light-dark(rgb(255 255 255 / 0.75), rgb(50 50 54 / 0.75));
}
.back:active {
  transform: scale(0.94);
}
.backIcon {
  width: 15px;
  height: 15px;
}

/* The step's words, under the illustration. They come in 70 ms after a step change, over 90 ms:
   the old ones are gone in a frame. */
.content {
  padding: 13.8px 24px 0;
  animation: enter 90ms 70ms ease-out both;
}
@keyframes enter {
  from {
    opacity: 0;
  }
}
.eyebrow {
  margin: 0;
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 600;
  line-height: 16px;
}
.title {
  margin: 6.8px 0 0;
  color: var(--ink);
  font-size: 20px;
  font-weight: 700;
  line-height: 26px;
  outline: none;
}
.body {
  max-width: 379.5px;
  margin: 7.5px 0 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 20px;
}

/* Step 1: what the app does. */
.features {
  margin: 20px 0 0;
  padding: 0;
  list-style: none;
}
.feature {
  display: grid;
  grid-template-columns: 41.5px 1fr;
  gap: 17.5px;
  align-items: start;
}
.feature + .feature {
  margin-top: 19.7px;
}
.featureIcon {
  display: grid;
  place-items: center;
  width: 41.5px;
  height: 41.5px;
  margin: 1px 0 0 1px;
  box-sizing: border-box;
  border-radius: 10px;
  background: var(--tile);
  box-shadow:
    0 0 0 1px var(--tile-edge),
    0 1px 2px rgb(0 0 0 / 0.05),
    0 3px 8px rgb(0 0 0 / 0.04);
  color: var(--icon);
}
.icon {
  width: 21px;
  height: 21px;
}
.featureText {
  display: flex;
  flex-direction: column;
  gap: 2.5px;
  min-width: 0;
}
.featureText b {
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
}
.featureText span {
  color: var(--muted);
  font-size: 13px;
  line-height: 20px;
}

/* Steps 2 to 4: a heading over grouped rows, drawn a little wider than the words. */
.section {
  margin: 20px 0 0;
}
.label {
  margin: 0 -6.4px;
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
}
.group {
  margin: 10.5px -7px 0;
  border: 1px solid var(--line);
  border-radius: 12.5px;
}
.content[data-step='3'] .group {
  margin-top: 21px;
}
.shortcutRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  height: 58px;
  margin: 0 14px 0 15.4px;
}
.shortcutRow + .shortcutRow {
  border-top: 1px solid var(--line);
}
.rowTitle {
  color: var(--ink);
  font-size: 15px;
  line-height: 20px;
}
.keycap {
  display: inline-grid;
  place-items: center;
  min-width: 63px;
  height: 33px;
  padding: 0 13.6px;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--tile);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition:
    border-color 150ms ease,
    box-shadow 150ms ease,
    transform 150ms ease;
}
.keycap:hover {
  border-color: light-dark(#d2d2d2, #4c4c51);
}
.keycap:active {
  transform: scale(0.97);
}
.keycap[data-recording] {
  border-color: var(--focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 22%, transparent);
  color: var(--muted);
  font-size: 13px;
  font-weight: 400;
}
.keycap[data-recording] span {
  animation: blink 1.1s ease-in-out infinite;
}
@keyframes blink {
  50% {
    opacity: 0.45;
  }
}

/* Switch rows (steps 3 and 4). */
.toggleRow {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin: 0 15.6px 0 15.4px;
  padding: 12px 0 13px;
}
.toggleRow + .toggleRow {
  border-top: 1px solid var(--line);
  padding-bottom: 11.7px;
}
.toggleRow[data-disabled] .toggleText {
  opacity: 0.45;
}
.toggleText {
  display: flex;
  flex-direction: column;
  gap: 2.5px;
  min-width: 0;
  transition: opacity 200ms ease;
}
.toggleText .rowTitle {
  font-size: 14px;
  font-weight: 600;
}
.rowText {
  color: var(--muted);
  font-size: 13px;
  line-height: 20px;
}
.switch {
  position: relative;
  flex: 0 0 auto;
  width: 43.6px;
  height: 22.6px;
  margin: 1.3px 0 0;
  padding: 0;
  border: 0;
  border-radius: 11.3px;
  background: var(--switch-off);
  box-shadow: 0 0 0 1px light-dark(rgb(0 0 0 / 0.03), rgb(255 255 255 / 0.04));
  cursor: pointer;
  transition: background-color 200ms ease;
}
.switch[aria-checked='true'] {
  background: var(--switch-on);
}
.switch[aria-disabled='true'] {
  opacity: 0.45;
  cursor: default;
}
.knob {
  position: absolute;
  top: 4.2px;
  left: 3.3px;
  width: 22.5px;
  height: 14.2px;
  border-radius: 7.1px;
  background: var(--knob);
  box-shadow: 0 0.5px 1.5px rgb(0 0 0 / 0.18);
  transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1);
}
.switch[aria-checked='true'] .knob {
  transform: translateX(14.6px);
}
.switch:active:not([aria-disabled='true']) .knob {
  transform: scaleX(1.12);
  transform-origin: left center;
}
.switch[aria-checked='true']:active:not([aria-disabled='true']) .knob {
  transform: translateX(14.6px) scaleX(1.12);
  transform-origin: right center;
}

/* Step 4: the suggested folders, as chips, and the AI switch. */
.chips {
  /* One row that scrolls sideways once a new folder or its name field makes it longer, so the
     sheet keeps its height and the footer is never cut; the padding keeps focus rings in view. */
  display: flex;
  gap: 10.4px;
  margin: 13.4px -4px -4px -5.4px;
  padding: 4px;
  overflow-x: auto;
  scrollbar-width: none;
}
.chips > * {
  flex: none;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 32.5px;
  padding: 0 13.2px;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 8.5px;
  background: var(--tile);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.03);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition:
    border-color 150ms ease,
    background-color 150ms ease,
    color 150ms ease,
    transform 150ms ease;
}
.chip:hover {
  border-color: light-dark(#d2d2d2, #4c4c51);
}
.chip:active {
  transform: scale(0.96);
}
.chipIcon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  transition: color 150ms ease;
}
.chip[data-kind='ideas'] .chipIcon {
  color: var(--ideas);
}
.chip[data-kind='inspiration'] .chipIcon {
  color: var(--inspiration);
}
.chip[data-kind='work'] .chipIcon {
  color: var(--work);
}
.chip[data-kind='custom'] .chipIcon {
  color: var(--custom);
}
/* A folder left out: dashed, and greyed. */
.chip[aria-pressed='false'] {
  border-style: dashed;
  background: transparent;
  box-shadow: none;
  color: var(--chip-off);
}
.chip[aria-pressed='false'] .chipIcon {
  color: var(--chip-off);
}
.chipAdd {
  width: 32.5px;
  padding: 0;
  justify-content: center;
}
.chipAdd .chipIcon {
  width: 19px;
  height: 19px;
  color: var(--ink);
}
.chipInput {
  width: 128px;
  cursor: text;
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 22%, transparent);
}
.chipInput::placeholder {
  color: var(--chip-off);
}
.aiGroup {
  margin-top: 24.2px;
}
.aiGroup .toggleRow {
  margin: 0 28px 0 29.7px;
  padding: 14.1px 0 16.9px;
}
.aiGroup .switch {
  margin-top: 5.8px;
}
.aiGroup .rowTitle {
  font-size: 15px;
  font-weight: 500;
}
.aiGroup .toggleText {
  gap: 4.8px;
}
.aiGroup .rowText {
  font-size: 12px;
  line-height: 16px;
}

/* ─── The footer ─────────────────────────────────────────────────────────────────────────────── */

.footer {
  display: flex;
  align-items: center;
  flex: 1 0 auto;
  min-height: 69.5px;
  padding: 2px 16px 0 24px;
  box-sizing: border-box;
}
.skip {
  height: 40px;
  margin-left: 0;
  padding: 0 12.9px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 17px;
  cursor: pointer;
  animation: enter 90ms 70ms ease-out both;
  transition: background-color 150ms ease;
}
.skip:hover {
  background: var(--ghost-hover);
}
.primary {
  height: 43.6px;
  margin-left: auto;
  padding: 0 21.5px;
  border: 0;
  border-radius: 10px;
  background: var(--primary);
  color: var(--primary-ink);
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color 150ms ease,
    transform 150ms ease;
}
.primary:hover {
  background: var(--primary-hover);
}
.primary:active,
.primary[data-pressed] {
  transform: scale(0.97);
}

.back:focus-visible,
.skip:focus-visible,
.primary:focus-visible,
.keycap:focus-visible,
.switch:focus-visible,
.chip:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A narrow stage: the panel takes its width and its content's height. */
@container (max-width: 479px) {
  .panel {
    width: 100%;
    height: auto;
  }
  .sheet {
    flex: 0 0 auto;
    padding-bottom: 22px;
  }
  .body {
    max-width: none;
  }
  .content {
    padding: 12px 18px 0;
  }
  .footer {
    padding: 12px 12px 12px 14px;
  }
  .skip {
    font-size: 15px;
  }
}

/* ─── The demo pointer and its click ring ───────────────────────────────────────────────────── */

.pointerLayer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.pointer {
  width: 36px;
  height: 36px;
}
.hand {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .arrow {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .hand {
  visibility: visible;
}
.ring {
  position: absolute;
  top: -50px;
  left: -50px;
  width: 100px;
  height: 100px;
  box-sizing: border-box;
  border: 2px solid var(--ring);
  border-radius: 50%;
  opacity: 0;
}

.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) {
  .content,
  .back,
  .skip,
  .panel[data-restart] {
    animation: none;
  }
  .knob,
  .switch,
  .chip,
  .keycap,
  .primary {
    transition: none;
  }
}
```

### menubar-onboarding/hero.tsx

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

'use client';

import { useEffect, useLayoutEffect, useRef } from 'react';
import {
  Canvas,
  CapturePill,
  CapturesPopover,
  FoldersPopover,
  Keycaps,
  MenuBar,
  MiniWindow,
  Selection,
  Thumbnail,
  type HeroFolder,
} from './art';
import { MiniArrow } from './icons';
import { timeline, type Timeline } from './timelines';
import styles from './hero.module.css';

/*
 * The illustration at the top of the panel: a small desktop that acts out each step.
 *
 * - It is one desktop for the four steps (wallpaper, menu bar, the window being captured, the
 *   app's popover, a thumbnail) seen through a camera that moves and zooms, plus the desktop app's
 *   canvas laid over it in the first step. Each step sets the parts that stay still (data-scene)
 *   and plays its timeline (timelines.ts): one Web Animations API animation per moving part, all
 *   started on the same frame and looping together.
 * - The desktop is drawn 448 px wide and scaled to the panel's width, so a narrow panel shows the
 *   same picture smaller.
 * - It plays while the panel is on screen and the tab visible, and pauses otherwise. Under reduced
 *   motion it holds a still moment of each step instead.
 */

/** The desktop's width, and the height of the hero, in its own px. */
export const HERO_W = 448;
export const HERO_H = 210;

export function Hero({
  step,
  keys,
  auto,
  enhanced,
  folders,
  ai,
  running,
  still,
}: {
  step: number;
  /** The capture shortcut, key by key. */
  keys: string[];
  auto: boolean;
  enhanced: boolean;
  folders: HeroFolder[];
  ai: boolean;
  /** On screen and in a visible tab: the loops play. */
  running: boolean;
  /** Reduced motion: a still moment instead. */
  still: boolean;
}) {
  const box = useRef<HTMLDivElement>(null);
  const stage = useRef<HTMLDivElement>(null);
  const animations = useRef<Animation[]>([]);
  const running$ = useRef(running);
  running$.current = running;

  // The desktop scales to the panel's width.
  useLayoutEffect(() => {
    const element = box.current;
    if (!element) return;
    const fit = () => element.style.setProperty('--k', String(element.clientWidth / HERO_W || 1));
    fit();
    const observer = new ResizeObserver(fit);
    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  const target = folders.findIndex((folder) => folder.kind === 'inspiration');
  const slot = target >= 0 ? Math.min(target, 2) : Math.min(1, Math.max(0, folders.length - 1));
  const folderKey = folders.map((folder) => folder.id).join(',');

  // Each step (and each option that changes what it shows) starts its timeline from the top.
  useEffect(() => {
    const root = stage.current;
    if (!root) return;
    const plan: Timeline = timeline(step, { auto, enhanced, slot });
    const list = play(root, plan);
    animations.current = list;
    if (still) {
      for (const animation of list) {
        animation.currentTime = plan.pose * 1000;
        animation.pause();
      }
    } else if (!running$.current) list.forEach((animation) => animation.pause());
    return () => {
      list.forEach((animation) => animation.cancel());
      animations.current = [];
    };
  }, [step, auto, enhanced, slot, folderKey, still]);

  // Off screen, or in a hidden tab, the loops wait where they are.
  useEffect(() => {
    if (still) return;
    for (const animation of animations.current) {
      if (running) animation.play();
      else animation.pause();
    }
  }, [running, still]);

  return (
    <div ref={box} className={styles.hero} data-part="hero" aria-hidden="true">
      <div ref={stage} className={styles.stage} data-scene={step + 1}>
        <div className={styles.camera} data-a="camera">
          <span className={styles.wall}>
            <i />
            <i />
            <i />
            <i />
            <i />
            <i />
            <i />
            <i />
          </span>
          <div className={styles.scenery} data-a="scenery">
            <MenuBar />
            <div className={styles.windowAt}>
              <MiniWindow />
            </div>
          </div>
          <span className={styles.dim} data-a="dim" />
          <div className={styles.windowAt}>
            <span className={styles.lit} data-a="lit">
              <MiniWindow />
            </span>
            <span className={styles.flash} data-a="flash" />
            <Selection name="hov" />
            <Selection name="sel" />
            <CapturePill />
          </div>
          <Keycaps keys={keys} />
          <CapturesPopover />
          <FoldersPopover folders={folders} ai={ai} />
          <Thumbnail />
          <span className={styles.cursor} data-a="cursor">
            <MiniArrow className={styles.cursorGlyph} />
          </span>
        </div>
        <Canvas />
        <span className={styles.fade} />
      </div>
    </div>
  );
}

/** Starts every track of `plan` on the parts of `root`, all on the same frame. */
function play(root: HTMLElement, plan: Timeline): Animation[] {
  const list: Animation[] = [];
  for (const track of plan.tracks) {
    const cycle = track.cycle ?? plan.cycle;
    const frames = [...track.frames].sort((a, b) => a[0] - b[0]);
    if (frames[0][0] > 0) frames.unshift([0, { ...frames[0][1] }]);
    if (frames[frames.length - 1][0] < cycle)
      frames.push([cycle, { ...frames[frames.length - 1][1] }]);
    const keyframes = frames.map(([time, frame]) => ({
      ...frame,
      offset: Math.min(1, time / cycle),
    }));
    root.querySelectorAll<HTMLElement>(`[data-a="${track.a}"]`).forEach((element) => {
      list.push(
        element.animate(keyframes, {
          duration: cycle * 1000,
          delay: (track.delay ?? 0) * 1000,
          iterations: track.iterations ?? Infinity,
          fill: 'both',
        }),
      );
    });
  }
  const now = document.timeline.currentTime;
  if (now !== null) list.forEach((animation) => (animation.startTime = now));
  return list;
}
```

### menubar-onboarding/timelines.ts

```tsx
/*
 * The illustrations' timelines: for each step, what every moving part of the desktop does, second
 * by second, as keyframes the Web Animations API plays on a loop. A part is found by its data-a
 * name in the hero. Every value is a transform or an opacity (or, for two small parts, a clip-path
 * and a background), so the compositor plays them.
 *
 * Coordinates are the desktop's own px: the desktop is drawn 448 px wide with the window at
 * (112, 51), 196 × 134, and a camera (the "camera" part) moves and zooms over it. The canvas of the
 * desktop app is drawn over the desktop, in the hero's px.
 */

export type Frame = [number, Keyframe];
export type Track = {
  /** The data-a name of the part (several parts may share it). */
  a: string;
  /** Times in seconds from the start of the loop, each with what the part looks like then. */
  frames: Frame[];
  /** The loop's length in seconds; the step's own by default. */
  cycle?: number;
  /** Seconds before the first loop starts. */
  delay?: number;
  /** How many times it plays; for ever by default. */
  iterations?: number;
};

export type Timeline = {
  cycle: number;
  tracks: Track[];
  /** A still moment, for reduced motion. */ pose: number;
};

/** The ease of most moves: a quick start and a long, soft landing. */
export const EASE = 'cubic-bezier(0.32, 0.72, 0, 1)';
const INOUT = 'cubic-bezier(0.45, 0, 0.25, 1)';
const LINEAR = 'linear';

/** A camera: zoom `z` with the desktop point (ox, oy) at the hero's top-left corner. */
const cam = (z: number, ox: number, oy: number): Keyframe => ({
  transform: `translate(${(-ox * z).toFixed(2)}px, ${(-oy * z).toFixed(2)}px) scale(${z})`,
});
const at = (x: number, y: number, s = 1): string =>
  `translate(${x.toFixed(2)}px, ${y.toFixed(2)}px) scale(${s})`;

/** The cameras of the illustrations. */
export const CAMERA = {
  desktop: cam(1, 0, 0),
  menu: cam(1.66, 178.8, 0),
  window: cam(1.353, 44.45, 41.24),
  thumb: cam(2.2, 0, 173.4),
  wall: cam(1.8, 100, 30),
  shortcuts: cam(1.145, 27.4, 0),
  folders: cam(2.36, 236.9, 0),
};

const o = (opacity: number, easing = EASE): Keyframe => ({ opacity, easing });

/** A part that shows at `on` (fading in over `fade` s) and hides at `off`. */
function shown(a: string, on: number, off: number, cycle: number, fade = 0.15, out = fade): Track {
  return {
    a,
    frames: [
      [0, o(0)],
      [on, o(0)],
      [on + fade, o(1)],
      [off, o(1)],
      [Math.min(cycle, off + out), o(0)],
      [cycle, o(0)],
    ],
  };
}

type Rect = { x: number; y: number; w: number; h: number };

/** The four edges and four handles of a selection drawn through the given rects. */
function frameTracks(name: string, steps: [number, Rect, string?][]): Track[] {
  const part = (suffix: string, place: (r: Rect) => string): Track => ({
    a: `${name}-${suffix}`,
    frames: steps.map(([t, r, easing]) => [t, { transform: place(r), easing: easing ?? EASE }]),
  });
  return [
    part('t', (r) => `translate(${r.x}px, ${r.y}px) scale(${Math.max(r.w, 0.01)}, 1)`),
    part('b', (r) => `translate(${r.x}px, ${r.y + r.h}px) scale(${Math.max(r.w, 0.01)}, 1)`),
    part('l', (r) => `translate(${r.x}px, ${r.y}px) scale(1, ${Math.max(r.h, 0.01)})`),
    part('r', (r) => `translate(${r.x + r.w}px, ${r.y}px) scale(1, ${Math.max(r.h, 0.01)})`),
    part('h0', (r) => `translate(${r.x}px, ${r.y}px)`),
    part('h1', (r) => `translate(${r.x + r.w}px, ${r.y}px)`),
    part('h2', (r) => `translate(${r.x}px, ${r.y + r.h}px)`),
    part('h3', (r) => `translate(${r.x + r.w}px, ${r.y + r.h}px)`),
  ];
}

/** Parts of the window, in its own px. */
const WINDOW: Rect = { x: 0, y: 0, w: 196, h: 134 };
const PICTURE: Rect = { x: 8.2, y: 19.2, w: 77.6, h: 54 };
const NOTE: Rect = { x: 92.4, y: 19.2, w: 96.3, h: 54 };

/** A clip-path that shows only `r` of the window (or nothing, at its middle). */
const reveal = (r: Rect | null, radius = 4): string =>
  r
    ? `inset(${r.y}px ${(196 - r.x - r.w).toFixed(1)}px ${(134 - r.y - r.h).toFixed(1)}px ${r.x}px round ${radius}px)`
    : 'inset(67px 98px 67px 98px round 4px)';

/** The capture on the canvas: whole, then cut by the layout's right edge. */
const ROUND = 'inset(0px 0px 0px 0px round 7px 7px 7px 7px)';
const CLIPPED = 'inset(0px 86.6px 0px 0px round 7px 0px 0px 7px)';

// ─── Step 1: the menu bar, a capture, its thumbnail, the canvas ─────────────────────────────────

const C1 = 16;
const step1: Timeline = {
  cycle: C1,
  pose: 2.4,
  tracks: [
    {
      a: 'camera',
      frames: [
        [0, { ...CAMERA.desktop, easing: INOUT }],
        [0.6, { ...CAMERA.desktop, easing: INOUT }],
        [0.88, { ...CAMERA.menu, easing: EASE }],
        [3.3, { ...CAMERA.menu, easing: 'cubic-bezier(0.2, 0.9, 0.1, 1)' }],
        [4.35, { ...CAMERA.window, easing: EASE }],
        [6.35, { ...CAMERA.window, easing: INOUT }],
        [6.85, { ...CAMERA.thumb, easing: EASE }],
        [15.5, { ...CAMERA.thumb, easing: LINEAR }],
        [15.52, { ...CAMERA.wall, easing: LINEAR }],
        [15.68, { ...CAMERA.wall, easing: INOUT }],
        [16, { ...CAMERA.desktop }],
      ],
    },
    // The window and the menu bar step aside while the canvas is up, and come back with the loop.
    {
      a: 'scenery',
      frames: [
        [0, o(1)],
        [15.5, o(1, LINEAR)],
        [15.52, o(0)],
        [15.84, o(0)],
        [16, o(1)],
      ],
    },
    shown('appIcon', 1.45, 3.3, C1, 0.05, 0.04),
    {
      a: 'pop1',
      frames: [
        [0, { opacity: 0, transform: 'translateY(-2px) scale(0.94)', easing: EASE }],
        [1.48, { opacity: 0, transform: 'translateY(-2px) scale(0.94)', easing: EASE }],
        [1.7, { opacity: 1, transform: 'translateY(0px) scale(1)', easing: EASE }],
        [3.22, { opacity: 1, transform: 'translateY(0px) scale(1)', easing: EASE }],
        [3.3, { opacity: 0, transform: 'translateY(0px) scale(1)', easing: EASE }],
        [16, { opacity: 0, transform: 'translateY(-2px) scale(0.94)' }],
      ],
    },
    {
      a: 'capBtn',
      frames: [
        [0, { backgroundColor: 'rgba(0, 0, 0, 0)' }],
        [2.95, { backgroundColor: 'rgba(0, 0, 0, 0)' }],
        [3.02, { backgroundColor: 'rgba(0, 0, 0, 0.08)' }],
        [3.3, { backgroundColor: 'rgba(0, 0, 0, 0.08)' }],
        [3.32, { backgroundColor: 'rgba(0, 0, 0, 0)' }],
        [16, { backgroundColor: 'rgba(0, 0, 0, 0)' }],
      ],
    },
    {
      a: 'dim',
      frames: [
        [0, o(0)],
        [3.3, o(0)],
        [3.44, o(1, INOUT)],
        [6.05, o(1, INOUT)],
        [6.5, o(0)],
        [16, o(0)],
      ],
    },
    {
      a: 'sel',
      frames: [
        [0, o(0)],
        [4.42, o(0)],
        [4.45, o(1)],
        [6.05, o(1)],
        [6.1, o(0)],
        [16, o(0)],
      ],
    },
    ...frameTracks('sel', [
      [0, { x: 0, y: 0, w: 0, h: 0 }],
      [4.45, { x: 0, y: 0, w: 2, h: 2 }, 'cubic-bezier(0.42, 0, 0.3, 1)'],
      [5.08, WINDOW],
      [16, WINDOW],
    ]),
    {
      a: 'pill',
      frames: [
        [0, { opacity: 0, transform: 'scale(0.94)', easing: EASE }],
        [5.25, { opacity: 0, transform: 'scale(0.94)', easing: EASE }],
        [5.42, { opacity: 1, transform: 'scale(1)', easing: EASE }],
        [6.05, { opacity: 1, transform: 'scale(1)', easing: EASE }],
        [6.1, { opacity: 0, transform: 'scale(1)', easing: EASE }],
        [16, { opacity: 0, transform: 'scale(0.94)' }],
      ],
    },
    {
      a: 'flash',
      frames: [
        [0, o(0)],
        [6.05, o(0)],
        [6.12, o(0.55)],
        [6.5, o(0)],
        [16, o(0)],
      ],
    },
    // The thumbnail: a card slides up behind, then the capture itself.
    {
      a: 'thumbBack',
      frames: [
        [0, { opacity: 0, transform: 'translateY(12px)', easing: EASE }],
        [6.72, { opacity: 0, transform: 'translateY(12px)', easing: EASE }],
        [7.0, { opacity: 1, transform: 'translateY(0px)', easing: EASE }],
        [8.8, { opacity: 1, transform: 'translateY(0px)', easing: EASE }],
        [9.0, { opacity: 0, transform: 'translateY(0px)', easing: EASE }],
        [16, { opacity: 0, transform: 'translateY(12px)' }],
      ],
    },
    {
      a: 'thumbFront',
      frames: [
        [0, { opacity: 0, transform: 'translateY(16px)', easing: EASE }],
        [6.85, { opacity: 0, transform: 'translateY(16px)', easing: LINEAR }],
        [6.95, { opacity: 1, transform: 'translateY(15px)', easing: INOUT }],
        [7.5, { opacity: 1, transform: 'translateY(0px)', easing: EASE }],
        [8.8, { opacity: 1, transform: 'translateY(0px)', easing: EASE }],
        [9.0, { opacity: 0, transform: 'translateY(0px)', easing: EASE }],
        [16, { opacity: 0, transform: 'translateY(16px)' }],
      ],
    },
    shown('edit', 7.85, 8.8, C1, 0.12),
    shown('show', 7.85, 8.8, C1, 0.12),
    {
      a: 'edit',
      frames: [
        [0, { backgroundColor: 'rgba(255, 255, 255, 0.9)' }],
        [8.45, { backgroundColor: 'rgba(255, 255, 255, 0.9)' }],
        [8.5, { backgroundColor: 'rgba(235, 235, 235, 0.95)' }],
        [8.8, { backgroundColor: 'rgba(235, 235, 235, 0.95)' }],
        [8.82, { backgroundColor: 'rgba(255, 255, 255, 0.9)' }],
        [16, { backgroundColor: 'rgba(255, 255, 255, 0.9)' }],
      ],
    },
    // The pointer: to the app's icon, a click; to Capture, a click; it drags the selection; it
    // finds the thumbnail and clicks Edit.
    {
      a: 'cursor',
      frames: [
        [0, { opacity: 1, transform: at(233.4, 170), easing: INOUT }],
        [0.6, { opacity: 1, transform: at(233.4, 170), easing: INOUT }],
        [0.95, { opacity: 1, transform: at(329, 39), easing: INOUT }],
        [1.28, { opacity: 1, transform: at(345.6, 4.6), easing: EASE }],
        [1.4, { opacity: 1, transform: at(345.6, 4.6, 0.82), easing: EASE }],
        [1.5, { opacity: 1, transform: at(345.6, 4.6), easing: EASE }],
        [1.92, { opacity: 1, transform: at(345.6, 4.6), easing: INOUT }],
        [2.85, { opacity: 1, transform: at(298, 89), easing: EASE }],
        [2.96, { opacity: 1, transform: at(298, 89, 0.82), easing: EASE }],
        [3.08, { opacity: 1, transform: at(298, 89), easing: EASE }],
        [3.3, { opacity: 1, transform: at(298, 89), easing: LINEAR }],
        [3.32, { opacity: 1, transform: at(112.4, 51.4, 1.55), easing: INOUT }],
        [
          4.42,
          { opacity: 1, transform: at(112.4, 51.4, 1.55), easing: 'cubic-bezier(0.4, 0, 0.3, 1)' },
        ],
        [4.78, { opacity: 1, transform: at(308, 185, 1.55), easing: EASE }],
        [5.15, { opacity: 1, transform: at(308, 185, 1.55), easing: EASE }],
        [5.3, { opacity: 0, transform: at(308, 185, 1.55), easing: LINEAR }],
        [7.72, { opacity: 0, transform: at(118, 227.8, 1.7), easing: EASE }],
        [7.8, { opacity: 1, transform: at(116, 227.8, 1.7), easing: INOUT }],
        [8.22, { opacity: 1, transform: at(48.8, 238.4, 1.7), easing: EASE }],
        [8.45, { opacity: 1, transform: at(48.8, 238.4, 1.7), easing: EASE }],
        [8.52, { opacity: 1, transform: at(48.8, 238.4, 1.45), easing: EASE }],
        [8.64, { opacity: 1, transform: at(48.8, 238.4, 1.7), easing: EASE }],
        [8.8, { opacity: 0, transform: at(48.8, 238.4, 1.7), easing: LINEAR }],
        [15.84, { opacity: 0, transform: at(233.4, 170), easing: EASE }],
        [16, { opacity: 1, transform: at(233.4, 170) }],
      ],
    },
    // The canvas of the desktop app: it opens over the thumbnail, the capture lands in its middle,
    // the view zooms in, a backdrop grows around the capture, then a layout around both.
    {
      a: 'canvas',
      frames: [
        [0, o(0)],
        [8.8, o(0, INOUT)],
        [9.1, o(1)],
        [15.55, o(1, INOUT)],
        [15.8, o(0)],
        [16, o(0)],
      ],
    },
    {
      a: 'canvasWin',
      frames: [
        [0, { transform: 'scale(1.18)', easing: EASE }],
        [8.8, { transform: 'scale(1.18)', easing: EASE }],
        [9.3, { transform: 'scale(1)', easing: INOUT }],
        [10.4, { transform: 'scale(1)', easing: INOUT }],
        [10.9, { transform: 'scale(1.3)', easing: EASE }],
        [16, { transform: 'scale(1.3)' }],
      ],
    },
    {
      a: 'capture',
      frames: [
        [0, { opacity: 0, transform: at(110, -90, 1.5), clipPath: ROUND, easing: EASE }],
        [8.8, { opacity: 0, transform: at(110, -90, 1.5), clipPath: ROUND, easing: EASE }],
        [9.05, { opacity: 0.35, transform: at(126.6, -62, 1.39), clipPath: ROUND, easing: INOUT }],
        [9.7, { opacity: 1, transform: at(173.4, 51, 0.547), clipPath: ROUND, easing: INOUT }],
        [10.4, { opacity: 1, transform: at(173.4, 51, 0.547), clipPath: ROUND, easing: INOUT }],
        [10.9, { opacity: 1, transform: at(150, 50, 0.75), clipPath: ROUND, easing: EASE }],
        [11.1, { opacity: 1, transform: at(141, 46, 0.834), clipPath: ROUND, easing: EASE }],
        [12.05, { opacity: 1, transform: at(141, 46, 0.834), clipPath: ROUND, easing: INOUT }],
        [12.42, { opacity: 1, transform: at(220, 42.4, 1.234), clipPath: CLIPPED, easing: EASE }],
        [16, { opacity: 1, transform: at(220, 42.4, 1.234), clipPath: CLIPPED }],
      ],
    },
    {
      a: 'backdrop',
      frames: [
        [0, { opacity: 0, transform: at(129.75, 34.4, 0.9), easing: EASE }],
        [10.75, { opacity: 0, transform: at(129.75, 34.4, 0.9), easing: EASE }],
        [11.08, { opacity: 1, transform: at(119.5, 26.6, 1), easing: EASE }],
        [12.05, { opacity: 1, transform: at(119.5, 26.6, 1), easing: INOUT }],
        [12.42, { opacity: 1, transform: at(93, 5.6, 1.277), easing: EASE }],
        [16, { opacity: 1, transform: at(93, 5.6, 1.277) }],
      ],
    },
    shown('logo', 12.22, 16, C1, 0.25),
    shown('lines', 12.28, 16, C1, 0.25),
  ],
};

// ─── Step 2: the shortcut is pressed, and the window is selected ────────────────────────────────

const C2 = 5.6;
const press = (a: string, on: number): Track => ({
  a,
  frames: [
    [0, o(0)],
    [on, o(0)],
    [on + 0.06, o(1)],
    [5.0, o(1)],
    [5.2, o(0)],
    [C2, o(0)],
  ],
});
const step2: Timeline = {
  cycle: C2,
  pose: 3,
  tracks: [
    shown('keys', 0.6, 5.0, C2, 0.25, 0.2),
    press('key0', 1.25),
    press('key1', 1.45),
    press('key2', 1.75),
    shown('sel', 1.85, 5.0, C2, 0.08, 0.2),
    ...frameTracks('sel', [
      [0, WINDOW],
      [C2, WINDOW],
    ]),
    shown('dim', 2.0, 5.0, C2, 0.15, 0.2),
    {
      a: 'pill',
      frames: [
        [0, { opacity: 0, transform: 'scale(0.94)', easing: EASE }],
        [2.35, { opacity: 0, transform: 'scale(0.94)', easing: EASE }],
        [2.5, { opacity: 1, transform: 'scale(1)', easing: EASE }],
        [5.0, { opacity: 1, transform: 'scale(1)', easing: EASE }],
        [5.2, { opacity: 0, transform: 'scale(1)', easing: EASE }],
        [C2, { opacity: 0, transform: 'scale(0.94)' }],
      ],
    },
    {
      a: 'scenery',
      frames: [
        [0, o(1)],
        [5.0, o(1)],
        [5.2, o(0)],
        [5.3, o(0)],
        [5.5, o(1)],
        [C2, o(1)],
      ],
    },
  ],
};

// ─── Step 3: the pointer finds areas by itself, and one is picked ────────────────────────────────

const C3 = 5.4;
/** Where the pointer rests over the picture and over the note, in the desktop's px. */
const OVER_PICTURE = [150, 92] as const;
const OVER_NOTE = [226, 84] as const;
/** The pointer of step 3 is drawn larger, as it is in the reference's close-up. */
const S3 = 1.58;

function step3(auto: boolean, enhanced: boolean): Timeline {
  const cursor = (x: number, y: number, s = S3) => ({ opacity: 1, transform: at(x, y, s) });
  const intro: Track = {
    a: 'dim',
    iterations: 1,
    cycle: 0.3,
    frames: [
      [0, o(0)],
      [0.08, o(0)],
      [0.3, o(1)],
    ],
  };
  if (!auto) {
    // Without automatic selection, the pointer drags a selection by hand over the note.
    const [x, y] = [112 + NOTE.x, 51 + NOTE.y];
    return {
      cycle: C3,
      pose: 2.5,
      tracks: [
        intro,
        {
          a: 'cursor',
          frames: [
            [0, { ...cursor(...OVER_PICTURE), easing: INOUT }],
            [0.6, { ...cursor(x, y), easing: EASE }],
            [0.9, { ...cursor(x, y), easing: 'cubic-bezier(0.4, 0, 0.3, 1)' }],
            [1.55, { ...cursor(x + NOTE.w, y + NOTE.h), easing: EASE }],
            [3.6, { ...cursor(x + NOTE.w, y + NOTE.h), easing: INOUT }],
            [4.6, { ...cursor(...OVER_PICTURE), easing: EASE }],
            [C3, cursor(...OVER_PICTURE)],
          ],
        },
        shown('sel', 0.88, 3.6, C3, 0.04, 0.15),
        ...frameTracks('sel', [
          [0, { x: NOTE.x, y: NOTE.y, w: 0, h: 0 }],
          [0.9, { x: NOTE.x, y: NOTE.y, w: 1, h: 1 }, 'cubic-bezier(0.4, 0, 0.3, 1)'],
          [1.55, NOTE],
          [C3, NOTE],
        ]),
        {
          a: 'lit',
          frames: [
            [0, { opacity: 0 }],
            [C3, { opacity: 0 }],
          ],
        },
        {
          a: 'hov',
          frames: [
            [0, { opacity: 0 }],
            [C3, { opacity: 0 }],
          ],
        },
      ],
    };
  }
  // Without enhanced detection, the note isn't found on its own: over it, the whole window is.
  const second = enhanced ? NOTE : WINDOW;
  const hover = (r: Rect, easing = EASE): Keyframe => ({ clipPath: reveal(r), easing });
  return {
    cycle: C3,
    pose: 4.2,
    tracks: [
      intro,
      {
        a: 'cursor',
        frames: [
          [0, { ...cursor(...OVER_PICTURE), easing: INOUT }],
          [0.6, { ...cursor(146, 89), easing: INOUT }],
          [1.2, { ...cursor(152, 93), easing: INOUT }],
          [1.8, { ...cursor(148, 90.5), easing: INOUT }],
          [1.95, { ...cursor(148, 90.5), easing: INOUT }],
          [2.25, { ...cursor(...OVER_NOTE), easing: EASE }],
          [3.75, { ...cursor(...OVER_NOTE), easing: EASE }],
          [3.83, { ...cursor(OVER_NOTE[0], OVER_NOTE[1], S3 * 0.84), easing: EASE }],
          [3.95, { ...cursor(...OVER_NOTE), easing: EASE }],
          [5.0, { ...cursor(...OVER_NOTE), easing: INOUT }],
          [5.3, { ...cursor(...OVER_PICTURE), easing: EASE }],
          [C3, cursor(...OVER_PICTURE)],
        ],
      },
      {
        a: 'lit',
        frames: [
          [0, { opacity: 1, ...hover(PICTURE, LINEAR) }],
          [2.12, { opacity: 1, ...hover(PICTURE, EASE) }],
          [2.2, { opacity: 1, ...hover(second, LINEAR) }],
          [5.2, { opacity: 1, ...hover(second, EASE) }],
          [5.28, { opacity: 1, ...hover(PICTURE, LINEAR) }],
          [C3, { opacity: 1, ...hover(PICTURE) }],
        ],
      },
      {
        a: 'hov',
        frames: [
          [0, { opacity: 1 }],
          [C3, { opacity: 1 }],
        ],
      },
      ...frameTracks('hov', [
        [0, PICTURE, LINEAR],
        [2.12, PICTURE, EASE],
        [2.2, second, LINEAR],
        [5.2, second, EASE],
        [5.28, PICTURE, LINEAR],
        [C3, PICTURE],
      ]),
      shown('sel', 3.85, 4.8, C3, 0.03, 0.05),
      ...frameTracks('sel', [
        [0, second],
        [C3, second],
      ]),
    ],
  };
}

// ─── Step 4: the popover's folders, and a capture dropping into one ─────────────────────────────

const C4 = 4.8;
/** The capture's row, and the middle of each folder, in the popover's px. */
const ROW = [11, 57.5] as const;
const SLOTS = [
  [19.6, 36.7],
  [54, 36.7],
  [88.3, 36.7],
] as const;

function step4(slot: number): Timeline {
  const [fx, fy] = SLOTS[Math.max(0, Math.min(2, slot))];
  const drag = (x: number, y: number, s: number, opacity: number, easing = EASE): Keyframe => ({
    opacity,
    transform: `translate(${x}px, ${y}px) scale(${s})`,
    easing,
  });
  return {
    cycle: C4,
    pose: 3,
    tracks: [
      {
        a: 'camera',
        iterations: 1,
        cycle: 0.45,
        frames: [
          [0, { ...CAMERA.window, easing: INOUT }],
          [0.12, { ...CAMERA.window, easing: INOUT }],
          [0.45, CAMERA.folders],
        ],
      },
      {
        a: 'pop4',
        iterations: 1,
        cycle: 0.4,
        frames: [
          [0, { opacity: 0, transform: 'translateY(-2px) scale(0.95)', easing: EASE }],
          [0.14, { opacity: 0, transform: 'translateY(-2px) scale(0.95)', easing: EASE }],
          [0.4, { opacity: 1, transform: 'translateY(0px) scale(1)' }],
        ],
      },
      {
        a: 'appIcon',
        iterations: 1,
        cycle: 0.2,
        frames: [
          [0, o(0)],
          [0.14, o(0)],
          [0.2, o(1)],
        ],
      },
      {
        a: 'drag',
        delay: 1.8,
        frames: [
          [0, drag(ROW[0], ROW[1], 0.9, 0)],
          [0.25, drag(ROW[0], ROW[1], 1, 1)],
          [0.35, drag(ROW[0], ROW[1] - 1, 1.15, 1, INOUT)],
          [0.85, drag(fx, fy, 1.6, 1)],
          [1.25, drag(fx, fy, 1.6, 1)],
          [1.5, drag(fx, fy + 2, 0.5, 0, LINEAR)],
          [1.6, drag(ROW[0], ROW[1], 0.9, 0)],
          [C4, drag(ROW[0], ROW[1], 0.9, 0)],
        ],
      },
      {
        a: 'target',
        delay: 1.8,
        frames: [
          [0, o(0)],
          [0.72, o(0)],
          [0.85, o(1)],
          [1.45, o(1)],
          [1.75, o(0)],
          [C4, o(0)],
        ],
      },
    ],
  };
}

export function timeline(
  step: number,
  options: { auto: boolean; enhanced: boolean; slot: number },
): Timeline {
  if (step === 0) return step1;
  if (step === 1) return step2;
  if (step === 2) return step3(options.auto, options.enhanced);
  return step4(options.slot);
}
```

### menubar-onboarding/art.tsx

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

'use client';

import type { ReactNode } from 'react';
import {
  AppMark,
  BatteryIcon,
  BriefcaseIcon,
  BulbIcon,
  CameraIcon,
  EditIcon,
  EyeIcon,
  FolderHeartIcon,
  FolderIcon,
  GearIcon,
  HomeIcon,
  ReturnIcon,
  RingMark,
  SearchIcon,
  SwitchesIcon,
  VideoIcon,
  WifiIcon,
} from './icons';
import styles from './hero.module.css';

/*
 * The drawings the illustrations are made of, all in the desktop's own px (the illustration
 * scales the whole desktop to the panel's width): the menu bar, the window being captured, the
 * menu-bar app's popover in its two states, the capture's floating thumbnail and the canvas of
 * the desktop app. They are decorative (the hero that holds them is aria-hidden); every part that
 * moves carries a data-a name, which the timelines in hero.tsx animate.
 */

/** A folder of the popover, and of the panel's suggested folders. */
export type FolderKind = 'ideas' | 'inspiration' | 'work' | 'custom';
export type HeroFolder = { id: string; name: string; kind: FolderKind; files: number };

export function FolderGlyph({ kind, className }: { kind: FolderKind; className?: string }) {
  if (kind === 'ideas') return <BulbIcon className={className} />;
  if (kind === 'inspiration') return <FolderHeartIcon className={className} />;
  if (kind === 'work') return <BriefcaseIcon className={className} />;
  return <FolderIcon className={className} />;
}

/** The menu bar across the top of the desktop. */
export function MenuBar() {
  return (
    <div className={styles.menubar} data-part="hero-menubar">
      <span className={styles.menuApp}>Files</span>
      {['File', 'Edit', 'View', 'Go', 'Window', 'Help'].map((item) => (
        <span key={item} className={styles.menuItem} data-item={item}>
          {item}
        </span>
      ))}
      <span className={styles.appIcon}>
        <span className={styles.appLit} data-a="appIcon" />
        <AppMark className={styles.appGlyph} />
      </span>
      <BatteryIcon className={`${styles.status} ${styles.battery}`} />
      <WifiIcon className={`${styles.status} ${styles.wifi}`} />
      <SearchIcon className={`${styles.status} ${styles.search}`} />
      <SwitchesIcon className={`${styles.status} ${styles.switches}`} />
      <span className={styles.clock}>Tue 9:41</span>
    </div>
  );
}

/** The window being captured: a little moodboard page. */
export function MiniWindow({ className = '' }: { className?: string }) {
  return (
    <div className={`${styles.window} ${className}`}>
      <span className={styles.dots}>
        <i />
        <i />
        <i />
      </span>
      <span className={styles.picture} />
      <span className={styles.note}>
        <b>A little inspiration</b>
        <small>Make room for your next idea.</small>
        <i />
        <i />
      </span>
      {['Explore', 'Collect', 'Create'].map((label) => (
        <span key={label} className={styles.tile}>
          <i />
          <small>{label}</small>
          <em />
        </span>
      ))}
    </div>
  );
}

function PopoverHead() {
  return (
    <div className={styles.popHead}>
      <HomeIcon className={styles.popHome} />
      <span className={styles.segments}>
        <b>All</b>
        <span>Captures</span>
        <span>Exports</span>
      </span>
      <GearIcon className={styles.popGear} />
    </div>
  );
}

function Rows({ count }: { count: number }) {
  return (
    <span className={styles.rows}>
      {Array.from({ length: count }, (_, index) => (
        <span key={index} className={styles.row}>
          <i />
          <b />
          <em />
        </span>
      ))}
    </span>
  );
}

/** The popover in the first illustration: the latest captures, and the buttons to take one. */
export function CapturesPopover() {
  return (
    <div className={`${styles.popover} ${styles.popCaptures}`} data-a="pop1">
      <PopoverHead />
      <Rows count={3} />
      <span className={styles.popFoot}>
        <span className={styles.popButton} data-a="capBtn">
          <CameraIcon className={styles.popIcon} />
          Capture
        </span>
        <span className={styles.popButton}>
          <VideoIcon className={styles.popIcon} />
          Record
        </span>
        <span className={styles.popSquare}>
          <FolderIcon className={styles.popIcon} />
        </span>
      </span>
    </div>
  );
}

/** The popover in the last illustration: the folders, a capture dropping into one of them. */
export function FoldersPopover({ folders, ai }: { folders: HeroFolder[]; ai: boolean }) {
  const target = folders.findIndex((folder) => folder.kind === 'inspiration');
  const into = target >= 0 ? target : Math.min(1, folders.length - 1);
  return (
    <div className={`${styles.popover} ${styles.popFolders}`} data-a="pop4">
      <PopoverHead />
      <span className={styles.folders} data-count={folders.length}>
        {folders.slice(0, 3).map((folder, index) => (
          <span key={folder.id} className={styles.folder} data-kind={folder.kind}>
            <span className={styles.folderPapers} />
            {index === into && <span className={styles.folderGlow} data-a="target" />}
            <span className={styles.folderBody}>
              <FolderGlyph kind={folder.kind} className={styles.folderGlyph} />
              <b>{folder.name}</b>
              <small>
                {folder.files} {folder.files === 1 ? 'file' : 'files'}
              </small>
            </span>
          </span>
        ))}
      </span>
      <Rows count={2} />
      {folders.length > 0 && (
        <span className={styles.dragged} data-a="drag" data-slot={Math.min(into, 2)}>
          <span className={styles.draggedPicture} />
          {ai && <span className={styles.spark} />}
        </span>
      )}
    </div>
  );
}

/** The keys of the shortcut, pressed one after the other. */
export function Keycaps({ keys }: { keys: string[] }) {
  return (
    <div className={styles.keys} data-a="keys">
      {keys.map((key, index) => (
        <span key={index} className={styles.key}>
          <span className={styles.keyDown} data-a={`key${index}`} />
          <span className={styles.keyFace}>{key}</span>
        </span>
      ))}
    </div>
  );
}

/** A selection's frame: four edges and four handles, each moved by its own transform. */
export function Selection({ name }: { name: string }) {
  return (
    <div className={styles.selection} data-a={name}>
      <i className={styles.edgeTop} data-a={`${name}-t`} />
      <i className={styles.edgeLeft} data-a={`${name}-l`} />
      <i className={styles.edgeRight} data-a={`${name}-r`} />
      <i className={styles.edgeBottom} data-a={`${name}-b`} />
      <b className={styles.handle} data-a={`${name}-h0`} />
      <b className={styles.handle} data-a={`${name}-h1`} />
      <b className={styles.handle} data-a={`${name}-h2`} />
      <b className={styles.handle} data-a={`${name}-h3`} />
    </div>
  );
}

/** The bar under a selection: return captures, escape cancels. */
export function CapturePill() {
  return (
    <div className={styles.pill} data-a="pill">
      <span className={styles.pillKey}>
        <ReturnIcon className={styles.pillIcon} />
      </span>
      Capture
      <span className={styles.pillKey}>esc</span>
      Cancel
    </div>
  );
}

/** The capture's thumbnail, floating in a corner of the screen, and its two actions. */
export function Thumbnail() {
  return (
    <div className={styles.thumb} data-a="thumb">
      <span className={styles.thumbBack} data-a="thumbBack" />
      <span className={styles.thumbFront} data-a="thumbFront">
        <MiniWindow className={styles.thumbWindow} />
      </span>
      <span className={styles.thumbAction} data-a="edit">
        <EditIcon className={styles.thumbIcon} />
        Edit
      </span>
      <span className={`${styles.thumbAction} ${styles.thumbShow}`} data-a="show">
        <EyeIcon className={styles.thumbIcon} />
        Show
      </span>
    </div>
  );
}

/** The desktop app's canvas: the capture lands on it, gets a backdrop, then a layout. */
export function Canvas() {
  return (
    <div className={styles.canvas} data-a="canvas">
      <span className={styles.canvasWin} data-a="canvasWin">
        <span className={styles.canvasSide} />
      </span>
      <span className={styles.backdrop} data-a="backdrop">
        <span className={styles.logo} data-a="logo">
          <RingMark className={styles.logoMark} />
        </span>
        <span className={styles.lines} data-a="lines">
          <i />
          <i />
          <em />
          <em />
        </span>
      </span>
      <span className={styles.capture} data-a="capture">
        <MiniWindow />
      </span>
    </div>
  );
}

/** A layer of the hero that only some illustrations show. */
export function Only({ when, children }: { when: boolean; children: ReactNode }) {
  return when ? <>{children}</> : null;
}
```

### menubar-onboarding/hero.module.css

```css
/* The hero's illustrations (hero.tsx, art.tsx): a small desktop in its own px, scaled to the
   panel by --k (the panel's width over 448). Every part sits at a fixed place on the desktop;
   the parts that move start where their timeline starts (timelines.ts), hidden until it shows
   them. Colours are light-dark() tokens: at night the desktop is a dusk and its windows dark.

   Memory. The hero is one clipped box. The camera, the canvas and each moving part are moved and
   faded by transforms and opacity only; the wallpaper is gradients, painted once. */

.hero {
  --wall-1: light-dark(#f7d050, #b8752a);
  --wall-2: light-dark(#f6b23a, #a9581f);
  --wall-3: light-dark(#f4a02e, #8f3f1d);
  --wall-4: light-dark(#e2643f, #6a2620);
  --wall-5: light-dark(#d4431f, #4d1c22);
  --wall-sun: light-dark(#ffe58a, #e7a64a);
  --wall-rim: light-dark(#fbe27a, #c99a4c);
  --wall-sheen: light-dark(#f9f07c, #d9a24a);
  --wall-sky: light-dark(#5fbae6, #3d6796);
  --wall-blue: light-dark(#3a86c8, #24497f);
  --menubar: light-dark(rgb(255 255 255 / 0.55), rgb(30 30 32 / 0.55));
  --menu-ink: light-dark(#191919, #f1f1f1);
  --win: light-dark(#fafafa, #2b2b2e);
  --win-edge: light-dark(rgb(0 0 0 / 0.06), rgb(255 255 255 / 0.08));
  --win-line: light-dark(#ebebeb, #3a3a3d);
  --win-dot: light-dark(#d0d0d0, #58585c);
  --win-fill: light-dark(#efefef, #38383b);
  --win-tile: light-dark(#fdfdfd, #313134);
  --win-bar: light-dark(#dcdcdc, #4a4a4e);
  --win-ink: light-dark(#222, #ececec);
  --win-muted: light-dark(#8c8c8c, #9a9a9e);
  --pop: light-dark(rgb(251 251 251 / 0.98), rgb(40 40 43 / 0.98));
  --pop-fill: light-dark(#ececec, #3a3a3d);
  --pop-chip: light-dark(#fff, #57575b);
  --pop-ink: light-dark(#333, #e6e6e6);
  --pop-row: light-dark(#e7e7e7, #3c3c40);
  --pop-bar: light-dark(#d5d5d5, #4d4d51);
  --pop-bar-2: light-dark(#e6e6e6, #3f3f43);
  --key: light-dark(#fff, #48484c);
  --key-edge: light-dark(#c7c7c7, #232325);
  --key-down: light-dark(#cfcfcf, #2c2c2f);
  --key-ink: light-dark(#151515, #f2f2f2);
  --dim: rgb(0 0 0 / 0.35);
  --select: light-dark(#161616, #f4f4f4);
  --select-fill: light-dark(#fff, #1d1d1f);
  --hover: light-dark(rgb(30 30 30 / 0.55), rgb(255 255 255 / 0.6));
  --pill: light-dark(rgb(255 255 255 / 0.94), rgb(44 44 47 / 0.94));
  --pill-ink: light-dark(#3a3a3a, #e4e4e4);
  --pill-key: light-dark(#bdbdbd, #6a6a6e);
  --canvas: light-dark(#f9f9f9, #1f1f21);
  --canvas-dot: light-dark(#c4c4c4, #444448);
  --canvas-side: light-dark(#f2f2f2, #252528);
  --canvas-edge: light-dark(#e3e3e3, #333336);
  --card-bg: var(--sheet, #fdfdfd);
  --ideas: light-dark(#f2a31b, #f6b443);
  --ideas-bg: light-dark(#fdf4e6, #3b3226);
  --inspiration: light-dark(#9b55e0, #b98af0);
  --inspiration-bg: light-dark(#f4effd, #342d42);
  --work: light-dark(#3b7ff0, #6ea2f6);
  --work-bg: light-dark(#ebf2fd, #26313f);
  --custom: light-dark(#6d6d72, #a6a6ab);
  --custom-bg: light-dark(#f1f1f2, #333336);
  position: relative;
  width: 100%;
  height: calc(210px * var(--k, 1));
  overflow: hidden;
  overflow: clip;
  pointer-events: none;
  user-select: none;
}

.stage {
  position: absolute;
  top: 0;
  left: 0;
  width: 448px;
  height: 210px;
  overflow: hidden;
  transform: scale(var(--k, 1));
  transform-origin: 0 0;
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    sans-serif;
  line-height: 1;
  color: var(--win-ink);
}

/* ─── The desktop, seen through the camera ─────────────────────────────────────────────────── */

.camera {
  position: absolute;
  top: 0;
  left: 0;
  width: 448px;
  height: 280px;
  transform-origin: 0 0;
  will-change: transform;
}
.stage[data-scene='2'] .camera {
  transform: translate(-31.37px, 0px) scale(1.145);
}
.stage[data-scene='3'] .camera,
.stage[data-scene='4'] .camera {
  transform: translate(-60.14px, -55.8px) scale(1.353);
}

/* A dune-like afternoon: a warm sweep of yellow to red, a blue sky in the top right corner and
   pale folds between. */
.wall {
  position: absolute;
  top: -80px;
  left: -80px;
  width: 608px;
  height: 440px;
  overflow: hidden;
  background:
    radial-gradient(70px 50px at 34% 24%, var(--wall-sheen) 0%, transparent 100%),
    linear-gradient(90deg, var(--wall-1) 2%, transparent 30%),
    linear-gradient(
      180deg,
      var(--wall-1) 18%,
      var(--wall-2) 30%,
      var(--wall-3) 48%,
      var(--wall-3) 64%,
      var(--wall-4) 100%
    );
}
/* The folds: ellipses with crisp edges, some turned. In order: the pale rim of the sky, the sky
   at the top left, the red fold on the right, the blue one over it, and its yellow edge. */
.wall i {
  position: absolute;
  border-radius: 50%;
}
.wall i:nth-child(6) {
  left: 205px;
  top: 180px;
  width: 50px;
  height: 230px;
  rotate: -50deg;
  background: radial-gradient(closest-side, var(--wall-4) 0%, var(--wall-4) 55%, transparent 100%);
}
.wall i:nth-child(7) {
  left: 185px;
  top: 195px;
  width: 30px;
  height: 200px;
  rotate: -50deg;
  background: radial-gradient(closest-side, var(--wall-sun) 0%, transparent 100%);
}
.wall i:nth-child(8) {
  left: 35px;
  top: 270px;
  width: 70px;
  height: 120px;
  rotate: -30deg;
  background: radial-gradient(closest-side, var(--wall-blue) 50%, transparent 100%);
}
.wall i:nth-child(1) {
  left: -150px;
  top: -124px;
  width: 400px;
  height: 288px;
  background: radial-gradient(closest-side, var(--wall-rim) 78%, transparent 100%);
}
.wall i:nth-child(2) {
  left: -110px;
  top: -94px;
  width: 320px;
  height: 228px;
  background: radial-gradient(
    closest-side,
    var(--wall-sky) 0%,
    var(--wall-sky) 94%,
    transparent 100%
  );
}
.wall i:nth-child(3) {
  left: 400px;
  top: 80px;
  width: 70px;
  height: 260px;
  rotate: 28deg;
  background: radial-gradient(closest-side, var(--wall-4) 45%, transparent 100%);
}
.wall i:nth-child(4) {
  left: 469px;
  top: -10px;
  width: 80px;
  height: 196px;
  rotate: 31deg;
  background: radial-gradient(closest-side, var(--wall-rim) 80%, transparent 100%);
}
.wall i:nth-child(5) {
  left: 477px;
  top: 0px;
  width: 64px;
  height: 176px;
  rotate: 31deg;
  background: radial-gradient(
    closest-side,
    var(--wall-blue) 0%,
    var(--wall-blue) 92%,
    transparent 100%
  );
}

.scenery {
  position: absolute;
  inset: 0;
}

.menubar {
  position: absolute;
  top: 0;
  left: 0;
  width: 448px;
  height: 8.3px;
  background: var(--menubar);
  color: var(--menu-ink);
  font-size: 4.8px;
}
.menuApp,
.menuItem,
.clock {
  position: absolute;
  top: 1.9px;
  white-space: nowrap;
}
.menuApp {
  left: 18px;
  font-weight: 700;
}
.menuItem[data-item='File'] {
  left: 38.5px;
}
.menuItem[data-item='Edit'] {
  left: 52.8px;
}
.menuItem[data-item='View'] {
  left: 67px;
}
.menuItem[data-item='Go'] {
  left: 84px;
}
.menuItem[data-item='Window'] {
  left: 97px;
}
.menuItem[data-item='Help'] {
  left: 120px;
}
.clock {
  left: 424px;
  font-weight: 500;
}
.appIcon {
  position: absolute;
  top: 0.15px;
  left: 340.2px;
  width: 10.4px;
  height: 8px;
  border-radius: 2px;
  display: grid;
  place-items: center;
}
/* The highlight behind the icon is what fades: the icon itself stays. */
.appLit {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: light-dark(rgb(0 0 0 / 0.55), rgb(255 255 255 / 0.35));
  opacity: 0;
}
.appGlyph {
  position: relative;
  width: 6.2px;
  height: 6.2px;
}
.status {
  position: absolute;
  top: 0.9px;
  width: 6.4px;
  height: 6.4px;
}
.battery {
  left: 359.4px;
  width: 7.4px;
}
.wifi {
  left: 376.2px;
}
.search {
  left: 392.4px;
  width: 5px;
}
.switches {
  left: 406.2px;
}

/* The window being captured, 196 × 134, drawn the same in the desktop, its highlight, the
   thumbnail and the canvas. */
.windowAt {
  position: absolute;
  top: 51px;
  left: 112px;
  width: 196px;
  height: 134px;
}
.stage[data-scene='2'] .windowAt {
  top: 88px;
}
.window {
  position: absolute;
  inset: 0;
  width: 196px;
  height: 134px;
  border-radius: 6.8px;
  background: var(--win);
  box-shadow:
    0 0 0 0.5px var(--win-edge),
    0 3px 10px rgb(0 0 0 / 0.1);
  overflow: hidden;
}
.dots {
  position: absolute;
  top: 3.7px;
  left: 5.3px;
  display: flex;
  gap: 3px;
}
.dots i {
  width: 3.6px;
  height: 3.6px;
  border-radius: 50%;
  background: var(--win-dot);
}
.window::after {
  content: '';
  position: absolute;
  top: 11.4px;
  left: 0;
  right: 0;
  height: 0.6px;
  background: var(--win-line);
}
.picture,
.draggedPicture {
  background:
    linear-gradient(
      90deg,
      rgb(255 255 255 / 0) 0%,
      rgb(255 255 255 / 0.18) 34%,
      rgb(255 255 255 / 0) 35%,
      rgb(255 255 255 / 0) 60%,
      rgb(255 255 255 / 0.16) 61%,
      rgb(255 255 255 / 0) 80%
    ),
    linear-gradient(180deg, rgb(255 255 255 / 0.12), rgb(0 0 0 / 0.05)),
    linear-gradient(
      90deg,
      #3b48b2 0%,
      #5753c6 16%,
      #8a5bcd 33%,
      #b864b4 46%,
      #db6f8f 60%,
      #e8846f 76%,
      #eea27f 100%
    );
}
.picture {
  position: absolute;
  top: 19.2px;
  left: 8.2px;
  width: 77.6px;
  height: 54px;
  border-radius: 3.6px;
}
.note {
  position: absolute;
  top: 19.2px;
  left: 92.4px;
  width: 96.3px;
  height: 54px;
  border-radius: 4px;
  background: var(--win-fill);
}
.note b {
  position: absolute;
  top: 10.4px;
  left: 7.7px;
  font-size: 7.6px;
  font-weight: 600;
  white-space: nowrap;
}
.note small {
  position: absolute;
  top: 23.4px;
  left: 7.7px;
  font-size: 4.9px;
  color: var(--win-muted);
  white-space: nowrap;
}
.note i {
  position: absolute;
  left: 7.7px;
  top: 36.3px;
  width: 65px;
  height: 2.2px;
  border-radius: 1.1px;
  background: var(--win-bar);
}
.note i + i {
  top: 42.9px;
  width: 48.4px;
}
.tile {
  position: absolute;
  top: 81.4px;
  left: 8.2px;
  width: 55.6px;
  height: 40.8px;
  border-radius: 3.6px;
  background: var(--win-tile);
  box-shadow: 0 0 0 0.5px var(--win-line);
}
.tile:nth-of-type(5) {
  left: 70.4px;
}
.tile:nth-of-type(6) {
  left: 132.6px;
}
.tile i {
  position: absolute;
  top: 3.9px;
  left: 3.9px;
  right: 3.9px;
  height: 18.2px;
  border-radius: 2.6px;
  background: var(--win-fill);
}
.tile small {
  position: absolute;
  top: 26.6px;
  left: 3.9px;
  font-size: 4.7px;
  color: light-dark(#a8a8a8, #85858a);
}
.tile em {
  position: absolute;
  top: 34.3px;
  left: 3.9px;
  width: 31px;
  height: 1.5px;
  border-radius: 1px;
  background: var(--win-line);
}

/* The screen dims while a capture is being taken; in step 3 the area the pointer finds is lit
   through it (a copy of the window, clipped to that area). */
.dim {
  position: absolute;
  top: -80px;
  left: -80px;
  width: 608px;
  height: 440px;
  background: var(--dim);
  opacity: 0;
}
.lit {
  position: absolute;
  inset: 0;
  opacity: 0;
  clip-path: inset(67px 98px 67px 98px round 4px);
}
.lit .window {
  box-shadow: none;
}
.flash {
  position: absolute;
  inset: 0;
  border-radius: 6.8px;
  background: #fff;
  opacity: 0;
}

.selection {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.edgeTop,
.edgeBottom,
.edgeLeft,
.edgeRight {
  position: absolute;
  top: 0;
  left: 0;
  background: var(--select);
  transform-origin: 0 0;
}
.edgeTop,
.edgeBottom {
  width: 1px;
  height: 0.75px;
  margin-top: -0.375px;
}
.edgeLeft,
.edgeRight {
  width: 0.75px;
  height: 1px;
  margin-left: -0.375px;
}
.handle {
  position: absolute;
  top: -2.3px;
  left: -2.3px;
  width: 4.6px;
  height: 4.6px;
  box-sizing: border-box;
  border: 0.6px solid var(--select);
  background: var(--select-fill);
}
.selection[data-a='hov'] .handle {
  display: none;
}
.selection[data-a='hov'] .edgeTop,
.selection[data-a='hov'] .edgeBottom,
.selection[data-a='hov'] .edgeLeft,
.selection[data-a='hov'] .edgeRight {
  background: var(--hover);
}

.pill {
  position: absolute;
  top: 61px;
  left: 63.7px;
  height: 12px;
  padding: 0 2.6px;
  display: flex;
  align-items: center;
  gap: 2.4px;
  border-radius: 3.6px;
  background: var(--pill);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
  color: var(--pill-ink);
  font-size: 4.6px;
  white-space: nowrap;
  opacity: 0;
}
.pillKey {
  display: inline-grid;
  place-items: center;
  height: 6.4px;
  min-width: 7px;
  padding: 0 1.1px;
  box-sizing: border-box;
  border: 0.45px solid var(--pill-key);
  border-radius: 1.6px;
  font-size: 4px;
}
.pillKey + .pillKey,
.pillKey:not(:first-child) {
  margin-left: 2.4px;
}
.pillIcon {
  width: 4.4px;
  height: 4.4px;
}

/* The keys of the shortcut, above the window in step 2. */
.keys {
  position: absolute;
  top: 34px;
  left: 168.3px;
  display: flex;
  gap: 6.5px;
  opacity: 0;
}
.key {
  position: relative;
  width: 32.3px;
  height: 32.3px;
  border-radius: 5.4px;
  background: var(--key);
  box-shadow:
    0 1.8px 0 var(--key-edge),
    0 2.4px 4px rgb(0 0 0 / 0.12);
}
.keyDown {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--key-down);
  opacity: 0;
}
.keyFace {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--key-ink);
  font-size: 13.6px;
  font-weight: 500;
}

/* The app's popover, under its icon in the menu bar. */
.popover {
  position: absolute;
  top: 12.2px;
  left: 278.2px;
  width: 107.8px;
  height: 107.5px;
  border-radius: 7px;
  background: var(--pop);
  box-shadow:
    0 0 0 0.5px var(--win-edge),
    0 4px 14px rgb(0 0 0 / 0.14);
  color: var(--pop-ink);
  opacity: 0;
  transform-origin: 67px 0;
}
.popHead {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 20px;
}
.popHome,
.popGear {
  position: absolute;
  top: 6.2px;
  width: 9.4px;
  height: 9.4px;
}
.popHome {
  left: 6px;
}
.popGear {
  left: 91.6px;
}
.segments {
  position: absolute;
  top: 7.4px;
  left: 24.9px;
  width: 59.2px;
  height: 9.4px;
  padding: 0.7px;
  box-sizing: border-box;
  display: flex;
  border-radius: 2.6px;
  background: var(--pop-fill);
  font-size: 4.3px;
}
.segments > * {
  flex: 1 1 auto;
  display: grid;
  place-items: center;
  font-weight: 400;
}
.segments > b {
  flex: 0 0 13px;
  border-radius: 2px;
  background: var(--pop-chip);
  box-shadow: 0 0.3px 0.8px rgb(0 0 0 / 0.12);
  font-weight: 600;
}
.segments > span + span {
  border-left: 0.4px solid light-dark(#d0d0d0, #4d4d51);
}
.rows {
  position: absolute;
  top: 22px;
  left: 4.2px;
  right: 4.2px;
}
.popFolders .rows {
  top: 51.7px;
}
.row {
  position: relative;
  display: block;
  height: 16.9px;
}
.row i {
  position: absolute;
  top: 0;
  left: 0;
  width: 13.9px;
  height: 11.4px;
  border-radius: 2.4px;
  background: var(--pop-row);
}
.row b {
  position: absolute;
  top: 3px;
  left: 17.1px;
  width: 59.2px;
  height: 2.2px;
  border-radius: 1.1px;
  background: var(--pop-bar);
}
.row em {
  position: absolute;
  top: 7px;
  left: 17.1px;
  width: 38px;
  height: 1.5px;
  border-radius: 1px;
  background: var(--pop-bar-2);
}
.popFoot {
  position: absolute;
  top: 85.8px;
  left: 3.6px;
  right: 3.6px;
  height: 15.7px;
  display: flex;
  gap: 6.8px;
}
.popButton,
.popSquare {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.8px;
  height: 15.7px;
  border-radius: 3.4px;
  font-size: 6.3px;
  color: light-dark(#6f6f6f, #b5b5b9);
}
.popButton {
  width: 37.3px;
}
.popButton + .popButton {
  width: 36.2px;
  border-left: 0.4px solid var(--pop-row);
  border-radius: 0;
}
.popSquare {
  width: 12.8px;
  margin-left: auto;
}
.popIcon {
  width: 6.6px;
  height: 6.6px;
}

/* Its folders, in the last step. */
.folders {
  position: absolute;
  top: 26px;
  left: 4.4px;
  display: flex;
  gap: 3.9px;
}
.folder {
  position: relative;
  width: 30.4px;
  height: 20.8px;
}
.folderPapers {
  position: absolute;
  top: 0;
  right: 1.4px;
  width: 17px;
  height: 3.2px;
  border-radius: 1.2px 1.6px 0 0;
  background: linear-gradient(90deg, #e2638a, #ec8a6a 60%, #f2a777);
  clip-path: polygon(20% 0, 100% 0, 100% 100%, 0 100%);
}
.folderPapers::before {
  content: '';
  position: absolute;
  inset: 0;
}
.folderBody {
  position: absolute;
  inset: 1.6px 0 0;
  border-radius: 1.6px 3.4px 3.4px 3.4px;
  background: var(--custom-bg);
  box-shadow:
    0 0 0 0.45px light-dark(rgb(0 0 0 / 0.07), rgb(255 255 255 / 0.09)),
    0 0.8px 1.6px rgb(0 0 0 / 0.06);
}
.folderBody::before {
  /* The folder's tab, on its top left. */
  content: '';
  position: absolute;
  top: -1.6px;
  left: 0;
  width: 12px;
  height: 3px;
  border-radius: 1.6px 1.6px 0 0;
  background: inherit;
}
.folder[data-kind='ideas'] .folderBody {
  background: var(--ideas-bg);
}
.folder[data-kind='inspiration'] .folderBody {
  background: var(--inspiration-bg);
}
.folder[data-kind='work'] .folderBody {
  background: var(--work-bg);
}
.folderGlyph {
  position: absolute;
  top: 3.2px;
  left: 2.8px;
  width: 4.2px;
  height: 4.2px;
  color: var(--custom);
}
.folder[data-kind='ideas'] .folderGlyph {
  color: var(--ideas);
}
.folder[data-kind='inspiration'] .folderGlyph {
  color: var(--inspiration);
}
.folder[data-kind='work'] .folderGlyph {
  color: var(--work);
}
.folderBody b {
  position: absolute;
  top: 8.6px;
  left: 2.8px;
  font-size: 3.5px;
  font-weight: 600;
  white-space: nowrap;
}
.folderBody small {
  position: absolute;
  top: 13.4px;
  left: 2.8px;
  font-size: 3px;
  color: var(--win-muted);
  white-space: nowrap;
}
.folderGlow {
  position: absolute;
  inset: 1.6px 0 0;
  z-index: 1;
  border-radius: 1.6px 3.4px 3.4px 3.4px;
  background: rgb(155 85 224 / 0.16);
  box-shadow: 0 0 0 0.6px rgb(155 85 224 / 0.45);
  opacity: 0;
}
.dragged {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  width: 13.3px;
  height: 9.5px;
  margin: -4.75px 0 0 -6.65px;
  opacity: 0;
}
.draggedPicture {
  position: absolute;
  inset: 0;
  border-radius: 1.8px;
  box-shadow: 0 1.4px 3px rgb(0 0 0 / 0.2);
}
.spark {
  position: absolute;
  top: -1.6px;
  right: -1.6px;
  width: 3.6px;
  height: 3.6px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 30%, #b07cf2 32%);
  box-shadow: 0 0 2px #b07cf2;
}

/* The capture's thumbnail, in the bottom left corner of the screen. */
.thumb {
  position: absolute;
  top: 217.8px;
  left: 13.6px;
  width: 62.3px;
  height: 42.6px;
}
.thumbBack,
.thumbFront {
  position: absolute;
  opacity: 0;
}
.thumbBack {
  top: -2.8px;
  left: 2.4px;
  width: 57.6px;
  height: 40px;
  border-radius: 4px;
  background: light-dark(rgb(255 246 238 / 0.62), rgb(70 60 60 / 0.62));
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.08);
}
.thumbFront {
  inset: 0;
  border-radius: 3.4px;
  overflow: hidden;
  box-shadow:
    0 0 0 0.4px var(--win-edge),
    0 2px 6px rgb(0 0 0 / 0.16);
}
.thumbWindow {
  transform: scale(0.318);
  transform-origin: 0 0;
  box-shadow: none;
}
.thumbAction {
  position: absolute;
  top: 9px;
  left: 13.9px;
  width: 34.5px;
  height: 12px;
  display: flex;
  align-items: center;
  gap: 2.6px;
  padding-left: 5px;
  box-sizing: border-box;
  border-radius: 3px;
  background: rgb(255 255 255 / 0.9);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
  color: #5a5a5a;
  font-size: 6px;
  opacity: 0;
}
.thumbShow {
  top: 22.4px;
  color: #b4b4b4;
  background: rgb(255 255 255 / 0.55);
}
.thumbIcon {
  width: 6.4px;
  height: 6.4px;
}

/* The pointer inside the illustrations; its tip is the box's origin. */
.cursor {
  position: absolute;
  top: -0.9px;
  left: -1.35px;
  width: 9px;
  height: 9px;
  transform-origin: 1.35px 0.9px;
  opacity: 0;
}
.cursorGlyph {
  display: block;
  width: 9px;
  height: 9px;
}

/* ─── The desktop app's canvas, over the desktop, in the hero's px ───────────────────────────── */

.canvas {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.canvasWin {
  position: absolute;
  top: -12px;
  left: 20.6px;
  width: 404px;
  height: 252px;
  border-radius: 10px;
  background:
    radial-gradient(circle, var(--canvas-dot) 0 0.8px, transparent 1.1px) 4px 4px / 12.7px 12.7px,
    var(--canvas);
  box-shadow: 0 0 0 0.5px var(--canvas-edge);
  transform-origin: 201.4px 112px;
  overflow: hidden;
}
.canvasSide {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 22.9px;
  background: var(--canvas-side);
  box-shadow: 0.5px 0 0 var(--canvas-edge);
}
.capture {
  position: absolute;
  top: 0;
  left: 0;
  width: 196px;
  height: 134px;
  transform-origin: 0 0;
  opacity: 0;
}
.capture .window {
  box-shadow:
    0 0 0 0.5px var(--win-edge),
    0 4px 14px rgb(0 0 0 / 0.12);
}
.backdrop {
  position: absolute;
  top: 0;
  left: 0;
  width: 205px;
  height: 156.7px;
  border-radius: 14.6px;
  overflow: hidden;
  transform-origin: 0 0;
  opacity: 0;
  background:
    radial-gradient(60% 48% at 100% 0%, var(--wall-blue) 0%, var(--wall-blue) 50%, transparent 76%),
    radial-gradient(34% 30% at 0% 0%, var(--wall-sky) 0%, var(--wall-sky) 40%, transparent 80%),
    radial-gradient(42% 46% at 0% 100%, var(--wall-sky) 0%, transparent 72%),
    radial-gradient(70% 58% at 52% 34%, var(--wall-sun) 0%, transparent 72%),
    radial-gradient(42% 62% at 22% 70%, var(--wall-4) 0%, transparent 72%),
    radial-gradient(40% 50% at 82% 82%, var(--wall-4) 0%, transparent 72%),
    linear-gradient(160deg, var(--wall-1), var(--wall-2) 40%, var(--wall-3) 70%, var(--wall-4));
}
.logo {
  position: absolute;
  top: 14.05px;
  left: 14.05px;
  width: 14.9px;
  height: 14.9px;
  display: grid;
  place-items: center;
  border-radius: 3.6px;
  background: rgb(255 255 255 / 0.92);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.1);
  color: #1f1f1f;
  opacity: 0;
}
.logoMark {
  width: 8.4px;
  height: 8.4px;
}
.lines {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.lines i,
.lines em {
  position: absolute;
  left: 13.8px;
  background: rgb(255 255 255 / 0.88);
}
.lines i {
  top: 58.2px;
  width: 67.3px;
  height: 6.9px;
  border-radius: 3.45px;
}
.lines i + i {
  top: 71.4px;
  width: 47.9px;
}
.lines em {
  top: 83.7px;
  width: 56.9px;
  height: 2.9px;
  border-radius: 1.45px;
}
.lines em + em {
  top: 91.2px;
  width: 39.7px;
}

/* The bottom of the hero fades into the sheet. */
.fade {
  position: absolute;
  left: 0;
  right: 0;
  top: 108px;
  height: 108px;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--card-bg) 0%, transparent) 0%,
    color-mix(in srgb, var(--card-bg) 18%, transparent) 28%,
    color-mix(in srgb, var(--card-bg) 50%, transparent) 52%,
    color-mix(in srgb, var(--card-bg) 86%, transparent) 76%,
    var(--card-bg) 92%
  );
}
```

### menubar-onboarding/icons.tsx

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

'use client';

import type { ReactNode } from 'react';

/*
 * The line icons of the panel and its illustrations: 24-unit drawings stroked in currentColor.
 * Their size comes from the class they are given (the lab draws an unsized SVG at 16 px), and
 * they are decorative: whatever they stand for is said in words next to them.
 */

function Icon({
  className,
  children,
  fill,
}: {
  className?: string;
  children: ReactNode;
  fill?: boolean;
}) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill={fill ? 'currentColor' : 'none'}
      stroke={fill ? 'none' : 'currentColor'}
      strokeWidth="1.7"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      focusable="false"
    >
      {children}
    </svg>
  );
}

type P = { className?: string };

export const CameraIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M4.5 8.2c0-1 .8-1.8 1.8-1.8h1.6l1.3-1.9h5.6l1.3 1.9h1.6c1 0 1.8.8 1.8 1.8v8.6c0 1-.8 1.8-1.8 1.8H6.3c-1 0-1.8-.8-1.8-1.8z" />
    <circle cx="12" cy="12.4" r="3.2" />
    <circle cx="16.6" cy="9.3" r="0.35" fill="currentColor" />
  </Icon>
);

export const FolderIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M4 7.2c0-1 .8-1.7 1.7-1.7h3.6l1.9 2h7.1c1 0 1.7.8 1.7 1.7v7.6c0 1-.8 1.7-1.7 1.7H5.7c-1 0-1.7-.8-1.7-1.7z" />
    <path d="M4 9.6h16" />
  </Icon>
);

export const EditIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M11.5 5H7.2C6 5 5 6 5 7.2v9.6C5 18 6 19 7.2 19h9.6c1.2 0 2.2-1 2.2-2.2v-4.3" />
    <path d="M17.4 4.6a1.9 1.9 0 0 1 2.7 2.7l-6.7 6.7-3.4.9.9-3.4z" />
  </Icon>
);

export const ChevronLeftIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M14.5 5.5 8 12l6.5 6.5" />
  </Icon>
);

export const PlusIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M12 5v14M5 12h14" />
  </Icon>
);

export const BulbIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M9.2 17.2v-1.1c0-1.2-.6-2.2-1.4-3.1A5.6 5.6 0 0 1 12 3.6a5.6 5.6 0 0 1 4.2 9.4c-.8.9-1.4 1.9-1.4 3.1v1.1" />
    <path d="M9.4 20.4h5.2M10 17.3h4" />
  </Icon>
);

export const FolderHeartIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M11.3 18.5H5.7c-1 0-1.7-.8-1.7-1.7V7.2c0-1 .8-1.7 1.7-1.7h3.6l1.9 2h7.1c1 0 1.7.8 1.7 1.7v1.6" />
    <path d="M16.9 20.2s-3.6-2-3.6-4.4c0-1.1.8-1.9 1.8-1.9.8 0 1.4.4 1.8 1.1.4-.7 1-1.1 1.8-1.1 1 0 1.8.8 1.8 1.9 0 2.4-3.6 4.4-3.6 4.4z" />
  </Icon>
);

export const BriefcaseIcon = ({ className }: P) => (
  <Icon className={className}>
    <rect x="3.8" y="7.2" width="16.4" height="11.8" rx="2" />
    <path d="M9 7.2V5.9c0-.8.6-1.4 1.4-1.4h3.2c.8 0 1.4.6 1.4 1.4v1.3M3.8 12.2h16.4M10.6 12.2v1.6h2.8v-1.6" />
  </Icon>
);

export const HomeIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M4.8 10.6c0-.6.3-1.2.8-1.6l5.2-4a2 2 0 0 1 2.4 0l5.2 4c.5.4.8 1 .8 1.6v6.6c0 1.1-.9 2-2 2H6.8c-1.1 0-2-.9-2-2z" />
    <path d="M9.4 15.6h5.2" />
  </Icon>
);

export const GearIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M10.4 3.8h3.2l.5 2.2 1.6.9 2.1-.7 1.6 2.8-1.6 1.5v1.9l1.6 1.5-1.6 2.8-2.1-.7-1.6.9-.5 2.2h-3.2l-.5-2.2-1.6-.9-2.1.7-1.6-2.8 1.6-1.5v-1.9L4.6 9l1.6-2.8 2.1.7 1.6-.9z" />
    <circle cx="12" cy="12" r="2.6" />
  </Icon>
);

export const VideoIcon = ({ className }: P) => (
  <Icon className={className}>
    <rect x="3.5" y="6.5" width="12" height="11" rx="2.2" />
    <path d="m15.5 10.6 4.2-2.4c.4-.2.8.1.8.5v6.6c0 .4-.4.7-.8.5l-4.2-2.4" />
  </Icon>
);

export const EyeIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M2.8 12s3.4-6 9.2-6 9.2 6 9.2 6-3.4 6-9.2 6-9.2-6-9.2-6z" />
    <circle cx="12" cy="12" r="2.7" />
  </Icon>
);

export const ReturnIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M18 6v5.5c0 1.1-.9 2-2 2H7" />
    <path d="m10 10.5-3 3 3 3" />
  </Icon>
);

export const BatteryIcon = ({ className }: P) => (
  <Icon className={className}>
    <rect x="3" y="7.5" width="16" height="9" rx="2.6" />
    <rect x="5.4" y="9.9" width="11.2" height="4.2" rx="1" fill="currentColor" stroke="none" />
    <path d="M21 10.6v2.8" />
  </Icon>
);

export const WifiIcon = ({ className }: P) => (
  <Icon className={className}>
    <path d="M3.6 9.4a12 12 0 0 1 16.8 0M6.6 12.6a7.8 7.8 0 0 1 10.8 0M9.6 15.7a3.6 3.6 0 0 1 4.8 0" />
    <circle cx="12" cy="18.6" r="0.6" fill="currentColor" />
  </Icon>
);

export const SearchIcon = ({ className }: P) => (
  <Icon className={className}>
    <circle cx="10.5" cy="10.5" r="5.5" />
    <path d="m14.6 14.6 4.9 4.9" />
  </Icon>
);

export const SwitchesIcon = ({ className }: P) => (
  <Icon className={className}>
    <rect x="3.5" y="4.5" width="17" height="6.4" rx="3.2" />
    <rect x="3.5" y="13.1" width="17" height="6.4" rx="3.2" />
    <circle cx="7.2" cy="7.7" r="1.5" fill="currentColor" stroke="none" />
    <circle cx="16.8" cy="16.3" r="1.5" fill="currentColor" stroke="none" />
  </Icon>
);

/** The app's own mark in the menu bar: two offset squares, one solid. */
export const AppMark = ({ className }: P) => (
  <svg className={className} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <rect x="9.5" y="4" width="10.5" height="10.5" rx="2.4" fill="currentColor" />
    <rect
      x="4.4"
      y="9.4"
      width="10.2"
      height="10.2"
      rx="2.4"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.6"
    />
  </svg>
);

/** The logo on the canvas's layout: a ring. */
export const RingMark = ({ className }: P) => (
  <svg className={className} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" strokeWidth="3.4" />
  </svg>
);

/** The pointer inside the illustrations: the system's arrow, drawn small. */
export const MiniArrow = ({ className }: P) => (
  <svg className={className} viewBox="0 0 20 20" aria-hidden="true" focusable="false">
    <path
      d="M3 2v14.5l3.6-3.3 2.6 5.6 2.4-1.1-2.6-5.5h5.2z"
      fill="#141414"
      stroke="#fff"
      strokeWidth="1.3"
      strokeLinejoin="round"
    />
  </svg>
);
```

### menubar-onboarding/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>
  );
}
```

### menubar-onboarding/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. `visible` and `reduced` are returned too: the
 * illustrations loop while the panel is visible, whoever is using it, and hold still under reduced
 * motion.
 */
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, visible, reduced, playing: visible && !reduced && !taken };
}
```