# Video timeline

> A compositing editor’s timeline: layers with lock and eye, a ruler with markers and a red playhead, striped clips, keyframe curves, a waveform, and a play button that lifts, charges and rings.

The timeline of a motion-design tool, dark first. A layers column with tools, lock, eye, colour and a keyframe navigator beside a timecode and transport, a ruler with marker flags and sections, and tracks of clips: striped, solid or audio with its waveform, with keyframe diamonds joined by their curve. Play it, scrub the playhead, drag and trim clips, split them with the razor, add keyframes and markers, zoom the ruler. It can also be driven frame by frame, for a film of it: the entrance staggers the clips in, the panel’s outline lights up, and the play button lifts off its socket, charges blue under beams of light and rings as it is pressed.

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

Category: Media.

## Anatomy

1. **Toolbar**: The pointer and the razor (V and C), and Add, a menu that puts a marker or a keyframe at the playhead.
2. **Timecode**: Where the playhead is, as m:ss.d in monospace: an `output` that follows every move of the clock.
3. **Play button**: Plays and pauses (Space). Pressed by a pointer it lifts off its socket, charges blue under beams and rings.
4. **Layer row**: Lock, eye, colour, tag, icon and name, then the keyframe navigator: previous, a diamond at the playhead, next, and record.
5. **Marker flag**: A flag on the ruler with a line down through the tracks; clicking it moves the playhead to it.
6. **Playhead**: A red head over a line through every track: a slider (arrow keys step a frame), dragged like the ruler to scrub.
7. **Clip**: Placed from its start and end at the zoom; drag it to move it, its edges to trim it, snapping within 6 px.
8. **Keyframe lane**: A property’s diamonds under its layer, joined by its curve, dotted from where the value holds.

## When to use

- Editing media against time: clips on stacked tracks that the reader moves, trims and splits, frame by frame.
- Animating properties: keyframes on a lane under their layer, joined by their curve, with a navigator to step between them.
- A playhead that scrubs, plays, loops and steps a frame, with a timecode that always reads where it is.
- Marking moments and stretches of an edit: flags on the ruler and named sections above the tracks.
- Recording a product film of the editor itself: the `presentation` prop draws any frame exactly, with no clock of its own.

## When not to use

- Plain playback of a finished video. Use a video player with a scrubber.
- A schedule of tasks over days or weeks. Use a project timeline, a Gantt chart.
- A story of events in order, with no lengths to edit. Use a vertical timeline or an activity feed.
- Trimming one clip to a start and an end. Use a range slider over its thumbnails.
- A phone-sized screen as the main place to edit: under 700 px the layers column narrows to lock, eye and name, and drags need room.

## Installation

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

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

Or copy all the source files below into one folder of your project. Either way it needs React 19 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 { VideoTimeline } from './video-timeline';

export function Edit() {
  return (
    <VideoTimeline
      duration={8}
      fps={30}
      onTimeChange={(seconds) => console.log(seconds)}
      onTracksChange={(tracks) => console.log(tracks)}
      autoplay={false}
    />
  );
}
```

## States

- **Paused**: The playhead at `defaultTime`, 0:01.7: the editor as it opens, settled.
- **Playing**: The play button turns to pause with a blue tint, and the playhead runs on in real time.
- **Play hovered**: Under a pointer the play button lifts off its dark socket.
- **Play charged**: Held down, it fills with a blue gradient under five beams of light.
- **Play pressed**: Released, it drops into its socket, swaps play for pause, and a ring spreads into a cyan halo.
- **Entrance**: `presentation.intro` at 0.45: clips grow from their left edge one after another; keyframes, curves and markers follow.
- **Outline lit**: `presentation.outline` at 1: the light has run round the panel and left a steady blue glow.
- **On a keyframe**: The playhead on a Threshold keyframe, 0:02.3: the navigator’s diamonds fill.
- **Clip selected**: `defaultSelected="focus-1"`: the clip is outlined in its own ink.
- **Razor**: `defaultTool="razor"`: the razor is pressed and a click on a clip splits it there.
- **Properties folded**: Depth Scan with `expanded: false`: its keyframe lane folds away and the rows close up.
- **Locked and hidden**: Focus Type `locked` (its clips will not move) and Gradient `hidden` (its clips fade).
- **Zoomed in**: `defaultZoom={120}`: tenths of a second on the ruler; the tracks scroll sideways under it.
- **Narrow**: Under 700 px: the transport takes a row of its own and the layers keep lock, eye, icon and name.
- **Light**: `colorScheme="light"`, or a light page: every colour is a light-dark() token.
- **Dark**: `colorScheme="dark"`, its first look: a black stage under a near-black panel.

## Examples

### Film, frame by frame

The presentation prop driven by filmState(): the slider picks a frame of the 60 fps film and the editor draws exactly it.

```tsx
'use client';

import { useState } from 'react';
import { FILM_SECONDS, RATE } from './choreography';
import { VideoTimeline, filmState } from './video-timeline';

/*
 * The film, frame by frame: a slider sets the second, and the editor draws filmState() of it
 * through its presentation prop: no clock of its own, the same picture for the same frame.
 */
export function FilmFrames() {
  const [frame, setFrame] = useState(0);
  const seconds = frame / RATE;
  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <VideoTimeline presentation={filmState(seconds)} colorScheme="dark" />
      <label
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 12,
          font: '500 13px/1.2 ui-monospace, Menlo, monospace',
        }}
      >
        <span style={{ minWidth: 112 }}>
          Frame {String(frame + 1).padStart(3, '0')} · {seconds.toFixed(2)} s
        </span>
        <input
          type="range"
          min={0}
          max={Math.round(FILM_SECONDS * RATE) - 1}
          step={1}
          value={frame}
          aria-label="Film frame"
          data-film-frame=""
          onChange={(event) => setFrame(Number(event.target.value))}
          style={{ flex: 1 }}
        />
      </label>
    </div>
  );
}
```

## How it works

- One clock: time in seconds is the playhead, the timecode and the keyframe navigator. Playing advances it on every animation frame by the real time elapsed (J and L play backwards and faster), looping at the end while Loop is on; every edit snaps to whole frames of fps.
- The layers column and the timeline share one vertical rhythm, a 46 px top bar, a 36 px ruler, 32 px rows and 27 px property lanes, so a layer and its track always line up; the ruler and the tracks scroll sideways together inside the timeline column, which is sized from the duration and the zoom (px per second).
- A clip is placed from its start and end. Dragging it moves it, its edges trim it, and both snap to the playhead, the markers and other clips’ edges within 6 px; the razor splits it where it is clicked. Arrow keys move a focused clip by a frame (Shift ten, Alt trims its end).
- Keyframes are diamonds on a property’s lane, joined by its curve, dotted from where the value holds. The diamond in a layer’s navigator adds or removes a keyframe at the playhead; the arrows beside it jump to the previous or next one.
- Presentation: given the presentation prop, it draws exactly that state, the time, playing, the entrance’s progress, the play button’s hover, lift, charge, beams and press, the outline, and runs no clock and no transition of its own, so a renderer can step it frame by frame. filmState(seconds) is a short film of it, written in frames of a 60 fps clock; the demo plays that film.
- The entrance grows each clip from its left edge in 44 frames on a cubic-bezier(.3, 0, .3, 1), ten frames apart, by clip-path so the rounded end shows; labels fade in after, then keyframes pop, their curves draw, the markers fade in and the playhead appears. The press drops the lifted button into its socket in 6 frames, swaps play for pause, and flashes a white ring that turns cyan and spreads into a halo.
- The play button’s looks are custom properties (--hover, --lift, --charge, --beams, --tint, --morph and the ring’s) mixed in CSS with color-mix() and calc(); a 3D renderer can set --vt-lift-depth to raise the button towards the camera and --vt-beams-transform to stand the beams up.
- Layout by container queries: under 700 px the layers column narrows to lock, eye, icon and name, and the transport moves to a row of its own.

## API reference

- **tracks** (`Track[]`, default `the sample project`): The tracks, top to bottom: { id, name, kind: layer | group | audio, tone, icon, striped?, clips: { id, start, end, label? }[], keyframes?, params?: { id, name, value, keyframes, holdFrom? }[], expanded?, hidden?, locked?, accent? }.
- **markers** (`Marker[]`, default `two markers`): Flags on the ruler: { id, time, label, tone: blue | green | red | purple }.
- **sections** (`Section[]`, default `Open, Reveal, Title`): The edit’s sections on the Sections row: { id, start, label, tone: blue | teal | purple }.
- **duration** (`number`, default `8`): The timeline’s length, in seconds.
- **fps** (`number`, default `30`): Frames per second: what stepping, dragging and trimming snap to.
- **defaultTime** (`number`, default `1.7`): Where the playhead starts, in seconds.
- **defaultZoom** (`number`, default `53`): The ruler’s scale to begin with, in px per second (20 to 360).
- **defaultLoop** (`boolean`, default `true`): Playback loops at the end.
- **defaultTool** (`'select' | 'razor'`, default `'select'`): The tool to open with: the pointer, or the razor that splits clips. Given, the demo stays off.
- **defaultSelected** (`string`): The id of a clip to open selected. Given, the demo stays off.
- **presentation** (`Presentation`): Draws exactly this state, with no clock or transition of its own: { time, playing, intro? (0–1), readout?, playhead?, tint?, hover?, lift?, charge?, beams?, press? (seconds since the press), outline? (0–1) }. filmState(seconds) returns the film’s.
- **onTimeChange** (`(seconds: number) => void`): Called as the playhead moves.
- **onPlayingChange** (`(playing: boolean) => void`): Called on play and pause.
- **onTracksChange** (`(tracks: Track[]) => void`): Called after an edit: a clip moved, trimmed, split or deleted, a keyframe, a toggle.
- **onMarkersChange** (`(markers: Marker[]) => void`): Called when a marker is added.
- **autoplay** (`boolean`, default `true`): While on screen and untouched, the editor plays its short film. Any input hands it over.
- **colorScheme** (`'light' | 'dark' | 'system'`): Light or dark colours, or the reader’s; without it, the page’s color-scheme.
- **className** (`string`): Extra class for the root, a stage of its own.

## Accessibility

- The playhead is a slider (its value in seconds, its text the timecode): the arrow keys step a frame, Shift ten, Home and End go to the ends. Space plays and pauses anywhere in the editor, J, K and L play backwards, pause and play faster.
- Tools, transport, loop, lock, eye, fold and keyframe buttons are real buttons with labels, pressed states and expanded states; the Add menu is a menu with arrow-key focus and Escape.
- Clips are focusable buttons named with their times: the arrow keys move one, Alt and the arrows trim its end, Delete removes it. Keyframes and marker flags are buttons that move the playhead to them.
- A polite live region says what changed: playing or paused, a split, a moved clip, a keyframe added or removed, a lock. The ruler’s labels, icons, beams and rings are hidden from screen readers.
- Under reduced motion the entrance, the beams, the lift and the ring are skipped and the looks change at once.

## Source

### video-timeline/video-timeline.tsx

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

'use client';

import {
  memo,
  useEffect,
  useEffectEvent,
  useId,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent as ReactKeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from 'react';
import {
  INTRO_FRAMES,
  RATE,
  filmState,
  intro,
  morph,
  ring,
  type Presentation,
} from './choreography';
import {
  MARKERS,
  SECTIONS,
  TRACKS,
  seeded,
  type Clip,
  type Marker,
  type Param,
  type Section,
  type Track,
} from './data';
import { Icon, type IconName } from './icons';
import { useAutoplay } from './use-autoplay';
import styles from './video-timeline.module.css';

export type { Clip, Marker, Param, Presentation, Section, Track };
export { filmState } from './choreography';

/*
 * Video timeline: a compositing editor's timeline. A layers column on the left (tools, the layer
 * list with lock, eye, colour and keyframe navigator) beside the timeline: a timecode and transport,
 * a ruler with marker flags and a red playhead, a row of sections, and the tracks with their clips,
 * keyframe lanes and an audio waveform.
 *
 * - One clock: `time` (seconds) is the playhead, the timecode and the keyframe navigator. Playing
 *   advances it every animation frame by the real time elapsed, looping at the end when Loop is on;
 *   every edit snaps to whole frames of `fps`.
 * - Both columns share one vertical rhythm (top bar, ruler, 32 px rows, 27 px property lanes), so
 *   a row and its track always line up. The ruler and the tracks scroll sideways together inside
 *   the timeline column; the layers column stays put.
 * - Clips are placed by left and width from their times and the zoom (px per second); a drag moves
 *   one, its edges trim it, the razor splits it, and a drag snaps to the playhead, markers and other
 *   clips' edges within 6 px. Keyframes are diamonds: on a property's lane, joined by its curve
 *   (dotted where it holds), or on a clip.
 * - Presentation: given the `presentation` prop, the component draws exactly that state (time,
 *   playing, the entrance, the play button's lift, charge, beams and ring, the outline) and runs no
 *   clock and no transition of its own, so a renderer can step it frame by frame. `filmState(s)`
 *   (choreography.ts) is a whole short film of it; the autoplay demo plays that same film.
 * - Light or dark: every colour is a light-dark() token, so the editor follows the page's
 *   color-scheme, or the one the colorScheme prop sets. Dark is its first look.
 */

const TOP = 46;
const HEAD = 36;
const ROW = 32;
const LANE = 27;
const FOOT = 24;
/** Where 0:00 sits in the timeline column, and the room after the end. */
const ORIGIN = 17;
const TAIL = 40;
const ZOOM_MIN = 20;
const ZOOM_MAX = 360;
/** How close, in px, a dragged edge snaps to the playhead, a marker or another clip. */
const SNAP = 6;

type Row =
  | { type: 'sections'; key: string }
  | { type: 'track'; key: string; track: Track; index: number }
  | { type: 'param'; key: string; track: Track; param: Param };

type Fx = { hover: number; lift: number; charge: number; beams: number; tint: number };
const FX_ZERO: Fx = { hover: 0, lift: 0, charge: 0, beams: 0, tint: 0 };

const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));

/** m:ss.d, the way the timecode reads. */
export function formatTime(seconds: number, tenths = true) {
  const t = Math.max(0, seconds);
  const whole = Math.floor(t + 1e-6);
  const m = Math.floor(whole / 60);
  const s = String(whole % 60).padStart(2, '0');
  if (!tenths) return `${m}:${s}`;
  const d = Math.floor((t - whole) * 10 + 1e-6);
  return `${m}:${s}.${clamp(d, 0, 9)}`;
}

const spoken = (seconds: number) => `${seconds.toFixed(2).replace(/0$/, '')} seconds`;

/** The keyframe times a row's navigator steps through. */
function keysOf(track: Track) {
  const times = [...(track.keyframes ?? [])];
  for (const param of track.params ?? []) times.push(...param.keyframes);
  return [...new Set(times)].sort((a, b) => a - b);
}

export function VideoTimeline({
  tracks: tracksProp = TRACKS,
  markers: markersProp = MARKERS,
  sections: sectionsProp = SECTIONS,
  duration = 8,
  fps = 30,
  defaultTime = 1.7,
  defaultZoom = 53,
  defaultLoop = true,
  defaultTool,
  defaultSelected,
  presentation,
  onTimeChange,
  onPlayingChange,
  onTracksChange,
  onMarkersChange,
  autoplay = true,
  colorScheme,
  className = '',
}: {
  /** The tracks, top to bottom: their clips, keyframes and animated properties. */
  tracks?: Track[];
  /** Flags on the ruler, each with a line down through the tracks. */
  markers?: Marker[];
  /** The edit's sections, on the Sections row. */
  sections?: Section[];
  /** The timeline's length, in seconds. */
  duration?: number;
  /** Frames per second: what stepping, dragging and trimming snap to. */
  fps?: number;
  /** Where the playhead starts, in seconds. */
  defaultTime?: number;
  /** The ruler's starting scale, in px per second. */
  defaultZoom?: number;
  /** Playback loops back to the start at the end. */
  defaultLoop?: boolean;
  /** The tool to open with, pointer or razor; given, the demo stays off. */
  defaultTool?: 'select' | 'razor';
  /** The id of a clip to open selected; given, the demo stays off. */
  defaultSelected?: string;
  /**
   * Draws exactly this state and runs no clock or transition of its own, for a frame-by-frame
   * renderer: `time`, `playing`, `intro` (the entrance, 0 to 1) and the play button's effects.
   * See choreography.ts and `filmState`.
   */
  presentation?: Presentation;
  onTimeChange?: (seconds: number) => void;
  onPlayingChange?: (playing: boolean) => void;
  /** Called after an edit: a clip moved, trimmed or split, a keyframe, a toggle. */
  onTracksChange?: (tracks: Track[]) => void;
  onMarkersChange?: (markers: Marker[]) => void;
  /** While on screen and untouched, the editor plays its short film: entrance, play, press. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  className?: string;
}) {
  const { ref: root, playing: demoPlaying, take } = useAutoplay<HTMLDivElement>(8000);
  const [tracks, setTracks] = useState(tracksProp);
  const [markers, setMarkers] = useState(markersProp);
  const [sections] = useState(sectionsProp);
  const [time, setTime] = useState(clamp(defaultTime, 0, duration));
  const [playing, setPlaying] = useState(false);
  const [rate, setRate] = useState(1);
  const [loop, setLoop] = useState(defaultLoop);
  const [zoom, setZoom] = useState(defaultZoom);
  const [tool, setTool] = useState<'select' | 'razor'>(defaultTool ?? 'select');
  const [selected, setSelected] = useState<string | null>(defaultSelected ?? null);
  const [armed, setArmed] = useState<string[]>([]);
  const [menu, setMenu] = useState(false);
  const [message, setMessage] = useState('');
  const [fx, setFx] = useState<Fx>(FX_ZERO);
  const [hovering, setHovering] = useState(false);
  const [holding, setHolding] = useState(false);
  /** Play was started by a press of the button itself: the charged look, its beams and ring. */
  const [hero, setHero] = useState(false);
  const [pressAt, setPressAt] = useState<number | null>(null);
  const [pressAge, setPressAge] = useState<number | null>(null);
  const [demoClock, setDemoClock] = useState(0);
  const [reduced, setReduced] = useState(false);
  const [onScreen, setOnScreen] = useState(true);
  const [size, setSize] = useState({ width: 760, height: 293 });
  const editor = useRef<HTMLDivElement>(null);
  const scroller = useRef<HTMLDivElement>(null);
  const scrubbing = useRef(false);
  const menuId = useId();

  // New data from outside starts the editor over on it.
  const [given, setGiven] = useState({ tracksProp, markersProp });
  if (given.tracksProp !== tracksProp || given.markersProp !== markersProp) {
    setGiven({ tracksProp, markersProp });
    setTracks(tracksProp);
    setMarkers(markersProp);
  }

  const presenting = presentation !== undefined;
  // Opening in a given tool or selection is a state to show: the demo stays off.
  const opened = defaultTool !== undefined || defaultSelected !== undefined;
  const demoOn = autoplay && demoPlaying && !presenting && !opened;
  const demo = useMemo(() => (demoOn ? filmState(demoClock) : null), [demoOn, demoClock]);
  const show = presentation ?? demo;

  const frame = 1 / fps;
  const snap = (t: number) => Math.round(t * fps) / fps;
  const x = (t: number) => ORIGIN + t * zoom;
  const contentWidth = ORIGIN + duration * zoom + TAIL;

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

  // Reduced motion, and whether the editor is near the screen (playback's clock rests otherwise).
  useEffect(() => {
    const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const read = () => setReduced(motion.matches);
    read();
    motion.addEventListener('change', read);
    const element = editor.current;
    const observer = new IntersectionObserver(([entry]) => setOnScreen(entry.isIntersecting), {
      rootMargin: '200px 0px',
    });
    if (element) observer.observe(element);
    const resize = new ResizeObserver(() => {
      if (element) setSize({ width: element.offsetWidth, height: element.offsetHeight });
    });
    if (element) resize.observe(element);
    return () => {
      motion.removeEventListener('change', read);
      observer.disconnect();
      resize.disconnect();
    };
  }, []);

  // The demo hands over: the reader finds the playhead where the film left it, paused.
  const [wasDemo, setWasDemo] = useState(false);
  if (wasDemo !== demoOn) {
    setWasDemo(demoOn);
    if (!demoOn && demoClock > 0) {
      setTime(clamp(snap(filmState(demoClock).time), 0, duration));
      setPlaying(false);
      setHero(false);
      setPressAt(null);
      setPressAge(null);
    }
    if (demoOn) setDemoClock(0);
  }

  const setTimeTo = (t: number) => {
    const next = clamp(t, 0, duration);
    setTime(next);
    onTimeChange?.(next);
  };
  const setPlayingTo = (next: boolean, by: 'button' | 'key' = 'key') => {
    if (next && time >= duration - frame / 2 && rate > 0) setTimeTo(0);
    setPlaying(next);
    setRate(1);
    const heroic = next && by === 'button' && !reduced;
    setHero(heroic);
    setPressAt(heroic ? performance.now() : null);
    setPressAge(heroic ? 0 : null);
    onPlayingChange?.(next);
    announce(next ? 'Playing' : `Paused at ${formatTime(time)}`);
  };

  const fxTarget: Fx = {
    hover: hovering ? 1 : 0,
    lift: hovering && !hero && !reduced ? 1 : 0,
    charge: holding || hero ? 1 : 0,
    beams: (holding || hero) && !reduced ? (hero ? 0.75 : 1) : 0,
    tint: playing && !hero ? 1 : 0,
  };
  const fxBusy = (Object.keys(fx) as (keyof Fx)[]).some(
    (key) => Math.abs(fx[key] - fxTarget[key]) > 0.002,
  );
  const pressBusy = pressAt !== null && (pressAge ?? 0) < 3.4;
  const ticking = !presenting && onScreen && (demoOn || playing || fxBusy || pressBusy);

  const step = useEffectEvent((dt: number, now: number) => {
    if (demoOn) {
      // The film, then playback on to the end, a breath, and again.
      const next = demoClock + dt;
      const state = filmState(next);
      setDemoClock(state.time >= duration ? 0 : next);
      return;
    }
    if (playing && !scrubbing.current) {
      let next = time + dt * rate;
      if (next >= duration) {
        if (loop) next -= duration;
        else {
          next = duration;
          setPlaying(false);
          setHero(false);
          onPlayingChange?.(false);
          announce('Reached the end');
        }
      } else if (next <= 0 && rate < 0) {
        next = loop ? duration + next : 0;
        if (!loop) setPlaying(false);
      }
      setTimeTo(next);
    }
    if (fxBusy) {
      const k = reduced ? 1 : 1 - Math.exp(-dt * 16);
      setFx((now) => {
        const out = { ...now };
        for (const key of Object.keys(out) as (keyof Fx)[]) {
          const target = fxTarget[key];
          out[key] =
            Math.abs(target - now[key]) < 0.003 ? target : now[key] + (target - now[key]) * k;
        }
        return out;
      });
    }
    if (pressAt !== null) {
      const age = (now - pressAt) / 1000;
      setPressAge(age > 3.7 ? null : age);
      if (age > 3.7) setPressAt(null);
    }
  });

  useEffect(() => {
    if (!ticking) return;
    let raf = 0;
    let last = performance.now();
    const loopFrame = (now: number) => {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      step(dt, now);
      raf = requestAnimationFrame(loopFrame);
    };
    raf = requestAnimationFrame(loopFrame);
    return () => cancelAnimationFrame(raf);
  }, [ticking]);

  // What is drawn: the presentation or the demo's frame, else the editor's own state.
  const view = {
    time: show ? clamp(show.time, 0, duration) : time,
    readout: show ? (show.readout ?? show.time) : time,
    playing: show ? show.playing : playing,
    introFrame: show ? clamp(show.intro ?? 1, 0, 1) * INTRO_FRAMES : INTRO_FRAMES,
    hover: show ? (show.hover ?? 0) : fx.hover,
    lift: show ? (show.lift ?? 0) : fx.lift,
    charge: show ? (show.charge ?? 0) : fx.charge,
    beams: show ? (show.beams ?? show.charge ?? 0) : fx.beams,
    tint: show ? (show.tint ?? (show.playing && !show.charge ? 1 : 0)) : fx.tint,
    press: show ? show.press : pressAge,
    outline: show ? show.outline : undefined,
  };
  const playheadAlpha = show?.playhead ?? intro.playhead(view.introFrame);

  const rows = useMemo(() => {
    const list: Row[] = [{ type: 'sections', key: 'sections' }];
    tracks.forEach((track, index) => {
      list.push({ type: 'track', key: track.id, track, index });
      if (track.expanded)
        for (const param of track.params ?? [])
          list.push({ type: 'param', key: `${track.id}/${param.id}`, track, param });
    });
    return list;
  }, [tracks]);
  const heightOf = (row: Row) => (row.type === 'param' ? LANE : ROW);
  const tracksHeight = rows.reduce((sum, row) => sum + heightOf(row), 0) + FOOT;

  // Edits update the tracks from their latest state, then tell onTracksChange once they settle.
  const [edited, setEdited] = useState(0);
  const patchTrack = (id: string, patch: (track: Track) => Track) => {
    setTracks((now) => now.map((track) => (track.id === id ? patch(track) : track)));
    setEdited((n) => n + 1);
  };
  const reportTracks = useEffectEvent(() => onTracksChange?.(tracks));
  useEffect(() => {
    if (edited) reportTracks();
  }, [edited]);

  /** Moves the playhead to the keyframe before or after it on a row. */
  const jumpKey = (times: number[], direction: -1 | 1, name: string) => {
    const eps = frame / 2;
    const target =
      direction < 0
        ? [...times].reverse().find((t) => t < view.time - eps)
        : times.find((t) => t > view.time + eps);
    if (target === undefined) return announce(`No keyframe ${direction < 0 ? 'before' : 'after'}`);
    setTimeTo(target);
    announce(`${name}: keyframe at ${formatTime(target)}`);
  };
  /** Adds a keyframe at the playhead, or takes away the one there. */
  const toggleKey = (track: Track, param?: Param) => {
    const t = snap(view.time);
    const toggle = (times: number[]) => {
      const at = times.findIndex((k) => Math.abs(k - t) < frame / 2);
      return at >= 0 ? times.filter((_, i) => i !== at) : [...times, t].sort((a, b) => a - b);
    };
    const had = (param ? param.keyframes : (track.keyframes ?? [])).some(
      (k) => Math.abs(k - t) < frame / 2,
    );
    patchTrack(track.id, (current) =>
      param
        ? {
            ...current,
            params: current.params?.map((p) =>
              p.id === param.id ? { ...p, keyframes: toggle(p.keyframes) } : p,
            ),
          }
        : { ...current, keyframes: toggle(current.keyframes ?? []) },
    );
    announce(`${had ? 'Removed' : 'Added'} a keyframe at ${formatTime(t)}`);
  };

  const zoomTo = (next: number, anchorX?: number) => {
    const element = scroller.current;
    const value = clamp(next, ZOOM_MIN, ZOOM_MAX);
    if (element) {
      const anchor = anchorX ?? element.clientWidth / 2;
      const at = (element.scrollLeft + anchor - ORIGIN) / zoom;
      requestAnimationFrame(() => {
        element.scrollLeft = ORIGIN + at * value - anchor;
      });
    }
    setZoom(value);
  };

  // Ctrl or ⌘ with the wheel (and a trackpad's pinch) zooms the ruler around the pointer.
  const onWheel = useEffectEvent((event: WheelEvent) => {
    if (!(event.ctrlKey || event.metaKey) || presenting) return;
    event.preventDefault();
    take();
    const rect = scroller.current!.getBoundingClientRect();
    zoomTo(zoom * Math.exp(-event.deltaY * 0.004), event.clientX - rect.left);
  });
  useEffect(() => {
    const element = scroller.current;
    if (!element) return;
    const listener = (event: WheelEvent) => onWheel(event);
    element.addEventListener('wheel', listener, { passive: false });
    return () => element.removeEventListener('wheel', listener);
  }, []);

  /** Time under a pointer, in the timeline column. */
  const timeAt = (clientX: number) => {
    const rect = scroller.current!.getBoundingClientRect();
    return (clientX - rect.left + scroller.current!.scrollLeft - ORIGIN) / zoom;
  };

  // Scrubbing: press anywhere on the ruler (or the playhead) and drag.
  const scrub = (event: ReactPointerEvent<HTMLElement>) => {
    if (presenting || event.button !== 0) return;
    if ((event.target as HTMLElement).closest('[data-part="marker"]')) return;
    event.preventDefault();
    const target = event.currentTarget;
    target.setPointerCapture(event.pointerId);
    scrubbing.current = true;
    setTimeTo(snap(timeAt(event.clientX)));
    const move = (e: PointerEvent) => setTimeTo(snap(timeAt(e.clientX)));
    const up = () => {
      scrubbing.current = false;
      target.removeEventListener('pointermove', move);
      target.removeEventListener('pointerup', up);
      target.removeEventListener('pointercancel', up);
    };
    target.addEventListener('pointermove', move);
    target.addEventListener('pointerup', up);
    target.addEventListener('pointercancel', up);
  };

  /** Times an edge may snap to: the playhead, the markers, other clips' edges. */
  const snapTargets = (except: string) => {
    const list = [time, 0, duration, ...markers.map((m) => m.time)];
    for (const track of tracks)
      for (const clip of track.clips) if (clip.id !== except) list.push(clip.start, clip.end);
    return list;
  };
  const snapEdge = (t: number, targets: number[]) => {
    let best = snap(t);
    let distance = SNAP / zoom;
    for (const target of targets)
      if (Math.abs(target - t) < distance) {
        distance = Math.abs(target - t);
        best = target;
      }
    return best;
  };

  // A clip: drag to move, its edges to trim; the razor splits it where it is clicked.
  const onClipDown = (event: ReactPointerEvent<HTMLElement>, track: Track, clip: Clip) => {
    if (presenting || event.button !== 0) return;
    event.stopPropagation();
    setSelected(clip.id);
    if (tool === 'razor') {
      const at = snap(timeAt(event.clientX));
      if (track.locked || at <= clip.start + frame || at >= clip.end - frame) return;
      patchTrack(track.id, (current) => ({
        ...current,
        clips: current.clips.flatMap((c) =>
          c.id === clip.id
            ? [
                { ...c, end: at },
                { ...c, id: `${c.id}-${Math.round(at * fps)}`, start: at },
              ]
            : [c],
        ),
      }));
      announce(`Split ${clip.label ?? track.name} at ${formatTime(at)}`);
      return;
    }
    if (track.locked) return announce(`${track.name} is locked`);
    const element = event.currentTarget;
    const rect = element.getBoundingClientRect();
    const edge = Math.min(8, rect.width / 4);
    const mode =
      event.clientX - rect.left < edge
        ? 'start'
        : rect.right - event.clientX < edge
          ? 'end'
          : 'move';
    element.setPointerCapture(event.pointerId);
    const from = event.clientX;
    const targets = snapTargets(clip.id);
    const length = clip.end - clip.start;
    const apply = (clientX: number) => {
      const delta = (clientX - from) / zoom;
      let start = clip.start;
      let end = clip.end;
      if (mode === 'move') {
        start = clamp(snapEdge(clip.start + delta, targets), 0, duration - length);
        const endSnap = snapEdge(clip.end + delta, targets);
        if (Math.abs(endSnap - (clip.end + delta)) < Math.abs(start - (clip.start + delta)))
          start = clamp(endSnap - length, 0, duration - length);
        end = start + length;
      } else if (mode === 'start')
        start = clamp(snapEdge(clip.start + delta, targets), 0, clip.end - frame * 3);
      else end = clamp(snapEdge(clip.end + delta, targets), clip.start + frame * 3, duration);
      latest = snapshot.map((t) =>
        t.id === track.id
          ? { ...t, clips: t.clips.map((c) => (c.id === clip.id ? { ...c, start, end } : c)) }
          : t,
      );
      setTracks(latest);
      return { start, end };
    };
    const snapshot = tracks;
    let latest = tracks;
    let last = { start: clip.start, end: clip.end };
    const move = (e: PointerEvent) => {
      last = apply(e.clientX);
    };
    const up = () => {
      element.removeEventListener('pointermove', move);
      element.removeEventListener('pointerup', up);
      element.removeEventListener('pointercancel', up);
      if (latest !== snapshot) setEdited((n) => n + 1);
      if (last.start !== clip.start || last.end !== clip.end)
        announce(
          `${clip.label ?? track.name}: ${formatTime(last.start)} to ${formatTime(last.end)}`,
        );
    };
    element.addEventListener('pointermove', move);
    element.addEventListener('pointerup', up);
    element.addEventListener('pointercancel', up);
  };

  const onClipKey = (event: ReactKeyboardEvent<HTMLElement>, track: Track, clip: Clip) => {
    if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
      if ((event.key === 'Delete' || event.key === 'Backspace') && !track.locked) {
        event.preventDefault();
        event.stopPropagation();
        patchTrack(track.id, (t) => ({ ...t, clips: t.clips.filter((c) => c.id !== clip.id) }));
        announce(`Deleted ${clip.label ?? track.name}`);
      }
      return;
    }
    event.preventDefault();
    event.stopPropagation();
    if (track.locked) return announce(`${track.name} is locked`);
    const delta = (event.key === 'ArrowLeft' ? -1 : 1) * frame * (event.shiftKey ? 10 : 1);
    let { start, end } = clip;
    if (event.altKey) end = clamp(end + delta, start + frame * 3, duration);
    else {
      start = clamp(start + delta, 0, duration - (end - start));
      end = start + (clip.end - clip.start);
    }
    patchTrack(track.id, (t) => ({
      ...t,
      clips: t.clips.map((c) => (c.id === clip.id ? { ...c, start, end } : c)),
    }));
    announce(`${formatTime(start)} to ${formatTime(end)}`);
  };

  // The editor's keys, wherever the focus is inside it (but not on a text field or a menu).
  const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
    if (presenting) return;
    take();
    const target = event.target as HTMLElement;
    if (target.closest('[role="menu"]')) return;
    const key = event.key;
    const onButton = target.tagName === 'BUTTON';
    if (key === ' ' && !onButton) {
      event.preventDefault();
      setPlayingTo(!playing);
    } else if ((key === 'ArrowLeft' || key === 'ArrowRight') && !event.defaultPrevented) {
      event.preventDefault();
      const n = event.shiftKey ? 10 : 1;
      setPlaying(false);
      setTimeTo(snap(time + (key === 'ArrowLeft' ? -n : n) * frame));
    } else if (key === 'Home' || key === 'End') {
      event.preventDefault();
      setTimeTo(key === 'Home' ? 0 : duration);
    } else if (key === 'k' || key === 'K') {
      setPlaying(false);
      announce('Paused');
    } else if (key === 'l' || key === 'L') {
      const next = playing && rate > 0 ? Math.min(4, rate * 2) : 1;
      setRate(next);
      setPlaying(true);
      announce(next === 1 ? 'Playing' : `Playing at ${next}×`);
    } else if (key === 'j' || key === 'J') {
      const next = playing && rate < 0 ? Math.max(-4, rate * 2) : -1;
      setRate(next);
      setPlaying(true);
      announce(`Playing backwards${next === -1 ? '' : ` at ${-next}×`}`);
    } else if (key === '+' || key === '=') zoomTo(zoom * 1.25);
    else if (key === '-' || key === '_') zoomTo(zoom / 1.25);
    else if (key === 'v' || key === 'V') {
      setTool('select');
      announce('Pointer');
    } else if (key === 'c' || key === 'C') {
      setTool('razor');
      announce('Razor: click a clip to split it');
    } else if (key === 'Escape') {
      setMenu(false);
      setSelected(null);
    }
  };

  const addMarker = () => {
    const t = snap(time);
    const next = [
      ...markers,
      {
        id: `m${Math.round(t * fps)}-${markers.length}`,
        time: t,
        label: `Mark ${formatTime(t)}s`.replace(/^Mark 0:0?/, 'Mark '),
        tone: 'red' as const,
      },
    ].sort((a, b) => a.time - b.time);
    setMarkers(next);
    onMarkersChange?.(next);
    setMenu(false);
    announce(`Marker added at ${formatTime(t)}`);
  };
  const addKey = () => {
    const owner = tracks.find((t) => t.params?.length && t.expanded) ?? tracks[0];
    setMenu(false);
    if (owner) toggleKey(owner, owner.params?.[0]);
  };

  // The play button: pressed by a pointer it charges, lifts, and shows its beams and ring.
  const onPlayDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
    if (presenting || event.button !== 0) return;
    setHolding(true);
    const up = () => {
      setHolding(false);
      window.removeEventListener('pointerup', up);
      window.removeEventListener('pointercancel', up);
    };
    window.addEventListener('pointerup', up);
    window.addEventListener('pointercancel', up);
  };

  const ringState = reduced && !show ? ring(null) : ring(view.press);
  const iconMorph = view.press != null ? morph(view.press) : view.playing ? 1 : 0;
  const shownMorph = view.press != null && !view.playing ? 0 : iconMorph;

  // The outline: a light running round the panel's edge, counter-clockwise from the top, then a
  // steady glow. Its path runs clockwise from the top-left corner, in 1000 units.
  const outline = view.outline;
  const perimeter = 2 * (size.width + size.height);
  const comet = outline === undefined ? 0 : clamp(outline / 0.78, 0, 1);
  const cometFrom = (0.45 * size.width * 1000) / perimeter;
  const cometTo = (-(size.height + 0.55 * size.width) * 1000) / perimeter;
  const cometAt = cometFrom + (cometTo - cometFrom) * comet;
  const cometAlpha =
    outline === undefined
      ? 0
      : Math.min(1, outline / 0.08) * (1 - clamp((comet - 0.82) / 0.18, 0, 1));
  const glow = outline === undefined ? undefined : clamp((outline - 0.44) / 0.56, 0, 1);

  const vars = {
    '--content-w': `${contentWidth}px`,
    '--tracks-h': `${tracksHeight}px`,
    '--glow': glow,
  } as CSSProperties;

  const playVars = {
    '--hover': view.hover,
    '--lift': view.lift,
    '--charge': view.charge,
    '--beams': view.beams,
    '--tint': view.tint,
    '--ring-gap': `${ringState.gap}px`,
    '--ring-alpha': ringState.alpha,
    '--ring-white': ringState.white,
    '--ring-fill': ringState.fill,
    '--morph': shownMorph,
  } as CSSProperties;

  const timecode = formatTime(view.readout);

  return (
    <div
      ref={root}
      className={`${styles.root} ${className}`}
      data-part="root"
      data-color-scheme={colorScheme}
      data-presenting={presenting || demoOn ? '' : undefined}
      onPointerDownCapture={() => {
        if (!presenting) take();
      }}
      onWheelCapture={(event) => {
        if (!presenting && (event.ctrlKey || event.metaKey)) take();
      }}
    >
      <div
        ref={editor}
        className={styles.editor}
        data-part="editor"
        data-tool={tool}
        data-glow={glow === undefined ? undefined : ''}
        role="group"
        aria-label="Video timeline editor"
        aria-describedby={`${menuId}-help`}
        style={{ ...vars, width: `min(100%, calc(var(--name-w) + ${contentWidth}px))` }}
        onKeyDown={onKeyDown}
      >
        <p id={`${menuId}-help`} className={styles.hidden}>
          Space plays and pauses; the arrow keys step a frame, with Shift ten; J, K and L play
          backwards, pause and play faster; plus and minus zoom; V and C pick the pointer or the
          razor.
        </p>

        {/* Top bar, left: the tools. */}
        <div className={styles.tools} data-part="toolbar" role="toolbar" aria-label="Tools">
          <div className={styles.toolGroup}>
            <button
              type="button"
              className={styles.tool}
              data-part="tool-pointer"
              aria-pressed={tool === 'select'}
              aria-label="Pointer (V)"
              onClick={() => setTool('select')}
            >
              <Icon name="pointer" />
            </button>
            <button
              type="button"
              className={styles.tool}
              data-part="tool-razor"
              aria-pressed={tool === 'razor'}
              aria-label="Razor (C)"
              onClick={() => setTool('razor')}
            >
              <Icon name="razor" />
            </button>
          </div>
          <div className={styles.addWrap}>
            <button
              type="button"
              className={styles.add}
              data-part="add"
              aria-haspopup="menu"
              aria-expanded={menu}
              aria-controls={menu ? `${menuId}-menu` : undefined}
              onClick={() => setMenu((open) => !open)}
            >
              <Icon name="add" className={styles.addIcon} />
              <span>Add</span>
              <Icon name="chevronDown" className={styles.addChevron} />
            </button>
            {menu && (
              <div
                id={`${menuId}-menu`}
                className={styles.menu}
                role="menu"
                aria-label="Add"
                onKeyDown={(event) => {
                  const items = [
                    ...event.currentTarget.querySelectorAll<HTMLElement>('[role="menuitem"]'),
                  ];
                  const at = items.indexOf(document.activeElement as HTMLElement);
                  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
                    event.preventDefault();
                    const n = items.length;
                    items[(at + (event.key === 'ArrowDown' ? 1 : n - 1)) % n]?.focus();
                  } else if (event.key === 'Escape' || event.key === 'Tab') {
                    setMenu(false);
                  }
                }}
              >
                <button
                  type="button"
                  role="menuitem"
                  autoFocus
                  className={styles.menuItem}
                  onClick={addMarker}
                >
                  <span className={styles.menuSwatch} data-tone="red" />
                  Marker at playhead
                </button>
                <button type="button" role="menuitem" className={styles.menuItem} onClick={addKey}>
                  <Icon name="diamond" className={styles.menuIcon} />
                  Keyframe at playhead
                </button>
              </div>
            )}
          </div>
        </div>

        {/* Top bar, right: timecode, transport, loop, length. */}
        <div className={styles.bar} data-part="transport-bar">
          <output className={styles.timecode} data-part="timecode" aria-label="Current time">
            {timecode}
          </output>
          <div
            className={styles.transport}
            data-part="transport"
            role="group"
            aria-label="Transport"
          >
            <button
              type="button"
              className={styles.tbtn}
              aria-label="Go to start"
              onClick={() => setTimeTo(0)}
            >
              <Icon name="start" />
            </button>
            <button
              type="button"
              className={styles.tbtn}
              aria-label="Back one frame"
              onClick={() => {
                setPlaying(false);
                setTimeTo(snap(time - frame));
              }}
            >
              <Icon name="stepBack" />
            </button>
            <div className={styles.playCell} style={playVars}>
              <span className={styles.socket} data-part="play-socket" aria-hidden="true" />
              <span className={styles.ring} data-part="play-ring" aria-hidden="true" />
              <span className={styles.beams} data-part="play-beams" aria-hidden="true">
                <i />
                <i />
                <i />
                <i />
                <i />
              </span>
              <button
                type="button"
                className={styles.play}
                data-part="play"
                data-playing={view.playing ? '' : undefined}
                aria-label={view.playing ? 'Pause (Space)' : 'Play (Space)'}
                onPointerEnter={(event) => {
                  if (event.pointerType !== 'touch') setHovering(true);
                }}
                onPointerLeave={() => setHovering(false)}
                onPointerDown={onPlayDown}
                onClick={(event) => setPlayingTo(!playing, event.detail > 0 ? 'button' : 'key')}
              >
                <span className={styles.playFace} aria-hidden="true" />
                <Icon name="play" className={styles.playIcon} />
                <Icon name="pause" className={styles.pauseIcon} />
              </button>
            </div>
            <button
              type="button"
              className={styles.tbtn}
              aria-label="Forward one frame"
              onClick={() => {
                setPlaying(false);
                setTimeTo(snap(time + frame));
              }}
            >
              <Icon name="stepForward" />
            </button>
            <button
              type="button"
              className={styles.tbtn}
              aria-label="Go to end"
              onClick={() => setTimeTo(duration)}
            >
              <Icon name="end" />
            </button>
          </div>
          <button
            type="button"
            className={styles.loop}
            data-part="loop"
            aria-pressed={loop}
            aria-label="Loop"
            onClick={() => setLoop((on) => !on)}
          >
            <Icon name="loop" />
          </button>
          <span
            className={styles.total}
            data-part="total"
            aria-label={`Length ${formatTime(duration)}`}
          >
            {formatTime(duration)}
          </span>
        </div>

        {/* The layers column. */}
        <div className={styles.layers} data-part="layers" role="list" aria-label="Layers">
          <div className={styles.head} aria-hidden="true">
            <Icon name="lock" className={styles.hLock} />
            <Icon name="eye" className={styles.hEye} />
            <Icon name="pin" className={styles.hPin} />
            <Icon name="tag" className={styles.hTag} />
            <span className={styles.hName}>Name</span>
          </div>
          {rows.map((row) => (
            <LayerRow
              key={row.key}
              row={row}
              time={view.time}
              frame={frame}
              armed={row.type !== 'sections' && armed.includes(row.track.id)}
              onExpand={(track) => patchTrack(track.id, (t) => ({ ...t, expanded: !t.expanded }))}
              onToggle={(track, key) => {
                patchTrack(track.id, (t) => ({ ...t, [key]: !t[key] }));
                announce(
                  key === 'hidden'
                    ? `${track.name} ${track.hidden ? 'shown' : 'hidden'}`
                    : `${track.name} ${track.locked ? 'unlocked' : 'locked'}`,
                );
              }}
              onArm={(track) =>
                setArmed((list) =>
                  list.includes(track.id)
                    ? list.filter((id) => id !== track.id)
                    : [...list, track.id],
                )
              }
              onJump={jumpKey}
              onKey={toggleKey}
            />
          ))}
        </div>

        {/* The timeline column: ruler, sections, tracks, playhead — one sideways scroller. */}
        <div
          ref={scroller}
          className={styles.timeline}
          data-part="timeline"
          tabIndex={0}
          aria-label="Timeline: drag the ruler to scrub; ctrl and the wheel zoom"
        >
          <div className={styles.content} style={{ width: contentWidth }}>
            <div className={styles.ruler} data-part="ruler" onPointerDown={scrub}>
              <Ruler duration={duration} zoom={zoom} />
              {markers.map((marker, i) => (
                <div
                  key={marker.id}
                  className={styles.marker}
                  data-part="marker"
                  data-tone={marker.tone}
                  style={{ left: x(marker.time), opacity: intro.marker(view.introFrame, i) }}
                >
                  <span className={styles.pin} aria-hidden="true" />
                  <button
                    type="button"
                    className={styles.flag}
                    aria-label={`Marker ${marker.label}: go to ${formatTime(marker.time)}`}
                    onClick={() => setTimeTo(marker.time)}
                  >
                    {marker.label}
                  </button>
                </div>
              ))}
            </div>
            <Lanes
              rows={rows}
              sections={sections}
              duration={duration}
              zoom={zoom}
              introFrame={view.introFrame}
              clipLead={show?.clipLead}
              selected={selected}
              onClipDown={onClipDown}
              onClipKey={onClipKey}
              onKeyframe={(t) => setTimeTo(t)}
              onBackground={(event) => {
                setSelected(null);
                scrub(event);
              }}
            />
            {markers.map((marker, i) => (
              <span
                key={marker.id}
                className={styles.markerLine}
                data-tone={marker.tone}
                aria-hidden="true"
                style={{ left: x(marker.time), opacity: intro.marker(view.introFrame, i) }}
              />
            ))}
            <div
              className={styles.playhead}
              data-part="playhead"
              style={{ transform: `translateX(${x(view.time)}px)`, opacity: playheadAlpha }}
            >
              <span
                className={styles.head2}
                role="slider"
                tabIndex={0}
                aria-label="Playhead"
                aria-valuemin={0}
                aria-valuemax={duration}
                aria-valuenow={Number(view.time.toFixed(3))}
                aria-valuetext={formatTime(view.time)}
                onPointerDown={scrub}
              >
                <svg viewBox="0 0 18 19" aria-hidden="true">
                  <path d="M4 0h10a4 4 0 0 1 4 4v7.4a2 2 0 0 1-.6 1.4l-6 5.6a2 2 0 0 1-2.8 0l-6-5.6A2 2 0 0 1 0 11.4V4a4 4 0 0 1 4-4z" />
                </svg>
              </span>
              <span className={styles.line} aria-hidden="true" />
            </div>
          </div>
        </div>

        {/* The outline's light and glow. */}
        <svg
          className={styles.outline}
          data-part="outline"
          aria-hidden="true"
          style={{ opacity: cometAlpha }}
        >
          <rect
            className={styles.cometTail}
            x="0.75"
            y="0.75"
            rx="14"
            pathLength={1000}
            style={{
              width: 'calc(100% - 1.5px)',
              height: 'calc(100% - 1.5px)',
              strokeDashoffset: -(((cometAt % 1000) + 1000) % 1000),
            }}
          />
          <rect
            className={styles.cometHead}
            x="0.75"
            y="0.75"
            rx="14"
            pathLength={1000}
            style={{
              width: 'calc(100% - 1.5px)',
              height: 'calc(100% - 1.5px)',
              strokeDashoffset: -(((cometAt % 1000) + 1000) % 1000),
            }}
          />
        </svg>
      </div>
      <p className={styles.hidden} aria-live="polite">
        {message}
      </p>
    </div>
  );
}

/** The ruler's second lines, labels and minor ticks. */
const Ruler = memo(function Ruler({ duration, zoom }: { duration: number; zoom: number }) {
  const every = zoom < 34 ? 2 : 1;
  const minor = zoom >= 170 ? 0.1 : zoom >= 34 ? 0.25 : 0.5;
  const seconds = [];
  for (let s = 0; s <= Math.ceil(duration); s += every) seconds.push(s);
  const ticks = [];
  for (let t = minor; t < duration + 0.5; t += minor)
    if (Math.abs(t - Math.round(t)) > 1e-6) ticks.push(t);
  return (
    <div aria-hidden="true">
      {seconds.map((s) => (
        <span key={s} className={styles.second} style={{ left: ORIGIN + s * zoom }}>
          <span className={styles.secondLabel}>{formatTime(s, false)}</span>
        </span>
      ))}
      {ticks.map((t) => (
        <span key={t.toFixed(2)} className={styles.tick} style={{ left: ORIGIN + t * zoom }} />
      ))}
    </div>
  );
});

const TONE_ICON: Record<Track['icon'], IconName> = {
  sun: 'sun',
  type: 'type',
  chip: 'chip',
  music: 'music',
  shape: 'shape',
  image: 'image',
};

/** One row of the layers column. */
function LayerRow({
  row,
  time,
  frame,
  armed,
  onExpand,
  onToggle,
  onArm,
  onJump,
  onKey,
}: {
  row: Row;
  time: number;
  frame: number;
  armed: boolean;
  onExpand: (track: Track) => void;
  onToggle: (track: Track, key: 'hidden' | 'locked') => void;
  onArm: (track: Track) => void;
  onJump: (times: number[], direction: -1 | 1, name: string) => void;
  onKey: (track: Track, param?: Param) => void;
}) {
  if (row.type === 'sections')
    return (
      <div className={styles.row} role="listitem" data-part="layer-row" data-kind="sections">
        <Icon name="sections" className={styles.sectionsIcon} />
        <span className={styles.rowName} data-col="sections">
          Sections
        </span>
      </div>
    );
  if (row.type === 'param') {
    const { track, param } = row;
    const keys = param.keyframes;
    const on = keys.some((k) => Math.abs(k - time) < frame / 2);
    return (
      <div className={styles.paramRow} role="listitem" data-part="layer-row" data-kind="param">
        <span className={styles.tree} aria-hidden="true" />
        <span className={styles.paramName}>{param.name}</span>
        <span className={styles.paramValue}>{param.value}</span>
        <button
          type="button"
          className={styles.navSmall}
          data-at="prev"
          aria-label={`${param.name}: previous keyframe`}
          onClick={() => onJump(keys, -1, param.name)}
        >
          <Icon name="prev" />
        </button>
        <button
          type="button"
          className={styles.keyToggle}
          data-at="param"
          data-animated={keys.length ? '' : undefined}
          aria-pressed={on}
          aria-label={`${param.name}: keyframe at the playhead`}
          onClick={() => onKey(track, param)}
        >
          <Icon name={on ? 'diamondFilled' : 'diamond'} />
        </button>
        <button
          type="button"
          className={styles.navSmall}
          data-at="next"
          aria-label={`${param.name}: next keyframe`}
          onClick={() => onJump(keys, 1, param.name)}
        >
          <Icon name="next" />
        </button>
      </div>
    );
  }
  const { track } = row;
  const keys = keysOf(track);
  const on = keys.some((k) => Math.abs(k - time) < frame / 2);
  const folding = track.kind !== 'layer';
  return (
    <div
      className={styles.row}
      role="listitem"
      data-part="layer-row"
      data-kind={track.kind}
      data-hidden={track.hidden ? '' : undefined}
      data-locked={track.locked ? '' : undefined}
    >
      {folding ? (
        <>
          <button
            type="button"
            className={styles.fold}
            aria-expanded={!!track.expanded}
            aria-label={`${track.name}: properties`}
            onClick={() => onExpand(track)}
          >
            <Icon name={track.expanded ? 'chevronDown' : 'chevronRight'} />
          </button>
          <span className={styles.tile} data-tone={track.tone} aria-hidden="true">
            <Icon name={TONE_ICON[track.icon]} />
          </span>
        </>
      ) : (
        <>
          <button
            type="button"
            className={styles.lock}
            aria-pressed={!!track.locked}
            aria-label={`Lock ${track.name}`}
            onClick={() => onToggle(track, 'locked')}
          >
            <Icon name="lock" />
          </button>
          <button
            type="button"
            className={styles.eye}
            aria-pressed={!track.hidden}
            aria-label={`Show ${track.name}`}
            onClick={() => onToggle(track, 'hidden')}
          >
            <Icon name={track.hidden ? 'eyeOff' : 'eye'} />
          </button>
          <span className={styles.swatch} data-tone={track.tone} aria-hidden="true" />
          <Icon name="tag" className={styles.tag} />
          <span className={styles.glyph} data-tone={track.tone} aria-hidden="true">
            <Icon name={TONE_ICON[track.icon]} />
          </span>
        </>
      )}
      <span className={styles.rowName} data-col={folding ? 'group' : 'layer'}>
        {track.name}
      </span>
      {track.kind !== 'audio' && (
        <span className={styles.nav}>
          <button
            type="button"
            className={styles.navBtn}
            aria-label={`${track.name}: previous keyframe`}
            onClick={() => onJump(keys, -1, track.name)}
          >
            <Icon name="prevKey" />
          </button>
          <button
            type="button"
            className={styles.keyToggle}
            aria-pressed={on}
            aria-label={`${track.name}: keyframe at the playhead`}
            onClick={() => onKey(track, track.expanded ? track.params?.[0] : undefined)}
          >
            <Icon name={on ? 'diamondFilled' : 'diamond'} />
          </button>
          <button
            type="button"
            className={styles.navBtn}
            aria-label={`${track.name}: next keyframe`}
            onClick={() => onJump(keys, 1, track.name)}
          >
            <Icon name="nextKey" />
          </button>
          <button
            type="button"
            className={styles.arm}
            aria-pressed={armed}
            aria-label={`${track.name}: record keyframes`}
            onClick={() => onArm(track)}
          >
            <Icon name={armed ? 'recordOn' : 'record'} />
          </button>
        </span>
      )}
    </div>
  );
}

/** The sections row and every track's lane: clips, keyframes, curves, waveform. */
const Lanes = memo(function Lanes({
  rows,
  sections,
  duration,
  zoom,
  introFrame,
  clipLead,
  selected,
  onClipDown,
  onClipKey,
  onKeyframe,
  onBackground,
}: {
  rows: Row[];
  sections: Section[];
  duration: number;
  zoom: number;
  introFrame: number;
  clipLead?: readonly number[];
  selected: string | null;
  onClipDown: (event: ReactPointerEvent<HTMLElement>, track: Track, clip: Clip) => void;
  onClipKey: (event: ReactKeyboardEvent<HTMLElement>, track: Track, clip: Clip) => void;
  onKeyframe: (t: number) => void;
  onBackground: (event: ReactPointerEvent<HTMLElement>) => void;
}) {
  const x = (t: number) => ORIGIN + t * zoom;
  return (
    <div className={styles.lanes} onPointerDown={onBackground}>
      {rows.map((row) => {
        if (row.type === 'sections')
          return (
            <div key={row.key} className={styles.lane} data-part="sections" aria-label="Sections">
              {sections.map((section, i) => {
                const end = sections[i + 1]?.start ?? duration;
                return (
                  <div
                    key={section.id}
                    className={styles.section}
                    data-part="section"
                    data-tone={section.tone}
                    data-first={i === 0 ? '' : undefined}
                    data-last={i === sections.length - 1 ? '' : undefined}
                    style={{ left: x(section.start), width: (end - section.start) * zoom }}
                  >
                    {i > 0 && (
                      <span className={styles.chip} aria-hidden="true">
                        <Icon name="transition" />
                      </span>
                    )}
                    <span className={styles.sectionLabel}>{section.label}</span>
                  </div>
                );
              })}
            </div>
          );
        if (row.type === 'param') {
          const { param } = row;
          const keys = param.keyframes;
          return (
            <div
              key={row.key}
              className={styles.paramLane}
              data-part="keyframe-lane"
              aria-label={`${param.name} keyframes`}
            >
              {keys.slice(0, -1).map((k, i) => {
                const dotted = param.holdFrom !== undefined && i >= param.holdFrom;
                return (
                  <span
                    key={`c${i}`}
                    className={styles.curve}
                    data-part="curve"
                    data-dotted={dotted ? '' : undefined}
                    aria-hidden="true"
                    style={
                      {
                        left: x(k),
                        width: (keys[i + 1] - k) * zoom,
                        '--draw': intro.curve(introFrame, i, dotted),
                      } as CSSProperties
                    }
                  />
                );
              })}
              {keys.map((k, i) => (
                <button
                  key={`k${i}`}
                  type="button"
                  className={styles.keyframe}
                  data-part="keyframe"
                  aria-label={`${param.name} keyframe at ${formatTime(k)}`}
                  style={{ left: x(k), '--pop': intro.keyframe(introFrame, i) } as CSSProperties}
                  onPointerDown={(event) => event.stopPropagation()}
                  onClick={() => onKeyframe(k)}
                />
              ))}
            </div>
          );
        }
        const { track, index } = row;
        return (
          <div
            key={row.key}
            className={styles.lane}
            data-part="track"
            data-hidden={track.hidden ? '' : undefined}
            data-locked={track.locked ? '' : undefined}
            aria-label={`${track.name} track`}
          >
            {track.clips.map((clip) => {
              const at = introFrame + (clipLead?.[index] ?? 0);
              const grown = intro.clip(at, index);
              const label = intro.label(at, index);
              return (
                <div
                  key={clip.id}
                  className={styles.clip}
                  data-part="clip"
                  data-tone={track.tone}
                  data-striped={track.striped ? '' : undefined}
                  data-audio={track.kind === 'audio' ? '' : undefined}
                  data-selected={selected === clip.id ? '' : undefined}
                  role="button"
                  tabIndex={0}
                  aria-pressed={selected === clip.id}
                  aria-label={`${clip.label ?? track.name}, ${spoken(clip.start)} to ${spoken(clip.end)}${track.locked ? ', locked' : ''}`}
                  style={
                    {
                      left: x(clip.start),
                      width: (clip.end - clip.start) * zoom,
                      '--grow': grown,
                      '--bloom': grown > 0 && grown < 1 ? Math.sin(Math.PI * grown) : 0,
                      '--label': label,
                    } as CSSProperties
                  }
                  onPointerDown={(event) => onClipDown(event, track, clip)}
                  onKeyDown={(event) => onClipKey(event, track, clip)}
                >
                  {track.kind === 'audio' && (
                    <Waveform
                      seed={index + 7}
                      start={clip.start}
                      end={clip.end}
                      accent={track.accent ?? Infinity}
                    />
                  )}
                  {track.kind === 'audio' && <span className={styles.grip} aria-hidden="true" />}
                  <span className={styles.clipLabel} aria-hidden="true">
                    <Icon name={TONE_ICON[track.icon]} className={styles.clipIcon} />
                    <span>{clip.label ?? track.name}</span>
                  </span>
                  {(track.keyframes ?? [])
                    .filter((k) => k >= clip.start && k <= clip.end)
                    .map((k) => (
                      <span
                        key={k}
                        className={styles.clipKey}
                        aria-hidden="true"
                        style={{ left: (k - clip.start) * zoom }}
                      />
                    ))}
                </div>
              );
            })}
          </div>
        );
      })}
      <div className={styles.foot} />
    </div>
  );
});

/** An audio clip's waveform: quiet until its accent, then about twice as loud. */
const Waveform = memo(function Waveform({
  seed,
  start,
  end,
  accent,
}: {
  seed: number;
  start: number;
  end: number;
  accent: number;
}) {
  const d = useMemo(() => {
    const random = seeded(seed * 9973);
    const step = 0.025;
    const top: string[] = [];
    const bottom: string[] = [];
    let smooth = 0.5;
    for (let t = start; t <= end + 1e-6; t += step) {
      smooth = smooth * 0.55 + random() * 0.45;
      const loud = t < accent ? 0.12 : 0.3 + 0.06 * Math.sin((t - accent) * 9);
      const ramp = Math.min(1, Math.max(0, (t - accent + 0.08) / 0.16));
      const a = (t < accent - 0.08 ? 0.12 : 0.12 + (loud - 0.12) * ramp) * (0.6 + smooth * 0.6);
      const px = ((t - start) * 100).toFixed(1);
      top.push(`${px} ${(12 - a * 24).toFixed(2)}`);
      bottom.unshift(`${px} ${(12 + a * 24).toFixed(2)}`);
    }
    return `M${top.join('L')}L${bottom.join('L')}Z`;
  }, [seed, start, end, accent]);
  return (
    <svg
      className={styles.wave}
      data-part="waveform"
      viewBox={`0 0 ${((end - start) * 100).toFixed(1)} 24`}
      preserveAspectRatio="none"
      aria-hidden="true"
    >
      <path d={d} />
    </svg>
  );
});

export { RATE };
```

### video-timeline/video-timeline.module.css

```css
/* Video timeline (video-timeline.tsx): a dark editor panel on a black stage — the layers column,
   the top bar with timecode and transport, the ruler, sections and tracks. Sizes are px at 13 px
   type: 46 px top bar, a 36 px ruler, 32 px rows, 27 px property lanes, a 282 px layers column.

   Colours are the tokens below, light-dark(light, dark): the editor takes the color-scheme it
   inherits from the page (light unless the page says otherwise), or the one its colorScheme prop
   sets (data-color-scheme). Clip colours are the same on both.

   Presenting (a frame-by-frame renderer, or the autoplay film) turns every transition off: each
   frame is drawn from the state it is handed. The play button reads its effects from custom
   properties (--hover, --lift, --charge, --beams, --tint, --morph, --ring-*), set inline.

   For a 3D camera: --vt-lift-depth moves the lifted play button towards the viewer (translateZ),
   and --vt-beams-transform stands the light beams up (e.g. rotateX(-90deg)); both do nothing flat. */

.root {
  --stage: #f1f2f4;
  --panel: #ffffff;
  --surface: #f7f8f9;
  --layers: #fbfbfc;
  --head: #f4f5f6;
  --track-bg: #f2f3f5;
  --ruler-bg: #f7f8f9;
  --edge: rgb(0 0 0 / 0.1);
  --line: rgb(0 0 0 / 0.07);
  --second: rgb(0 0 0 / 0.12);
  --minor: rgb(0 0 0 / 0.2);
  --ink: #1b1f23;
  --ink-2: #4a5056;
  --muted: #6c7277;
  --dim: #9aa0a5;
  --faint: #b5bac0;
  --ruler-ink: #73787d;
  --group: #eceef1;
  --group-edge: rgb(0 0 0 / 0.08);
  --picked: #dfeafc;
  --picked-edge: #9fc0ef;
  --picked-ink: #2c74e0;
  --play-rest: #e1e3e6;
  --play-hover: #d4d7db;
  --play-ink: #1b1f23;
  --tinted: #d9e8fc;
  --tinted-ink: #1f6fe0;
  --socket: rgb(0 0 0 / 0.1);
  --chip: #1b1f23;
  --chip-ink: #ffffff;
  --sec-blue: #d4e4fa;
  --sec-blue-ink: #1d4f91;
  --sec-teal: #cfeee8;
  --sec-teal-ink: #0f6b5c;
  --sec-purple: #e6dcf8;
  --sec-purple-ink: #5b3c9a;
  --tile-pink: #fbe3ee;
  --tile-pink-edge: #e9a9c6;
  --tile-green: #dcf3e5;
  --tile-green-edge: #8ccaa5;
  --tree: #c4c8cc;
  --menu: #ffffff;
  --menu-hover: #f0f2f4;
  --shadow: rgb(0 0 0 / 0.08);
  --focus: #2f7bff;
  /* The same on both schemes. */
  --glow-blue: #2f7bff;
  --playhead: #f2503a;
  --key: #fbe022;
  --key-edge: #a37f00;
  --key-ring: #f2cf1d;
  --key-amber: #f59f0b;
  --curve: #f2b6cf;
  --curve-dot: #b8909f;
  --hero-a: #46d2ff;
  --hero-b: #2f9bf5;
  --hero-c: #3156e8;

  position: relative;
  container-type: inline-size;
  display: grid;
  justify-items: center;
  width: 100%;
  min-height: 420px;
  padding: 96px clamp(12px, 4%, 24px) 48px;
  box-sizing: border-box;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  background: var(--stage);
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', Inter, 'Segoe UI', sans-serif;
  font-size: 13px;
  line-height: 1.2;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --stage: light-dark(#f1f2f4, #000000);
    --panel: light-dark(#ffffff, #0b0f10);
    --surface: light-dark(#f7f8f9, #0f1315);
    --layers: light-dark(#fbfbfc, #0a0d0e);
    --head: light-dark(#f4f5f6, #090c0d);
    --track-bg: light-dark(#f2f3f5, #020304);
    --ruler-bg: light-dark(#f7f8f9, #010203);
    --edge: light-dark(rgb(0 0 0 / 0.1), rgb(255 255 255 / 0.08));
    --line: light-dark(rgb(0 0 0 / 0.07), rgb(255 255 255 / 0.055));
    --second: light-dark(rgb(0 0 0 / 0.12), #1f2225);
    --minor: light-dark(rgb(0 0 0 / 0.2), #3a3d40);
    --ink: light-dark(#1b1f23, #e2e4e6);
    --ink-2: light-dark(#4a5056, #c9ccd0);
    --muted: light-dark(#6c7277, #a2a7ac);
    --dim: light-dark(#9aa0a5, #8a9095);
    --faint: light-dark(#b5bac0, #5c6166);
    --ruler-ink: light-dark(#73787d, #8c8d90);
    --group: light-dark(#eceef1, #161a1d);
    --group-edge: light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.07));
    --picked: light-dark(#dfeafc, #22324a);
    --picked-edge: light-dark(#9fc0ef, #3d5a80);
    --picked-ink: light-dark(#2c74e0, #7cc4ff);
    --play-rest: light-dark(#e1e3e6, #393e3f);
    --play-hover: light-dark(#d4d7db, #46494d);
    --play-ink: light-dark(#1b1f23, #ffffff);
    --tinted: light-dark(#d9e8fc, #143250);
    --tinted-ink: light-dark(#1f6fe0, #3b8cff);
    --socket: light-dark(rgb(0 0 0 / 0.1), rgb(255 255 255 / 0.1));
    --chip: light-dark(#1b1f23, #0c0e0f);
    --sec-blue: light-dark(#d4e4fa, #183257);
    --sec-blue-ink: light-dark(#1d4f91, #cfe0ff);
    --sec-teal: light-dark(#cfeee8, #062f2e);
    --sec-teal-ink: light-dark(#0f6b5c, #6fd8c6);
    --sec-purple: light-dark(#e6dcf8, #2a1c3f);
    --sec-purple-ink: light-dark(#5b3c9a, #b49ae0);
    --tile-pink: light-dark(#fbe3ee, #3a1d2b);
    --tile-pink-edge: light-dark(#e9a9c6, #8a4a68);
    --tile-green: light-dark(#dcf3e5, #15301f);
    --tile-green-edge: light-dark(#8ccaa5, #2f6a46);
    --tree: light-dark(#c4c8cc, #4e5358);
    --menu: light-dark(#ffffff, #171b1e);
    --menu-hover: light-dark(#f0f2f4, #22272b);
    --shadow: light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.5));
  }
}
.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;
}

.root svg {
  display: block;
  width: 14px;
  height: 14px;
  flex: none;
}

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

:where(.root button) {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.root :focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* ———————————————— The panel ———————————————— */

.editor {
  --name-w: 282px;
  position: relative;
  display: grid;
  grid-template-columns: var(--name-w) minmax(0, 1fr);
  grid-template-rows: 46px auto;
  grid-template-areas: 'tools bar' 'layers timeline';
  align-self: start;
  border-radius: 14px;
  background: var(--panel);
  box-shadow:
    0 0 0 1px var(--edge),
    0 12px 32px var(--shadow);
  transition: box-shadow 0.4s ease;
}
/* The steady glow: the panel's focus, or the outline the presentation sets. */
.editor:focus-within,
.editor[data-glow] {
  --glow: 1;
}
.editor:is(:focus-within, [data-glow]) {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--glow-blue) calc(var(--glow) * 95%), var(--edge)),
    0 0 calc(var(--glow) * 14px)
      color-mix(in srgb, var(--glow-blue) calc(var(--glow) * 60%), transparent),
    0 12px 32px var(--shadow);
}
.editor:focus-within:not([data-glow]) {
  --glow: 1;
}

.outline {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.root .outline {
  width: 100%;
  height: 100%;
}
.cometHead,
.cometTail {
  fill: none;
  stroke-linecap: round;
}
.cometHead {
  stroke: #d9ecff;
  stroke-width: 2.2;
  stroke-dasharray: 22 978;
  filter: drop-shadow(0 0 3px #5aa8ff) drop-shadow(0 0 8px #2f7bff);
}
.cometTail {
  stroke: #4b97ff;
  stroke-width: 1.6;
  stroke-dasharray: 110 890;
  opacity: 0.55;
  filter: drop-shadow(0 0 4px #2f7bff);
}

/* ———————————————— Top bar ———————————————— */

.tools,
.bar {
  position: relative;
  display: flex;
  align-items: center;
  height: 46px;
  background: var(--surface);
}
.tools {
  grid-area: tools;
  gap: 7px;
  padding-left: 5px;
  border-radius: 14px 0 0 0;
}
.bar {
  grid-area: bar;
  min-width: 0;
  border-radius: 0 14px 0 0;
}

.toolGroup {
  display: flex;
  gap: 2px;
  height: 32px;
  padding: 1px;
  box-sizing: border-box;
  border-radius: 10px;
  background: var(--group);
  box-shadow: inset 0 0 0 1px var(--group-edge);
}
.tool {
  display: grid;
  place-items: center;
  width: 29px;
  height: 30px;
  border-radius: 9px;
  color: var(--muted);
}
.tool[aria-pressed='true'] {
  background: var(--picked);
  box-shadow: inset 0 0 0 1px var(--picked-edge);
  color: var(--picked-ink);
}
.tool:hover:not([aria-pressed='true']) {
  color: var(--ink);
}
.root .tool svg {
  width: 15px;
  height: 15px;
}

.addWrap {
  position: relative;
}
.add {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  width: 83px;
  padding: 0 0 0 11px;
  box-sizing: border-box;
  border-radius: 10px;
  background: var(--group);
  box-shadow: inset 0 0 0 1px var(--group-edge);
  color: var(--ink);
  font-size: 16.5px;
  font-weight: 700;
  letter-spacing: 0.005em;
}
.add:hover {
  background: color-mix(in srgb, var(--group) 80%, var(--ink) 6%);
}
.root .addIcon {
  width: 14px;
  height: 14px;
}
.root .addChevron {
  width: 11px;
  height: 11px;
  color: var(--dim);
}

.menu {
  position: absolute;
  z-index: 5;
  top: calc(100% + 6px);
  left: 0;
  display: grid;
  min-width: 190px;
  padding: 4px;
  border-radius: 10px;
  background: var(--menu);
  box-shadow:
    0 0 0 1px var(--edge),
    0 10px 24px var(--shadow);
}
.menuItem {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 8px;
  border-radius: 7px;
  color: var(--ink);
  text-align: left;
  font-weight: 500;
}
.menuItem:hover,
.menuItem:focus-visible {
  background: var(--menu-hover);
}
.menuSwatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background: #f2503a;
}
.root .menuIcon {
  width: 12px;
  height: 12px;
  color: var(--key-ring);
}

.timecode {
  flex: none;
  width: 124px;
  padding-right: 10px;
  box-sizing: border-box;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums slashed-zero;
  text-align: right;
  white-space: nowrap;
}
.transport {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 1px;
  box-sizing: border-box;
  border-radius: 10px;
  background: var(--group);
  box-shadow: inset 0 0 0 1px var(--group-edge);
}
.tbtn {
  display: grid;
  place-items: center;
  width: 31px;
  height: 30px;
  border-radius: 8px;
  color: var(--ink);
}
.tbtn:hover {
  background: var(--menu-hover);
}
.root .tbtn svg {
  width: 14px;
  height: 14px;
}
.loop {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 32px;
  margin-left: 10px;
  border-radius: 9px;
  color: var(--muted);
  box-shadow: inset 0 0 0 1px var(--group-edge);
}
.loop[aria-pressed='true'] {
  background: var(--tinted);
  box-shadow: none;
  color: var(--tinted-ink);
}
.root .loop svg {
  width: 15px;
  height: 15px;
}
.total {
  flex: none;
  margin-left: 14px;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums slashed-zero;
  white-space: nowrap;
}

/* ———————— The play button: rest, hover, lift, charge, beams, press ring ———————— */

.playCell {
  position: relative;
  display: grid;
  place-items: center;
  width: 38px;
  height: 30px;
  margin: 0 2px;
}
.socket {
  position: absolute;
  inset: -2px -3px;
  border-radius: 10px;
  box-shadow: inset 0 0 0 1px var(--socket);
  opacity: calc(var(--lift) * 3);
}
.play {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 36px;
  height: 30px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--play-hover) calc(var(--hover) * 100%), var(--play-rest));
  color: color-mix(in srgb, var(--tinted-ink) calc(var(--tint) * 100%), var(--play-ink));
  box-shadow:
    0 calc(var(--lift) * 8px) calc(var(--lift) * 16px) rgb(0 0 0 / calc(var(--lift) * 0.45)),
    0 0 calc(var(--charge) * 18px) rgb(52 170 255 / calc(var(--charge) * 0.55));
  transform: translate3d(0, calc(var(--lift) * -4px), calc(var(--lift) * var(--vt-lift-depth, 0px)))
    scale(calc(1 + var(--lift) * 0.08));
  transition:
    transform 0.16s ease-out,
    background-color 0.12s;
}
/* The tint (playing) and the charge (the hero blue) are layers over the grey. */
.playFace,
.play::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
.play::before {
  background: var(--tinted);
  opacity: var(--tint);
}
.playFace {
  background:
    linear-gradient(180deg, rgb(255 255 255 / 0.32), rgb(255 255 255 / 0) 46%),
    linear-gradient(128deg, var(--hero-a) 0%, var(--hero-b) 48%, var(--hero-c) 100%);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.55),
    inset 0 -2px 3px rgb(20 40 170 / 0.35);
  opacity: var(--charge);
}
.playIcon,
.pauseIcon {
  grid-area: 1 / 1;
  position: relative;
}
.root .playIcon,
.root .pauseIcon {
  width: 13px;
  height: 13px;
}
.playIcon {
  opacity: calc(1 - var(--morph));
  transform: scale(calc(1 - var(--morph) * 0.35));
  filter: blur(calc(var(--morph) * (1 - var(--morph)) * 3px));
}
.pauseIcon {
  opacity: var(--morph);
  transform: scale(calc(0.65 + var(--morph) * 0.35));
  filter: blur(calc(var(--morph) * (1 - var(--morph)) * 3px));
}
.play .playIcon,
.play .pauseIcon {
  color: color-mix(in srgb, #ffffff calc(var(--charge) * 100%), currentColor);
}
.ring {
  position: absolute;
  inset: calc(-2px - var(--ring-gap)) calc(-1px - var(--ring-gap));
  border: 2px solid
    color-mix(
      in srgb,
      rgb(255 255 255 / var(--ring-alpha)) calc(var(--ring-white) * 100%),
      rgb(64 210 255 / var(--ring-alpha))
    );
  border-radius: calc(10px + var(--ring-gap));
  background: rgb(56 200 255 / var(--ring-fill));
  box-shadow: 0 0 10px rgb(64 200 255 / calc(var(--ring-alpha) * 0.6));
  pointer-events: none;
}
.beams {
  position: absolute;
  z-index: 0;
  bottom: 50%;
  left: 50%;
  display: flex;
  justify-content: center;
  width: 46px;
  height: 92px;
  margin-left: -23px;
  opacity: var(--beams);
  transform: var(--vt-beams-transform, none);
  transform-origin: 50% 100%;
  pointer-events: none;
  filter: blur(2px);
}
.beams i {
  flex: 1;
  margin: 0 -1px;
  background: linear-gradient(to top, var(--beam) 0%, transparent 92%);
  animation: flicker 2.6s ease-in-out infinite alternate;
}
.beams i:nth-child(1) {
  --beam: rgb(63 224 208 / 0.4);
  animation-delay: -0.4s;
}
.beams i:nth-child(2) {
  --beam: rgb(57 198 240 / 0.62);
  animation-delay: -1.3s;
}
.beams i:nth-child(3) {
  --beam: rgb(74 168 255 / 0.55);
  animation-delay: -2.1s;
}
.beams i:nth-child(4) {
  --beam: rgb(58 123 255 / 0.6);
  animation-delay: -0.9s;
}
.beams i:nth-child(5) {
  --beam: rgb(47 85 232 / 0.42);
  animation-delay: -1.7s;
}
@keyframes flicker {
  from {
    opacity: 1;
  }
  to {
    opacity: 0.62;
  }
}

/* ———————————————— Layers column ———————————————— */

.layers {
  grid-area: layers;
  position: relative;
  min-width: 0;
  overflow: hidden;
  background: var(--layers);
  border-right: 1px solid var(--line);
  border-radius: 0 0 0 14px;
}
.head,
.row,
.paramRow {
  position: relative;
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
}
.head {
  height: 36px;
  background: var(--head);
  color: var(--dim);
}
.head > svg,
.head > span {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
}
.hLock {
  left: 8px;
}
.hEye {
  left: 30px;
}
.hPin {
  left: 52px;
}
.hTag {
  left: 75px;
}
.hName {
  left: 120px;
  color: var(--muted);
  font-size: 15px;
  font-weight: 500;
}
.row {
  height: 32px;
}
.paramRow {
  height: 27px;
}
.row > *,
.paramRow > * {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
}
.rowName {
  left: 130px;
  font-size: 13.5px;
  max-width: calc(100% - 130px - 72px);
  overflow: hidden;
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.rowName[data-col='group'] {
  left: 58px;
  max-width: calc(100% - 58px - 72px);
}
.rowName[data-col='sections'] {
  left: 41px;
}
.root .sectionsIcon {
  left: 17px;
  width: 15px;
  height: 15px;
  color: #3fd6d0;
}
.fold {
  left: 3px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 5px;
  color: var(--dim);
}
.root .fold svg {
  width: 13px;
  height: 13px;
}
.tile {
  left: 24px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  box-sizing: border-box;
  border-radius: 6px;
}
.tile[data-tone='pink'] {
  background: var(--tile-pink);
  box-shadow: inset 0 0 0 1px var(--tile-pink-edge);
  color: #f5abcc;
}
.tile[data-tone='green'] {
  background: var(--tile-green);
  box-shadow: inset 0 0 0 1px var(--tile-green-edge);
  color: #c48cf5;
}
.tile:not([data-tone='pink'], [data-tone='green']) {
  background: var(--group);
  box-shadow: inset 0 0 0 1px var(--group-edge);
}
.lock,
.eye {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 5px;
  color: var(--dim);
}
.lock {
  left: 5px;
}
.eye {
  left: 28px;
}
.lock[aria-pressed='true'] {
  color: var(--ink);
}
.eye[aria-pressed='false'] {
  color: var(--faint);
}
.lock:hover,
.eye:hover {
  color: var(--ink);
}
.root .eye svg {
  width: 16px;
  height: 16px;
}
.swatch {
  left: 54px;
  width: 16px;
  height: 16px;
  border-radius: 4px;
}
.swatch[data-tone='yellow'] {
  background: #facf73;
}
.swatch[data-tone='magenta'] {
  background: #f79cfb;
}
.swatch[data-tone='pink'] {
  background: #fab6dd;
}
.swatch[data-tone='green'] {
  background: #9be2ba;
}
.swatch[data-tone='blue'] {
  background: #8cc4ff;
}
.swatch[data-tone='purple'] {
  background: #c3a8f5;
}
.root .tag {
  left: 76px;
  color: var(--faint);
}
.glyph {
  left: 105px;
  display: grid;
}
.root .glyph svg {
  width: 16px;
  height: 16px;
}
.glyph[data-tone='yellow'] {
  color: #f5a524;
}
.glyph[data-tone='magenta'] {
  color: #e357f0;
}
.glyph[data-tone='pink'] {
  color: #f07bb3;
}
.glyph[data-tone='green'] {
  color: #3fc47d;
}
.glyph[data-tone='blue'] {
  color: #4aa8ff;
}
.glyph[data-tone='purple'] {
  color: #a77ff0;
}
.row[data-hidden] .rowName,
.row[data-hidden] .swatch,
.row[data-hidden] .glyph {
  opacity: 0.4;
}

.nav {
  left: 207px;
  display: flex;
  align-items: center;
  height: 20px;
}
.navBtn,
.keyToggle,
.navSmall,
.arm {
  display: grid;
  place-items: center;
  width: 16px;
  height: 20px;
  border-radius: 4px;
  color: var(--ink-2);
}
.root .navBtn svg {
  width: 17px;
  height: 17px;
}
.keyToggle {
  margin: 0 1px;
  color: var(--key-ring);
}
.root .keyToggle svg {
  width: 15px;
  height: 15px;
}
.keyToggle[data-animated] {
  color: var(--key-amber);
}
.arm {
  margin-left: 3px;
  color: var(--faint);
}
.root .arm svg {
  width: 13px;
  height: 13px;
}
.arm[aria-pressed='true'] {
  color: #f2503a;
}
.navBtn:hover,
.navSmall:hover,
.keyToggle:hover {
  background: var(--menu-hover);
}

.tree {
  left: 36px;
  width: 9px;
  height: 18px;
  margin-top: -8px;
  box-sizing: border-box;
  border-left: 1px solid var(--tree);
  border-bottom: 1px solid var(--tree);
  border-bottom-left-radius: 5px;
}
.paramName {
  left: 50px;
  color: var(--muted);
  font-weight: 500;
}
.paramValue {
  right: calc(100% - 229px);
  color: var(--faint);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.navSmall {
  color: var(--dim);
}
.navSmall[data-at='prev'] {
  left: 233px;
}
.keyToggle[data-at='param'] {
  left: 246px;
  margin: 0;
}
.navSmall[data-at='next'] {
  left: 261px;
}
.root .navSmall svg {
  width: 12px;
  height: 12px;
}

/* ———————————————— Timeline column ———————————————— */

.timeline {
  grid-area: timeline;
  position: relative;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  border-radius: 0 0 14px 0;
  background: var(--track-bg);
  scrollbar-width: thin;
  scrollbar-color: var(--faint) transparent;
}
.timeline:focus-visible {
  outline-offset: -2px;
}
.content {
  position: relative;
}
.ruler {
  position: relative;
  height: 36px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--edge);
  background: var(--ruler-bg);
  cursor: ew-resize;
  touch-action: none;
}
.second {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--second);
}
.secondLabel {
  position: absolute;
  top: 3px;
  left: 4px;
  color: var(--ruler-ink);
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums slashed-zero;
  white-space: nowrap;
}
.tick {
  position: absolute;
  bottom: 0;
  width: 1px;
  height: 4px;
  background: var(--minor);
}

.marker {
  position: absolute;
  top: 20px;
  display: flex;
}
.pin {
  position: absolute;
  top: 1px;
  left: -6.5px;
  width: 13px;
  height: 14px;
  border-radius: 3px 3px 0 0;
  background: var(--tone);
  clip-path: polygon(0 0, 100% 0, 100% 58%, 50% 100%, 0 58%);
}
.flag {
  position: absolute;
  left: 8px;
  height: 13px;
  padding: 0 4px;
  border-radius: 4px;
  background: var(--tone);
  color: var(--tone-ink);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: -0.15px;
  line-height: 13px;
  font-variant-numeric: slashed-zero;
  white-space: nowrap;
}
.marker[data-tone='blue'],
.markerLine[data-tone='blue'] {
  --tone: #4aa8ff;
  --tone-ink: #0d3a6e;
}
.marker[data-tone='green'],
.markerLine[data-tone='green'] {
  --tone: #22d457;
  --tone-ink: #04380f;
}
.marker[data-tone='red'],
.markerLine[data-tone='red'] {
  --tone: #ff6b5a;
  --tone-ink: #4a0d06;
}
.marker[data-tone='purple'],
.markerLine[data-tone='purple'] {
  --tone: #b48cff;
  --tone-ink: #2a1160;
}
.markerLine {
  position: absolute;
  top: 36px;
  bottom: 0;
  width: 1px;
  background: color-mix(in srgb, var(--tone) 45%, transparent);
  pointer-events: none;
}

.lanes {
  position: relative;
}
.lane,
.paramLane {
  position: relative;
  height: 32px;
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
}
.paramLane {
  height: 27px;
}
.foot {
  height: 24px;
}

.section {
  position: absolute;
  top: 5px;
  display: flex;
  align-items: center;
  height: 22px;
  min-width: 0;
  overflow: hidden;
  box-sizing: border-box;
  padding-left: 9px;
  background: var(--sec);
  color: var(--sec-ink);
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}
.section[data-first] {
  border-radius: 6px 0 0 6px;
}
.section[data-last] {
  border-radius: 0 6px 6px 0;
}
.section[data-first][data-last] {
  border-radius: 6px;
}
.section:not([data-first]) {
  padding-left: 0;
  overflow: visible;
}
.section[data-tone='blue'] {
  --sec: var(--sec-blue);
  --sec-ink: var(--sec-blue-ink);
}
.section[data-tone='teal'] {
  --sec: var(--sec-teal);
  --sec-ink: var(--sec-teal-ink);
}
.section[data-tone='purple'] {
  --sec: var(--sec-purple);
  --sec-ink: var(--sec-purple-ink);
}
.chip {
  display: grid;
  place-items: center;
  flex: none;
  width: 21px;
  height: 21px;
  margin: 0 4px 0 -3px;
  border-radius: 6px;
  background: var(--chip);
  color: var(--chip-ink);
}
.root .chip svg {
  width: 13px;
  height: 13px;
}
.sectionLabel {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Clips: grown in by clip-path, so the rounded right end shows as they grow. */
.clip {
  position: absolute;
  top: 4px;
  height: 24px;
  box-sizing: border-box;
  border-radius: 6px;
  overflow: hidden;
  background: var(--fill);
  color: var(--clip-ink);
  clip-path: inset(0 calc((1 - var(--grow)) * 100%) 0 0 round 6px);
  filter: brightness(calc(1 + var(--bloom) * 0.12)) saturate(calc(1 + var(--bloom) * 0.3));
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.clip::before,
.clip::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: ew-resize;
}
.clip::before {
  left: 0;
}
.clip::after {
  right: 0;
}
.clip:active {
  cursor: grabbing;
}
.editor[data-tool='razor'] .clip,
.editor[data-tool='razor'] .clip::before,
.editor[data-tool='razor'] .clip::after {
  cursor: crosshair;
}
.lane[data-locked] .clip,
.lane[data-locked] .clip::before,
.lane[data-locked] .clip::after {
  cursor: not-allowed;
}
.lane[data-hidden] .clip {
  opacity: 0.32;
}
.clip[data-striped] {
  background: repeating-linear-gradient(
    135deg,
    var(--fill) 0 2.6px,
    var(--fill-2) 3.6px 6.1px,
    var(--fill) 7.1px
  );
}
.clip[data-selected] {
  box-shadow: inset 0 0 0 1.5px var(--clip-ink);
}
.clip:focus-visible {
  outline-offset: 1px;
}
.clip[data-tone='pink'] {
  --fill: #fab6dd;
  --fill-2: #f098c8;
  --clip-ink: #6d2a52;
  --clip-icon: #c25f92;
}
.clip[data-tone='yellow'] {
  --fill: #f9cf79;
  --fill-2: #f4bf55;
  --clip-ink: #6b4413;
  --clip-icon: #b5741f;
}
.clip[data-tone='magenta'] {
  --fill: #f99cff;
  --fill-2: #ee7cf0;
  --clip-ink: #6d1a6e;
  --clip-icon: #8a2a8e;
}
.clip[data-tone='green'] {
  --fill: #9be2ba;
  --fill-2: #82d6a6;
  --clip-ink: #0f4a2b;
  --clip-icon: #1d6a41;
}
.clip[data-tone='blue'] {
  --fill: #a9d2ff;
  --fill-2: #8cc0fb;
  --clip-ink: #0f3a6a;
  --clip-icon: #2a6bb5;
}
.clip[data-tone='purple'] {
  --fill: #cdb6fa;
  --fill-2: #b99cf3;
  --clip-ink: #3a1d72;
  --clip-icon: #6a44b8;
}
.clipLabel {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 11px;
  display: flex;
  align-items: center;
  gap: 7px;
  padding-right: 8px;
  font-size: 12.6px;
  font-weight: 600;
  white-space: nowrap;
  opacity: var(--label);
  pointer-events: none;
}
.clip[data-audio] .clipLabel {
  left: 15px;
  padding-left: 6px;
  margin-left: -6px;
  background: var(--fill);
}
.root .clipIcon {
  width: 13px;
  height: 13px;
  color: var(--clip-icon);
}
.grip {
  position: absolute;
  top: 9px;
  left: 3px;
  width: 6px;
  height: 6px;
  border-radius: 1px;
  background: #2f9a62;
}
.root .wave {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: #2f9a62;
}
.clipKey {
  position: absolute;
  top: 50%;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 1.5px;
  background: var(--key);
  box-shadow: 0 0 0 1px var(--key-edge);
  rotate: 45deg;
}

.curve {
  position: absolute;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background: var(--curve);
  clip-path: inset(0 calc((1 - var(--draw)) * 100%) 0 0);
  pointer-events: none;
}
.curve[data-dotted] {
  background: repeating-linear-gradient(to right, var(--curve-dot) 0 2px, transparent 2px 5.5px);
}
.keyframe {
  position: absolute;
  top: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
}
.keyframe::before {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 1.5px;
  background: var(--key);
  transform: rotate(45deg) scale(var(--pop));
}
.keyframe:hover::before {
  box-shadow: 0 0 0 1.5px #ffffff;
}

/* The playhead: its head in the ruler, its line down through the tracks. */
.playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
  pointer-events: none;
}
.head2 {
  position: absolute;
  top: 1px;
  left: -9px;
  width: 18px;
  height: 19px;
  pointer-events: auto;
  cursor: ew-resize;
  touch-action: none;
}
.root .head2 svg {
  width: 18px;
  height: 19px;
  fill: var(--playhead);
}
.head2:focus-visible {
  outline-offset: 2px;
  border-radius: 4px;
}
.line {
  position: absolute;
  top: 18px;
  bottom: 0;
  left: -1px;
  width: 2px;
  background: var(--playhead);
}

/* ———————————————— Presenting: no transitions, every frame drawn as given ———————————————— */

.root[data-presenting] *,
.root[data-presenting] *::before,
.root[data-presenting] *::after {
  transition: none !important;
  animation-play-state: paused !important;
}
.root[data-presenting] .beams i {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .root *,
  .root *::before,
  .root *::after {
    transition: none !important;
    animation: none !important;
  }
}

/* ———————————————— Narrow stages ———————————————— */

@container (max-width: 700px) {
  .editor {
    --name-w: 156px;
    grid-template-rows: 46px 46px auto;
    grid-template-areas: 'tools tools' 'bar bar' 'layers timeline';
  }
  .tools {
    border-radius: 14px 14px 0 0;
  }
  .bar {
    border-radius: 0;
    border-top: 1px solid var(--line);
    padding-left: 4px;
  }
  .timecode {
    width: auto;
    padding: 0 8px 0 4px;
  }
  .head > :not(.hName),
  .nav,
  .swatch,
  .root .tag,
  .paramValue,
  .navSmall,
  .keyToggle[data-at='param'] {
    display: none;
  }
  .hName {
    left: 10px;
  }
  .glyph {
    left: 50px;
  }
  .rowName,
  .rowName[data-col='group'] {
    left: 70px;
    max-width: calc(100% - 76px);
  }
  .rowName[data-col='sections'] {
    left: 34px;
  }
  .root .sectionsIcon {
    left: 12px;
  }
  .lock {
    left: 3px;
  }
  .eye {
    left: 24px;
  }
  .tile {
    left: 24px;
  }
  .rowName[data-col='group'] {
    left: 56px;
    max-width: calc(100% - 60px);
  }
  .paramName {
    left: 46px;
  }
}
@container (max-width: 420px) {
  .total {
    display: none;
  }
  .loop {
    margin-left: 6px;
  }
}
```

### video-timeline/choreography.ts

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

/*
 * The timeline's motion, as numbers: when each part of the entrance plays, how the play button
 * lifts, charges and is pressed, and a whole short film of the editor as a function of time.
 *
 * Everything is counted in frames of a 60 fps clock, the rate the motion was measured at, and
 * turned into progress values (0 to 1) by pure functions. The component draws whatever state it is
 * handed, so a renderer that steps frame by frame gets the exact same picture every time.
 */

/** The clock the motion is written in. */
export const RATE = 60;

/** How long the entrance lasts, in frames: clips, labels, keyframes, curves, markers, playhead. */
export const INTRO_FRAMES = 152;

/** The short film's length in seconds (see `filmState`). */
export const FILM_SECONDS = 740 / RATE;

/** The state a frame-by-frame renderer hands the component (its `presentation` prop). */
export type Presentation = {
  /** Where the playhead is, in seconds. */
  time: number;
  /** Shows pause instead of play. */
  playing: boolean;
  /** The entrance, 0 (empty tracks) to 1 (settled). */
  intro?: number;
  /** What the timecode reads, when it lags the playhead; `time` without it. */
  readout?: number;
  /** The playhead's opacity; without it, the entrance's own fade. */
  playhead?: number;
  /** The play button's playing look, a blue tint (0 to 1); without it, 1 while playing. */
  tint?: number;
  /** The play button's hover look (0 to 1). */
  hover?: number;
  /** The play button lifted off its socket (0 to 1). */
  lift?: number;
  /** The play button charged: its blue gradient (0 to 1). */
  charge?: number;
  /** The light beams over the play button (0 to 1); without it, `charge`. */
  beams?: number;
  /** Seconds since the play button was pressed: its icon swap and its ring. */
  press?: number | null;
  /** The panel's outline: a light running round it, then a steady glow (0 to 1). */
  outline?: number;
  /** Frames each track's clip (by track order) runs ahead of the entrance; none without it. */
  clipLead?: readonly number[];
};

const clamp = (value: number) => Math.min(1, Math.max(0, value));

/** A CSS-style cubic Bézier easing. */
export function bezier(x1: number, y1: number, x2: number, y2: number) {
  const cx = 3 * x1;
  const bx = 3 * (x2 - x1) - cx;
  const ax = 1 - cx - bx;
  const cy = 3 * y1;
  const by = 3 * (y2 - y1) - cy;
  const ay = 1 - cy - by;
  const x = (t: number) => ((ax * t + bx) * t + cx) * t;
  const y = (t: number) => ((ay * t + by) * t + cy) * t;
  return (progress: number) => {
    const p = clamp(progress);
    let lo = 0;
    let hi = 1;
    for (let i = 0; i < 24; i++) {
      const mid = (lo + hi) / 2;
      if (x(mid) < p) lo = mid;
      else hi = mid;
    }
    return y((lo + hi) / 2);
  };
}

/** A clip's growth: slow off its left edge, fast through the middle, a long settle. */
export const grow = bezier(0.3, 0, 0.3, 1);
export const easeOut = bezier(0.2, 0.7, 0.3, 1);
export const easeIn = bezier(0.5, 0, 0.9, 0.6);
const easeInOut = bezier(0.45, 0, 0.35, 1);

/** Progress of a span of frames [from, from + length) at frame `at`. */
export const span = (at: number, from: number, length: number) => clamp((at - from) / length);

/**
 * Where each part of the entrance is at entrance frame `f` (0 to INTRO_FRAMES). `index` is the
 * order of a track, keyframe or marker. Values are 0 to 1.
 */
export const intro = {
  /** Clips grow from their left edge, 44 frames each, 10 frames apart. */
  clip: (f: number, index: number) => grow(span(f, 20.75 + index * 10, 44)),
  /** Their labels fade in as they near their full length. */
  label: (f: number, index: number) => easeOut(span(f, 44.75 + index * 10, 16)),
  /** Keyframe diamonds pop in, 6.7 frames apart. */
  keyframe: (f: number, index: number) => easeOut(span(f, 85 + index * 6.7, 8)),
  /** The curve between keyframe `index` and the next draws from left to right. */
  curve: (f: number, index: number, dotted: boolean) =>
    dotted ? span(f, 128 + (index - 2) * 24, 24) : easeOut(span(f, 97 + index * 14, 12)),
  /** Ruler markers fade in, 8 frames apart. */
  marker: (f: number, index: number) => span(f, 103 + index * 8, 10),
  /** The playhead fades in where it stands. */
  playhead: (f: number) => span(f, 121, 7),
};

/**
 * The play button's ring after a press, `age` seconds on: a white flash hugging the button that
 * turns cyan and spreads into a fading halo. `gap` is in px between button and ring.
 */
export function ring(age: number | null | undefined) {
  if (age == null || age < 0) return { gap: 0, alpha: 0, white: 0, fill: 0 };
  const f = age * RATE;
  if (f < 8) return { gap: 0, alpha: 0, white: 0, fill: 0 };
  if (f < 12) return { gap: 0.5, alpha: 0.55 * span(f, 8, 4), white: 0, fill: 0 };
  const spread = easeOut(span(f, 16, 130));
  const fade = span(f, 20, 172);
  // Past the film's last frame (192 frames on), the halo goes out.
  const out = 1 - span(f, 196, 26);
  return {
    gap: 1.4 + spread * 11.6,
    alpha: (f < 20 ? 0.95 : 0.95 - 0.8 * Math.sqrt(fade)) * out,
    white: 1 - span(f, 16, 8),
    fill: (f < 18 ? 0 : 0.3 * span(f, 18, 8) * (1 - 0.75 * fade)) * out,
  };
}

/** The icon's swap from play to pause after a press, 0 to 1. */
export const morph = (age: number | null | undefined) =>
  age == null ? 1 : easeInOut(span(age * RATE, 2, 6));

/**
 * The editor's short film, frame by frame: what the component shows `seconds` into it. The flat
 * part only; a camera is up to the renderer. Shots: 0–4.33 s the entrance and a short play to
 * 0:01.7; 4.33–7.53 s still; 7.53–12.33 s the play button lifts, charges, is pressed and plays.
 */
export function filmState(seconds: number): Presentation {
  const f = seconds * RATE + 1;
  // The entrance, then a press of play at frame 111: the playhead fades in at 0:00.2 and runs at
  // real speed until it is paused at 0:01.7.
  let time = 0.2;
  let playing = false;
  let tint = 0;
  if (f >= 121) time = Math.min(1.7, 0.2 + (f - 121) / RATE);
  if (f >= 115 && f < 211) playing = true;
  tint = f < 211 ? span(f, 111, 8) : 1 - span(f, 211, 8);
  const outline = span(f, 136, 36);
  let hover = 0;
  let lift = 0;
  let charge = 0;
  let beams = 0;
  let press: number | null = null;
  if (f >= 437) {
    // Hovered and lifted (under the cut), charged blue with beams, pressed at 548.
    hover = span(f, 437, 10);
    lift = easeOut(span(f, 437, 16)) * (1 - easeIn(span(f, 548, 6)));
    charge = easeInOut(span(f, 460, 12));
    beams = easeOut(span(f, 462, 14)) * (1 - 0.3 * span(f, 556, 184));
    if (f >= 548) {
      press = (f - 548) / RATE;
      playing = f >= 552;
      // Playback picks up at 0:01.7 and eases to real speed.
      const run = Math.max(0, f - 576);
      time = 1.7 + (run < 24 ? (run * run) / 48 : run - 12) / RATE;
    }
  }
  return {
    time,
    playing,
    intro: span(f, 0, INTRO_FRAMES),
    readout: time,
    tint: press == null ? tint : 0,
    hover,
    lift,
    charge,
    beams,
    press,
    outline,
  };
}
```

### video-timeline/data.ts

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

/*
 * The timeline's data: tracks with their clips and animated properties, ruler markers and the
 * sections of the edit, and the sample project the demo opens on.
 */

/** A clip's colour: its fill, stripes and label ink come from it. */
export type ClipTone = 'pink' | 'yellow' | 'magenta' | 'green' | 'blue' | 'purple';
export type TrackIcon = 'sun' | 'type' | 'chip' | 'music' | 'shape' | 'image';

export type Clip = {
  id: string;
  /** Start and end, in seconds. */
  start: number;
  end: number;
  /** The label on the clip; the track's name without it. */
  label?: string;
};

/** An animated property of a track, drawn as a lane of keyframes under it. */
export type Param = {
  id: string;
  name: string;
  /** Its value at the playhead, as shown. */
  value: string;
  /** Keyframe times, in seconds. */
  keyframes: number[];
  /** From the keyframe at this index on, the curve holds: drawn dotted. */
  holdFrom?: number;
};

export type Track = {
  id: string;
  name: string;
  /**
   * 'layer': a row with lock, eye, swatch and tag. 'group' and 'audio': a row that folds open on
   * its properties, with an icon tile.
   */
  kind: 'layer' | 'group' | 'audio';
  tone: ClipTone;
  icon: TrackIcon;
  /** Diagonal stripes on its clips. */
  striped?: boolean;
  clips: Clip[];
  /** Keyframes on the clips themselves, in seconds. */
  keyframes?: number[];
  params?: Param[];
  /** Its properties' lanes are open. */
  expanded?: boolean;
  hidden?: boolean;
  locked?: boolean;
  /** Audio: where the waveform gets louder, in seconds. */
  accent?: number;
};

export type Marker = {
  id: string;
  /** In seconds. */
  time: number;
  label: string;
  tone: 'blue' | 'green' | 'red' | 'purple';
};

/** A stretch of the edit, running to the next section's start (or the end). */
export type Section = {
  id: string;
  start: number;
  label: string;
  tone: 'blue' | 'teal' | 'purple';
};

export const TRACKS: Track[] = [
  {
    id: 'depth',
    name: 'Depth Scan',
    kind: 'group',
    tone: 'pink',
    icon: 'sun',
    striped: true,
    expanded: true,
    clips: [{ id: 'depth-1', start: 1, end: 6 }],
    params: [
      {
        id: 'threshold',
        name: 'Threshold',
        value: '0.62',
        keyframes: [1, 2.35, 3.55, 4.55],
        holdFrom: 2,
      },
    ],
  },
  {
    id: 'focus',
    name: 'Focus Type',
    kind: 'layer',
    tone: 'yellow',
    icon: 'type',
    clips: [{ id: 'focus-1', start: 2.2, end: 8 }],
  },
  {
    id: 'gradient',
    name: 'Gradient',
    kind: 'layer',
    tone: 'magenta',
    icon: 'chip',
    striped: true,
    clips: [{ id: 'gradient-1', start: 0, end: 8, label: 'Gradient · Tidal air' }],
  },
  {
    id: 'score',
    name: 'Score · Calm',
    kind: 'audio',
    tone: 'green',
    icon: 'music',
    accent: 3.4,
    clips: [{ id: 'score-1', start: 0, end: 8 }],
    params: [{ id: 'volume', name: 'Volume', value: '−6 dB', keyframes: [] }],
  },
];

export const MARKERS: Marker[] = [
  { id: 'scan', time: 1, label: 'Scan in 1.0s', tone: 'blue' },
  { id: 'drop', time: 3.4, label: 'Drop 3.4s', tone: 'green' },
];

export const SECTIONS: Section[] = [
  { id: 'open', start: 0, label: 'Open', tone: 'blue' },
  { id: 'reveal', start: 2, label: 'Reveal', tone: 'teal' },
  { id: 'title', start: 5, label: 'Title', tone: 'purple' },
];

/** A seeded pseudo-random stream, so the waveform is the same on every render. */
export function seeded(seed: number) {
  let state = seed >>> 0 || 1;
  return () => {
    state ^= state << 13;
    state ^= state >>> 17;
    state ^= state << 5;
    return (state >>> 0) / 4294967296;
  };
}
```

### video-timeline/icons.tsx

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

/*
 * The editor's icons, drawn on a 16-unit grid in the text's colour (currentColor): each is a list
 * of shapes, filled or stroked with a round line of the given width. The stylesheet sizes them
 * (the lab page would otherwise force every unsized SVG to 16 px).
 */

type Shape = { d: string; fill?: boolean; width?: number; evenodd?: boolean };

const ring = (cx: number, cy: number, r: number) =>
  `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`;

/** The eight rays of the sun, from radius 4.4 to 6.4. */
const rays = Array.from({ length: 8 }, (_, i) => {
  const a = (i * Math.PI) / 4;
  const [c, s] = [Math.cos(a), Math.sin(a)];
  return `M${(8 + c * 4.4).toFixed(2)} ${(8 + s * 4.4).toFixed(2)}L${(8 + c * 6.3).toFixed(2)} ${(8 + s * 6.3).toFixed(2)}`;
}).join('');

export const ICONS = {
  pointer: [{ d: 'M3.6 2.4 12.9 7.1 8.7 8.5 7.2 12.9z', fill: true, width: 1.4 }],
  razor: [
    { d: ring(4.6, 11.4, 2) + ring(11.4, 11.4, 2), width: 1.5 },
    { d: 'M6 9.9 11.6 2.6M10 9.9 4.4 2.6', width: 1.5 },
  ],
  add: [
    { d: 'M7 2.6v8.8M2.6 7h8.8', width: 2 },
    { d: 'M10.6 14 14 10.6V14z', fill: true, width: 1 },
  ],
  chevronDown: [{ d: 'm4.2 6.2 3.8 3.8 3.8-3.8', width: 1.6 }],
  chevronRight: [{ d: 'm6.2 4.2 3.8 3.8-3.8 3.8', width: 1.6 }],
  lock: [
    { d: 'M5.4 7.2V5.4a2.6 2.6 0 0 1 5.2 0v1.8', width: 1.6 },
    {
      d: 'M3.6 8.4c0-.7.5-1.2 1.2-1.2h6.4c.7 0 1.2.5 1.2 1.2v4.2c0 .7-.5 1.2-1.2 1.2H4.8c-.7 0-1.2-.5-1.2-1.2z',
      fill: true,
      width: 1,
    },
  ],
  unlock: [
    { d: 'M5.4 7.2V5.4a2.6 2.6 0 0 1 5-1', width: 1.6 },
    {
      d: 'M3.6 8.4c0-.7.5-1.2 1.2-1.2h6.4c.7 0 1.2.5 1.2 1.2v4.2c0 .7-.5 1.2-1.2 1.2H4.8c-.7 0-1.2-.5-1.2-1.2z',
      fill: true,
      width: 1,
    },
  ],
  eye: [
    {
      d: 'M1.6 8c1.6-2.9 3.8-4.4 6.4-4.4s4.8 1.5 6.4 4.4c-1.6 2.9-3.8 4.4-6.4 4.4S3.2 10.9 1.6 8z',
      width: 1.4,
    },
    { d: ring(8, 8, 1.9), width: 1.4 },
  ],
  eyeOff: [
    {
      d: 'M1.6 8c1.6-2.9 3.8-4.4 6.4-4.4s4.8 1.5 6.4 4.4c-1.6 2.9-3.8 4.4-6.4 4.4S3.2 10.9 1.6 8z',
      width: 1.4,
    },
    { d: 'M2.6 13.4 13.4 2.6', width: 1.4 },
  ],
  pin: [
    { d: ring(11, 5, 2.3), fill: true, width: 1 },
    { d: 'M9.4 6.6 3.6 12.4', width: 1.6 },
  ],
  tag: [
    {
      d: 'M5.6 4h7.2c.8 0 1.4.6 1.4 1.4v5.2c0 .8-.6 1.4-1.4 1.4H5.6L2 8z' + ring(5.8, 8, 1),
      fill: true,
      evenodd: true,
      width: 1,
    },
  ],
  sections: [
    { d: 'M2.4 3.8v8.4M13.6 3.8v8.4', width: 1.8 },
    {
      d: 'M5.2 7c0-.4.3-.7.7-.7h4.2c.4 0 .7.3.7.7v2c0 .4-.3.7-.7.7H5.9c-.4 0-.7-.3-.7-.7z',
      fill: true,
      width: 1,
    },
  ],
  sun: [
    { d: ring(8, 8, 2.5), fill: true, width: 1 },
    { d: rays, width: 1.3 },
  ],
  type: [{ d: 'M3 5.2V3.4h10v1.8M8 3.4v9.2M6 12.6h4', width: 1.5 }],
  chip: [
    {
      d: 'M4.2 5.4c0-.7.5-1.2 1.2-1.2h5.2c.7 0 1.2.5 1.2 1.2v5.2c0 .7-.5 1.2-1.2 1.2H5.4c-.7 0-1.2-.5-1.2-1.2z',
      width: 1.4,
    },
    {
      d: 'M6.4 1.8v2.4M9.6 1.8v2.4M6.4 11.8v2.4M9.6 11.8v2.4M1.8 6.4h2.4M1.8 9.6h2.4M11.8 6.4h2.4M11.8 9.6h2.4',
      width: 1.3,
    },
    { d: 'M6 10 10 6v4z', fill: true, width: 1 },
  ],
  music: [
    { d: 'M6.4 11.6V4.4l6.6-1.4v7.4', width: 1.6 },
    { d: ring(4.7, 11.7, 1.8) + ring(11.3, 10.5, 1.8), fill: true, width: 1 },
  ],
  shape: [{ d: 'M3 3h10v10H3z', width: 1.5 }],
  image: [
    {
      d: 'M2.5 4.2c0-.9.8-1.7 1.7-1.7h7.6c.9 0 1.7.8 1.7 1.7v7.6c0 .9-.8 1.7-1.7 1.7H4.2c-.9 0-1.7-.8-1.7-1.7z',
      width: 1.4,
    },
    { d: 'm3 12 3.6-3.6 2.6 2.6 1.6-1.6L13 11.6', width: 1.4 },
  ],
  diamond: [{ d: 'M8 3.4 12.6 8 8 12.6 3.4 8z', width: 1.6 }],
  diamondFilled: [{ d: 'M8 3.4 12.6 8 8 12.6 3.4 8z', fill: true, width: 1.6 }],
  prevKey: [
    { d: 'M6.2 5 3.4 8l2.8 3', width: 1.5 },
    { d: 'M10.6 5.8 12.8 8l-2.2 2.2L8.4 8z', fill: true, width: 1 },
  ],
  nextKey: [
    { d: 'M9.8 5 12.6 8l-2.8 3', width: 1.5 },
    { d: 'M5.4 5.8 7.6 8l-2.2 2.2L3.2 8z', fill: true, width: 1 },
  ],
  prev: [{ d: 'M9.8 4.6 6.4 8l3.4 3.4', width: 1.5 }],
  next: [{ d: 'M6.2 4.6 9.6 8l-3.4 3.4', width: 1.5 }],
  record: [{ d: ring(8, 8, 4.6), width: 1.2 }],
  recordOn: [{ d: ring(8, 8, 4.6), fill: true, width: 1.2 }],
  start: [
    { d: 'M3.6 3.6v8.8', width: 1.8 },
    {
      d: 'M13 4.2v7.6c0 .5-.6.8-1 .5L6.2 8.6a.7.7 0 0 1 0-1.2L12 3.7c.4-.3 1 0 1 .5z',
      fill: true,
      width: 1,
    },
  ],
  stepBack: [
    { d: 'M10.2 4.4v7.2L4.6 8z', width: 1.4 },
    { d: 'M12.8 4v8', width: 1.8 },
  ],
  play: [
    {
      d: 'M5 3.6v8.8c0 .6.6.9 1.1.6l7-4.4c.5-.3.5-.9 0-1.2l-7-4.4C5.6 2.7 5 3 5 3.6z',
      fill: true,
      width: 1,
    },
  ],
  pause: [
    {
      d: 'M4.4 4.2c0-.5.4-.9.9-.9h.9c.5 0 .9.4.9.9v7.6c0 .5-.4.9-.9.9h-.9a.9.9 0 0 1-.9-.9zM8.9 4.2c0-.5.4-.9.9-.9h.9c.5 0 .9.4.9.9v7.6c0 .5-.4.9-.9.9h-.9a.9.9 0 0 1-.9-.9z',
      fill: true,
      width: 1,
    },
  ],
  stepForward: [
    { d: 'M5.8 4.4v7.2L11.4 8z', width: 1.4 },
    { d: 'M3.2 4v8', width: 1.8 },
  ],
  end: [
    {
      d: 'M3 4.2v7.6c0 .5.6.8 1 .5l5.8-3.7a.7.7 0 0 0 0-1.2L4 3.7c-.4-.3-1 0-1 .5z',
      fill: true,
      width: 1,
    },
    { d: 'M12.4 3.6v8.8', width: 1.8 },
  ],
  loop: [
    { d: 'M2.8 8.6V7.2c0-1.4 1.1-2.5 2.5-2.5h7.4', width: 1.6 },
    { d: 'm10.6 2.6 2.2 2.1-2.2 2.1', width: 1.6 },
    { d: 'M13.2 7.4v1.4c0 1.4-1.1 2.5-2.5 2.5H3.3', width: 1.6 },
    { d: 'm5.4 13.4-2.2-2.1 2.2-2.1', width: 1.6 },
  ],
  transition: [{ d: 'M2.6 4v8L8 8zM13.4 4v8L8 8z', width: 1.5 }],
} satisfies Record<string, Shape[]>;

export type IconName = keyof typeof ICONS;

export function Icon({ name, className }: { name: IconName; className?: string }) {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" className={className}>
      {(ICONS[name] as Shape[]).map((shape, i) => (
        <path
          key={i}
          d={shape.d}
          fill={shape.fill ? 'currentColor' : 'none'}
          fillRule={shape.evenodd ? 'evenodd' : undefined}
          stroke="currentColor"
          strokeWidth={shape.fill ? (shape.width === 1 ? 0 : shape.width) : shape.width}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      ))}
    </svg>
  );
}
```

### video-timeline/use-autoplay.ts

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

'use client';

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

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

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

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

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