# Care schedule

> A home-care agency’s day board: visits on a 7 AM to 7 PM timeline per caregiver, a live now line, travel links, a visit popover and drag to reassign.

The scheduling screen of an invented home-care agency. A sidebar holds the agency switcher, search, the sections with an alerts count, the service areas with their visit counts and the coordinator. The schedule lays out the day’s 48 visits on a timeline, one row per caregiver: confirmed, in progress, completed, missed and at-risk visits each have their look, dotted links show the travel time between them, a coverage row sits on top and the caregiver who called out sits at the bottom. A clock moves the now line and starts and finishes visits as it passes them. Click a visit for its popover, drag it to another caregiver or time, step through days, switch to the week or to clients, filter by area, search, or highlight one state from the legend.

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

Category: Dashboards.

## Anatomy

1. **Sidebar**: The agency switcher, a search on caregivers and clients (⌘K), the sections, the service areas with their visit counts, and the coordinator. A drawer under 880 px.
2. **Top bar**: The sidebar toggle, the breadcrumb, the running clock and the inbox and notification buttons.
3. **Header**: The title and the day’s counts, then the day stepper (previous, Today, next), Day/Week, Caregivers/Clients, the area menu and New visit.
4. **Coverage row**: Pinned on top: how many of the day’s visits have a caregiver, and the line saying all are covered.
5. **Caregiver row**: An illustrated avatar, the name, the role and the hours booked, then the lane with that caregiver’s visits on the 7 AM to 7 PM timeline.
6. **Visit**: A button placed by its start and length as shares of the day; its fill is its state. Click it for the popover, drag it or press Alt with the arrows to move it.
7. **Now line**: The clock’s minute across every row, moved by a transform; visits start and finish as it passes them.
8. **Visit popover**: The client, the care, the time, the caregiver, the check-in and the note, with Open visit and Message. Escape or a click elsewhere closes it.

## When to use

- A coordinator’s day board for people who go to places: caregivers, technicians, cleaners, each with timed visits across one working day.
- Days where who covers what changes by the hour: someone calls out, a visit is moved to another row or another time by dragging it.
- Work whose state comes from the clock: a visit is confirmed, in progress or completed as the now line passes it, with missed and at risk as flags.
- Schedules where the gap between two jobs matters, because it is time on the road: the dotted links show the travel minutes.
- Teams of a dozen or two rows that one person reads at a glance, filtered by area and searched by caregiver or client.

## When not to use

- Booking a time for oneself (a client picking a slot). Use a date picker with a list of free times.
- Rooms, desks or other resources that do not move. Use a resource calendar with columns per resource; there is no travel between them.
- Planning weeks or months ahead. The Week view only counts visits per day; use a month calendar or a Gantt chart for long plans.
- Hundreds of rows. The board lists ten caregivers before “Show all” and draws every block; page or virtualise a long roster instead.
- A single person’s day on a phone. Use a vertical agenda: the timeline reflows narrow but still scrolls sideways.

## Installation

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

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

export function SchedulePage() {
  return (
    <CareSchedule
      autoplay={false}
      defaultArea="harbor"
      onReassign={(visit, from) => console.log(visit.client, 'moved from', from.caregiver)}
    />
  );
}
```

## States

- **Visit open**: The reference: 1:38 PM on Monday, with the popover of Inés’s late-morning visit open under its row.
- **Nothing open**: `defaultSelected={null}`: the board alone: filled blocks under way, grey ones done, outlined ones still to come.
- **Morning**: `defaultNow="8:15"`: the now line near the start, almost every visit still confirmed.
- **End of day**: `defaultNow="18:30"`: nearly everything completed; the at-risk visit was never started, so it is now missed.
- **State highlighted**: `defaultHighlight="missed"`, or a click on the legend: the other visits fade back.
- **Search**: `defaultQuery="Ellery"`: only the rows with a match stay, and their other visits fade.
- **One area**: `defaultArea="harbor"`: only Harbor Point’s visits and the caregivers who have one there.
- **By client**: `defaultGroup="clients"`: one row per client, in the order of their first visit, without travel links.
- **Week**: `defaultView="week"`: each caregiver’s visits and hours per day, today marked, days off greyed.
- **Dark**: `colorScheme="dark"`: a warm charcoal frame with the same roles; every colour is a light-dark() token.
- **Narrow**: Under 880 px of its own width the sidebar becomes a drawer, the controls wrap and the timeline scrolls sideways with the names pinned.

## How it works

- The board is one 1440 × 1025 design. From 880 px up it is scaled to the root’s width in CSS alone: transform: scale(tan(atan2(100cqw, 1440px))) turns a length over a length into a number, so it is laid out once and looks the same at any size. Under 880 px a container query reflows it: the sidebar becomes a drawer behind a menu button, the controls wrap and the timeline scrolls sideways inside its card with the names pinned.
- The timeline is shares of the day. Each visit, travel link, hour line and drop target is placed by two custom properties, --s (where it starts) and --w (how long it lasts), as calc(8px + var(--s) * (100% - 16px)), so the same rules serve every width.
- A visit’s state comes from the clock, not the data: it is confirmed before its start, in progress until its end and completed after, while missed and at risk are flags. The clock runs twelve times faster than real time while the board is on screen (once under reduced motion), and the now line is a full-width track moved by translateX(var(--now) * 100%), with only its left edge seen.
- Dragging uses pointer events with pointer capture: past 4 px the block follows the pointer by a transform, the row under the pointer lights up, and a dashed slot shows where it would land, snapped to 15 minutes, green when it fits and red when it would overlap. A drop that fits moves the visit, recomputes the travel links around it and announces it; one that does not springs back. Alt and the arrow keys do the same from the keyboard.
- The popover is placed under the visit’s row and kept inside the board, above the row when there is no room below; a click elsewhere, Escape or a scroll of the timeline closes it.
- Other days are made from today’s plan: each caregiver’s day slides by up to half an hour, a visit is dropped now and then and weekends are lighter, so day stepping and the week view always have plausible data. Clients regroups the same visits by client.
- A demo pointer plays the board while it is on screen and untouched: it hovers and opens visits, drags one to another caregiver half an hour later, filters an area, steps to tomorrow and back and highlights a state. Any real pointer, key or wheel input hands control to the reader; it resumes six seconds after they leave, and stops off screen, in a hidden tab, under reduced motion and with autoplay={false}.
- Avatars and the agency mark are inline SVG illustrations, never photos, and every colour is a light-dark() token on the root.

## API reference

- **autoplay** (`boolean`, default `true`): Plays the demo pointer while the board is on screen and untouched. Never under reduced motion.
- **colorScheme** (`'light' | 'dark' | 'system'`): Forces a colour scheme. Without it the board follows the page’s color-scheme.
- **defaultNow** (`string`, default `'13:38'`): The time the clock starts at, H:MM on a 24-hour clock.
- **clockSpeed** (`number`, default `12`): How many times faster than real time the clock runs (always 1 under reduced motion).
- **defaultSelected** (`string | null`, default `'ines-1'`): The visit whose popover is open at first, or null for none.
- **defaultArea** (`'all' | 'millbrook' | 'harbor' | 'elm' | 'fallow'`, default `'all'`): The service area the board opens filtered to.
- **defaultView** (`'day' | 'week'`, default `'day'`): Opens on the day timeline or on the week.
- **defaultGroup** (`'caregivers' | 'clients'`, default `'caregivers'`): One row per caregiver, or one per client.
- **defaultHighlight** (`'confirmed' | 'in-progress' | 'completed' | 'missed' | 'at-risk' | null`, default `null`): The state the legend highlights at first; the other visits fade. Set, it keeps the demo off.
- **defaultQuery** (`string`, default `''`): What the search holds at first: matching caregivers and clients stay, the rest fade or go. Set, it keeps the demo off.
- **onReassign** (`(visit: Visit, from: { caregiver: string; start: number }) => void`): Called when a visit is dropped on another caregiver or time, by the reader or the demo; start is in minutes after midnight.
- **onOpenVisit** (`(visit: Visit) => void`): Called by the popover’s Open visit button.
- **onMessage** (`(visit: Visit) => void`): Called by the popover’s Message button.
- **className** (`string`): Added to the root, to size or place the board.

## Accessibility

- The schedule is a table of rows: each caregiver is a row header and each visit a button named by its client, time and state, described by how to move it. The popover is a labelled dialog the visit controls (aria-expanded); Escape closes it and returns focus to the visit.
- Alt with the arrow keys moves a focused visit to the caregiver above or below, or 15 minutes earlier or later; a move that would overlap is refused and said. Every move, filter and day change is announced in a polite live region.
- The area picker is a listbox button: arrow keys, Home and End move, Enter picks and Escape closes and returns focus. Day/Week, Caregivers/Clients, the legend and the sidebar areas are toggle buttons with aria-pressed; the current section has aria-current.
- On the narrow layout the menu button reports aria-expanded and Escape closes the drawer. ⌘K or Ctrl+K focuses the search.
- Illustrated avatars, icons, the travel links, hour lines and the now line are hidden from assistive technology; the demo pointer is decorative. Reduced motion stops the demo and the transitions and runs the clock at real time.

## Source

### care-schedule/care-schedule.tsx

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

'use client';

import {
  useCallback,
  useEffect,
  useEffectEvent,
  useId,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent as ReactKeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from 'react';
import { Avatar } from './avatar';
import {
  AREAS,
  CAREGIVERS,
  DAY_END,
  DAY_START,
  FIRST_ROWS,
  START_NOW,
  TODAY_DATE,
  travelBetween,
  visitsFor,
  type AreaId,
  type Caregiver,
  type Look,
  type Status,
  type Visit,
} from './data';
import { DemoPointer, runDemo, type Point } from './demo-pointer';
import { Icon, Mark, type IconName } from './icons';
import { useAutoplay } from './use-autoplay';
import styles from './care-schedule.module.css';

export type { AreaId, Status, Visit } from './data';

/*
 * Care schedule: the day board of an invented home-care agency. A sidebar (agency switcher,
 * search, sections, service areas with their visit counts, the signed-in coordinator) and a
 * schedule: the day's visits laid out on a 7 AM to 7 PM timeline, one row per caregiver, with
 * a live "now" line, a coverage row on top and the caregiver who called out at the bottom.
 *
 * - Each visit is a block placed by its start and length as shares of the day (CSS custom
 *   properties), so the timeline is the same at any width. Its state comes from the clock:
 *   confirmed (outlined), in progress (black) and completed (grey); missed (red, dashed) and at
 *   risk (amber) are flags. Dotted links between blocks are the travel time between visits.
 * - The clock runs (12 times real time by default) while the board is on screen: the now line
 *   slides by a transform, and visits start and finish as it passes them.
 * - Click a visit for its popover (client, care, time, caregiver, check-in, note, Open visit and
 *   Message); Escape or a click elsewhere closes it. Drag a visit to another caregiver, or along
 *   its row in steps of 15 minutes, to reassign or move it; Alt and the arrow keys do the same
 *   from the keyboard. A drop that would overlap another visit springs back.
 * - Day navigation (previous, Today, next) builds any other day from today's plan; Week shows the
 *   week by day; Clients turns the rows into clients; the area menu, the sidebar's areas, the
 *   search and the legend filter or highlight what is shown.
 * - While on screen and untouched, a demo pointer opens visits, drags one to another caregiver,
 *   filters an area, steps to tomorrow and back and highlights a state. Any real input hands
 *   control to the reader; reduced motion or autoplay={false} turns it off.
 * - Wide (the root 880 px and up) the board is a 1440 × 1025 design scaled to the root's width
 *   in CSS alone; narrower, it reflows: the sidebar becomes a drawer, the controls wrap and the
 *   timeline scrolls sideways inside its card with the names pinned.
 * - Light or dark: every colour is a light-dark() token on the root.
 */

export type CareScheduleProps = {
  className?: string;
  /** Plays the demo pointer while on screen and untouched. Never under reduced motion. */
  autoplay?: boolean;
  /** Forces a colour scheme; without it, the page's color-scheme decides. */
  colorScheme?: 'light' | 'dark' | 'system';
  /** The time the clock starts at, 'H:MM' on a 24-hour clock. */
  defaultNow?: string;
  /** How many times faster than real time the clock runs (1 under reduced motion). */
  clockSpeed?: number;
  /** The visit whose popover is open at first ('ines-1' in the reference), or null. */
  defaultSelected?: string | null;
  /** The service area shown at first. */
  defaultArea?: AreaId | 'all';
  defaultView?: 'day' | 'week';
  defaultGroup?: 'caregivers' | 'clients';
  /** The state the legend highlights at first (the others fade). Set, it keeps the demo off. */
  defaultHighlight?: Status | null;
  /** What the search holds at first. Set, it keeps the demo off. */
  defaultQuery?: string;
  /** Called when a visit is dropped on another caregiver or another time. */
  onReassign?: (visit: Visit, from: { caregiver: string; start: number }) => void;
  /** Called by the popover's Open visit button. */
  onOpenVisit?: (visit: Visit) => void;
  /** Called by the popover's Message button. */
  onMessage?: (visit: Visit) => void;
};

/* ------------------------------------------------------------------ helpers */

const HOURS = Array.from({ length: 13 }, (_, index) => DAY_START + index * 60);
const SPAN = DAY_END - DAY_START;
const STEP = 15;
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const STATES: { id: Status; label: string }[] = [
  { id: 'confirmed', label: 'Confirmed' },
  { id: 'in-progress', label: 'In progress' },
  { id: 'completed', label: 'Completed' },
  { id: 'missed', label: 'Missed' },
  { id: 'at-risk', label: 'At risk' },
];
const STATE_LABEL = Object.fromEntries(STATES.map((state) => [state.id, state.label])) as Record<
  Status,
  string
>;
const AREA_LABEL = Object.fromEntries(AREAS.map((area) => [area.id, area.label])) as Record<
  AreaId,
  string
>;
const NAV: { id: string; label: string; icon: IconName; badge?: number }[][] = [
  [
    { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
    { id: 'schedule', label: 'Schedule', icon: 'calendar' },
    { id: 'visits', label: 'Visits', icon: 'visits' },
    { id: 'alerts', label: 'Alerts', icon: 'bell', badge: 4 },
  ],
  [
    { id: 'caregivers', label: 'Caregivers', icon: 'caregivers' },
    { id: 'clients', label: 'Clients', icon: 'clients' },
  ],
  [
    { id: 'evv', label: 'EVV', icon: 'shield' },
    { id: 'reports', label: 'Reports', icon: 'reports' },
  ],
];
const SECTIONS = ['Operations', 'People', 'Compliance'];

const parseClock = (clock: string) => {
  const [hours, minutes] = clock.split(':').map(Number);
  return hours * 60 + (minutes || 0);
};
/** "7:30": a block's time, twelve-hour, no AM/PM. */
const short = (minutes: number) => {
  const hours = Math.floor(minutes / 60);
  return `${((hours + 11) % 12) + 1}:${String(Math.round(minutes % 60)).padStart(2, '0')}`;
};
/** "10:00 AM". */
const long = (minutes: number) => `${short(minutes)} ${minutes < 720 ? 'AM' : 'PM'}`;
const dateFor = (offset: number) => {
  const date = new Date(TODAY_DATE);
  date.setDate(date.getDate() + offset);
  return date;
};
const hoursOf = (visits: Visit[]) => visits.reduce((sum, v) => sum + (v.end - v.start), 0) / 60;
const firstName = (name: string) => name.split(' ')[0];
const initialed = (name: string) => {
  const [first, last] = name.split(' ');
  return last ? `${first} ${last[0]}.` : first;
};

function statusOf(visit: Visit, offset: number, now: number): Status {
  if (visit.flag === 'missed') return 'missed';
  if (offset < 0) return 'completed';
  if (offset > 0) return visit.flag ?? 'confirmed';
  if (visit.flag === 'at-risk') return now >= visit.end ? 'missed' : 'at-risk';
  if (now >= visit.end) return 'completed';
  if (now >= visit.start) return 'in-progress';
  return 'confirmed';
}

/** What the popover says about the check-in (electronic visit verification). */
function checkIn(visit: Visit, status: Status) {
  const feet = visit.distance ?? 12 + ((visit.client.length * 7) % 40);
  switch (status) {
    case 'completed':
      return { ok: true, text: `Verified · ${feet} ft from the door` };
    case 'in-progress':
      return { ok: true, text: `Checked in ${long(visit.start + 2)}` };
    case 'missed':
      return { ok: false, text: 'No check-in' };
    case 'at-risk':
      return { ok: false, text: 'Not en route · 4.2 mi away' };
    default:
      return { ok: false, text: 'Not started' };
  }
}

type Row = {
  id: string;
  kind: 'caregiver' | 'client' | 'out';
  name: string;
  meta: string;
  look: Look;
  visits: Visit[];
  /** Visits on the row that do not match the search. */
  dim?: Set<string>;
};

type Drag = {
  id: string;
  pointer: number;
  x: number;
  y: number;
  dx: number;
  dy: number;
  moving: boolean;
  row: string | null;
  start: number;
  ok: boolean;
};

/* ------------------------------------------------------------------ component */

export function CareSchedule({
  className,
  autoplay = true,
  colorScheme,
  defaultNow = '13:38',
  clockSpeed = 12,
  defaultSelected = 'ines-1',
  defaultArea = 'all',
  defaultView = 'day',
  defaultGroup = 'caregivers',
  defaultHighlight = null,
  defaultQuery = '',
  onReassign,
  onOpenVisit,
  onMessage,
}: CareScheduleProps) {
  const id = useId();
  const { ref: root, take, visible, reduced, playing } = useAutoplay<HTMLDivElement>(6000);
  const frame = useRef<HTMLDivElement>(null);
  const board = useRef<HTMLDivElement>(null);
  const scroller = useRef<HTMLDivElement>(null);
  const popover = useRef<HTMLDivElement>(null);
  const pointer = useRef<HTMLSpanElement>(null);
  const searchBox = useRef<HTMLInputElement>(null);
  const areaButton = useRef<HTMLButtonElement>(null);
  const areaMenu = useRef<HTMLUListElement>(null);
  const opener = useRef<HTMLElement | null>(null);

  const [offset, setOffset] = useState(0);
  const [view, setView] = useState(defaultView);
  const [group, setGroup] = useState(defaultGroup);
  const [area, setArea] = useState<AreaId | 'all'>(defaultArea);
  const [query, setQuery] = useState(defaultQuery);
  const [legend, setLegend] = useState<Status | null>(defaultHighlight);
  const [showAll, setShowAll] = useState(false);
  const [days, setDays] = useState<Record<number, Visit[]>>({});
  const [selected, setSelected] = useState<string | null>(defaultSelected);
  const [hover, setHover] = useState<string | null>(null);
  const [now, setNow] = useState(() => parseClock(defaultNow));
  const [drag, setDrag] = useState<Drag | null>(null);
  const [menu, setMenu] = useState(false);
  const [menuIndex, setMenuIndex] = useState(0);
  const [drawer, setDrawer] = useState(false);
  const [collapsed, setCollapsed] = useState(false);
  const [nav, setNav] = useState('schedule');
  const [bell, setBell] = useState(true);
  const [said, setSaid] = useState('');
  const [fresh, setFresh] = useState<string | null>(null);
  const added = useRef(0);

  const visits = useMemo(() => days[offset] ?? visitsFor(offset), [days, offset]);
  const setVisits = useCallback(
    (update: (list: Visit[]) => Visit[]) =>
      setDays((all) => ({ ...all, [offset]: update(all[offset] ?? visitsFor(offset)) })),
    [offset],
  );
  const status = useCallback((visit: Visit) => statusOf(visit, offset, now), [offset, now]);

  /* The clock: one simulated minute at a time, only while the board is on screen. */
  const speed = reduced ? 1 : Math.max(clockSpeed, 0.01);
  useEffect(() => {
    if (!visible) return;
    const timer = window.setInterval(
      () => setNow((minutes) => Math.min(minutes + 1, DAY_END)),
      60000 / speed,
    );
    return () => window.clearInterval(timer);
  }, [visible, speed]);

  /* ---------------------------------------------------------------- rows */

  const q = query.trim().toLowerCase();
  const inArea = useCallback((v: Visit) => area === 'all' || v.area === area, [area]);
  const rows = useMemo<Row[]>(() => {
    const shown = visits.filter(inArea);
    if (group === 'clients') {
      const byClient = new Map<string, Visit[]>();
      for (const visit of [...shown].sort((a, b) => a.start - b.start))
        byClient.set(visit.client, [...(byClient.get(visit.client) ?? []), visit]);
      return [...byClient.entries()]
        .filter(([client, list]) => {
          if (!q) return true;
          const carer = CAREGIVERS.find((c) => c.id === list[0].caregiver)?.name ?? '';
          return client.toLowerCase().includes(q) || carer.toLowerCase().includes(q);
        })
        .map(([client, list]) => ({
          id: `client:${client}`,
          kind: 'client' as const,
          name: client,
          meta: `${AREA_LABEL[list[0].area]} · ${list.length} visit${list.length > 1 ? 's' : ''}`,
          look: list[0].look,
          visits: list,
        }));
    }
    const out: Row[] = [];
    for (const caregiver of CAREGIVERS) {
      const own = visits.filter((v) => v.caregiver === caregiver.id);
      const mine = own.filter(inArea);
      const isOut = offset === 0 && caregiver.out;
      if (isOut) {
        if (area === 'all' && (!q || caregiver.name.toLowerCase().includes(q)))
          out.push({
            id: caregiver.id,
            kind: 'out',
            name: caregiver.name,
            meta: `${caregiver.role} · ${caregiver.out!.reason}`,
            look: caregiver.look,
            visits: [],
          });
        continue;
      }
      if (!own.length || (area !== 'all' && !mine.length)) continue;
      let dim: Set<string> | undefined;
      if (q && !caregiver.name.toLowerCase().includes(q)) {
        const hits = mine.filter((v) => v.client.toLowerCase().includes(q));
        if (!hits.length) continue;
        dim = new Set(mine.filter((v) => !hits.includes(v)).map((v) => v.id));
      }
      out.push({
        id: caregiver.id,
        kind: 'caregiver',
        name: caregiver.name,
        meta: `${caregiver.role} · ${hoursOf(own).toFixed(1)} h`,
        look: caregiver.look,
        visits: mine.sort((a, b) => a.start - b.start),
        dim,
      });
    }
    return out;
  }, [visits, inArea, group, q, offset, area]);

  // The called-out caregiver stays at the bottom; the rest are cut to FIRST_ROWS until "show all".
  const working = rows.filter((row) => row.kind !== 'out');
  const outRows = rows.filter((row) => row.kind === 'out');
  const limited = showAll ? working : working.slice(0, FIRST_ROWS);
  const listed = [...limited, ...outRows];
  const total = group === 'clients' ? rows.length : CAREGIVERS.length;
  const noun = group === 'clients' ? 'clients' : 'caregivers';
  const canExpand = working.length > FIRST_ROWS;

  const dayVisits = visits.length;
  const onShift = new Set(visits.map((v) => v.caregiver)).size + (offset === 0 ? 1 : 0);
  const date = dateFor(offset);
  const weekStart = dateFor(offset - ((date.getDay() + 6) % 7));
  const weekOffsets = Array.from(
    { length: 7 },
    (_, index) => offset - ((date.getDay() + 6) % 7) + index,
  );
  const dayLabel = `${WEEKDAYS[date.getDay()].slice(0, 3)}, ${MONTHS[date.getMonth()]} ${date.getDate()}`;
  const weekLabel = `${MONTHS[weekStart.getMonth()]} ${weekStart.getDate()} – ${dateFor(weekOffsets[6]).getDate()}`;
  const weekVisits = weekOffsets.map((day) =>
    day === offset ? visits : (days[day] ?? visitsFor(day)),
  );
  const weekCount = weekVisits.reduce((sum, list) => sum + list.length, 0);

  const selectedVisit = visits.find((v) => v.id === selected) ?? null;
  const open = Boolean(selectedVisit && view === 'day' && !drag?.moving);

  /* ---------------------------------------------------------------- announcements */

  const say = (text: string) => setSaid(text);

  /* ---------------------------------------------------------------- popover placement */

  useLayoutEffect(() => {
    const card = popover.current;
    const space = board.current;
    if (!open || !card || !space || !selectedVisit) return;
    const block = space.querySelector<HTMLElement>(
      `[data-visit="${CSS.escape(selectedVisit.id)}"]`,
    );
    if (!block) return;
    const place = () => {
      const box = space.getBoundingClientRect();
      const scale = box.width / space.offsetWidth || 1;
      const rect = block.getBoundingClientRect();
      const row = block.closest<HTMLElement>('[data-row]')?.getBoundingClientRect() ?? rect;
      const width = card.offsetWidth;
      const height = card.offsetHeight;
      let left = (rect.left - box.left) / scale;
      left = Math.max(8, Math.min(left, space.offsetWidth - width - 8));
      let top = (row.bottom - box.top) / scale;
      if (top + height > space.offsetHeight - 4) top = (row.top - box.top) / scale - height;
      card.style.left = `${left}px`;
      card.style.top = `${Math.max(0, top)}px`;
    };
    place();
    const observer = new ResizeObserver(place);
    observer.observe(space);
    return () => observer.disconnect();
  }, [open, selectedVisit, rows, now]);

  // Close the popover on a click elsewhere, or when the timeline scrolls under it.
  useEffect(() => {
    if (!open) return;
    const away = (event: PointerEvent) => {
      const target = event.target as Element;
      if (popover.current?.contains(target) || target.closest?.('[data-visit]')) return;
      setSelected(null);
    };
    const scroll = scroller.current;
    const close = () => setSelected(null);
    document.addEventListener('pointerdown', away);
    scroll?.addEventListener('scroll', close, { passive: true });
    return () => {
      document.removeEventListener('pointerdown', away);
      scroll?.removeEventListener('scroll', close);
    };
  }, [open]);

  useEffect(() => {
    if (!menu) return;
    const away = (event: PointerEvent) => {
      const target = event.target as Node;
      if (areaMenu.current?.contains(target) || areaButton.current?.contains(target)) return;
      setMenu(false);
    };
    document.addEventListener('pointerdown', away);
    return () => document.removeEventListener('pointerdown', away);
  }, [menu]);

  useEffect(() => {
    if (menu)
      areaMenu.current?.querySelectorAll<HTMLElement>('[role="option"]')[menuIndex]?.focus();
  }, [menu, menuIndex]);

  /* ---------------------------------------------------------------- moving visits */

  /** Whether `visit` fits on `row` from `start` without overlapping another visit. */
  const fits = useCallback(
    (visit: Visit, row: string, start: number) => {
      const end = start + (visit.end - visit.start);
      if (start < DAY_START || end > DAY_END) return false;
      if (group === 'clients') return true;
      const caregiver = CAREGIVERS.find((c) => c.id === row);
      if (!caregiver || (offset === 0 && caregiver.out)) return false;
      return !visits.some(
        (other) =>
          other.id !== visit.id &&
          other.caregiver === row &&
          other.start < end &&
          start < other.end,
      );
    },
    [visits, group, offset],
  );

  const move = useCallback(
    (visit: Visit, row: string, start: number) => {
      const from = { caregiver: visit.caregiver, start: visit.start };
      const length = visit.end - visit.start;
      const caregiver = group === 'clients' ? visit.caregiver : row;
      let moved: Visit | null = null;
      setVisits((list) => {
        const next = list.map((v) =>
          v.id === visit.id ? { ...v, caregiver, start, end: start + length } : v,
        );
        // Travel links: recomputed for the moved visit and for what now follows it, and
        // for what followed it where it came from.
        const relink = (list2: Visit[], carer: string) => {
          const day = list2.filter((v) => v.caregiver === carer).sort((a, b) => a.start - b.start);
          const travel = new Map<string, number | undefined>();
          day.forEach((v, index) => {
            const before = day[index - 1];
            if (!before) return travel.set(v.id, undefined);
            const gap = v.start - before.end;
            const touched = v.id === visit.id || before.id === visit.id;
            if (!touched && v.travel !== undefined) return;
            travel.set(
              v.id,
              gap >= 10 && gap <= 60 ? Math.min(travelBetween(before, v), gap - 2) : undefined,
            );
          });
          return list2.map((v) => (travel.has(v.id) ? { ...v, travel: travel.get(v.id) } : v));
        };
        let out = relink(next, caregiver);
        if (from.caregiver !== caregiver) {
          const follower = list
            .filter((v) => v.caregiver === from.caregiver && v.start >= visit.end)
            .sort((a, b) => a.start - b.start)[0];
          out = out.map((v) =>
            follower && v.id === follower.id ? { ...v, travel: undefined } : v,
          );
        }
        moved = out.find((v) => v.id === visit.id) ?? null;
        return out;
      });
      const carer = CAREGIVERS.find((c) => c.id === caregiver);
      say(
        `${visit.short} moved to ${carer?.name ?? ''}, ${long(start)} to ${long(start + length)}.`,
      );
      setFresh(visit.id);
      if (moved) onReassign?.(moved, from);
      else onReassign?.({ ...visit, caregiver, start, end: start + length }, from);
    },
    [group, setVisits, onReassign],
  );

  const pxPerMinute = () => {
    const lane = board.current?.querySelector<HTMLElement>('[data-lane]');
    return lane ? (lane.offsetWidth - 16) / SPAN : 1;
  };
  const scaleOf = () => {
    const space = frame.current;
    if (!space) return 1;
    return space.getBoundingClientRect().width / space.offsetWidth || 1;
  };
  const rowAt = (clientY: number) => {
    const list = board.current?.querySelectorAll<HTMLElement>('[data-row]') ?? [];
    for (const element of list) {
      const rect = element.getBoundingClientRect();
      if (clientY >= rect.top && clientY < rect.bottom) return element.dataset.row ?? null;
    }
    return null;
  };

  /** Where a drag of `visit` by (dx, dy) screen px would land. */
  const landing = (visit: Visit, origin: string, clientY: number, dx: number) => {
    const minutes = dx / scaleOf() / pxPerMinute();
    const start = Math.max(
      DAY_START,
      Math.min(
        DAY_END - (visit.end - visit.start),
        visit.start + Math.round(minutes / STEP) * STEP,
      ),
    );
    const row = group === 'clients' ? origin : rowAt(clientY);
    return { row, start, ok: Boolean(row) && fits(visit, row ?? '', start) };
  };

  const onBlockDown = (event: ReactPointerEvent<HTMLButtonElement>, visit: Visit, row: string) => {
    if (event.button !== 0 || view !== 'day') return;
    event.currentTarget.setPointerCapture?.(event.pointerId);
    setDrag({
      id: visit.id,
      pointer: event.pointerId,
      x: event.clientX,
      y: event.clientY,
      dx: 0,
      dy: 0,
      moving: false,
      row,
      start: visit.start,
      ok: true,
    });
  };
  const onBlockMove = (event: ReactPointerEvent<HTMLButtonElement>, visit: Visit, row: string) => {
    if (!drag || drag.pointer !== event.pointerId || drag.id !== visit.id) return;
    const dx = event.clientX - drag.x;
    const dy = event.clientY - drag.y;
    const moving = drag.moving || Math.hypot(dx, dy) > 4;
    if (!moving) return;
    const land = landing(visit, row, event.clientY, dx);
    setDrag({ ...drag, dx, dy, moving, row: land.row, start: land.start, ok: land.ok });
  };
  const onBlockUp = (event: ReactPointerEvent<HTMLButtonElement>, visit: Visit, row: string) => {
    if (!drag || drag.pointer !== event.pointerId) return;
    const was = drag;
    setDrag(null);
    if (!was.moving) {
      opener.current = event.currentTarget;
      setSelected((current) => (current === visit.id ? null : visit.id));
      return;
    }
    if (was.ok && was.row && (was.row !== row || was.start !== visit.start)) {
      move(visit, was.row, was.start);
      setSelected(null);
    } else if (!was.ok) say(`${visit.short} can't go there: it would overlap another visit.`);
  };

  const onBlockKey = (event: ReactKeyboardEvent<HTMLButtonElement>, visit: Visit, row: string) => {
    if (event.key === 'Escape') {
      setSelected(null);
      return;
    }
    if (!event.altKey || view !== 'day') return;
    const vertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
    const horizontal = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
    if (!vertical && !horizontal) return;
    event.preventDefault();
    let target = row;
    let start = visit.start;
    if (horizontal) start += event.key === 'ArrowLeft' ? -STEP : STEP;
    if (vertical && group === 'caregivers') {
      const candidates = listed.filter((r) => r.kind === 'caregiver').map((r) => r.id);
      const index = candidates.indexOf(row) + (event.key === 'ArrowUp' ? -1 : 1);
      if (index < 0 || index >= candidates.length) return;
      target = candidates[index];
    }
    if (!fits(visit, target, start)) {
      say(`${visit.short} can't go there: it would overlap another visit.`);
      return;
    }
    move(visit, target, start);
    const focus = () =>
      board.current?.querySelector<HTMLElement>(`[data-visit="${CSS.escape(visit.id)}"]`)?.focus();
    requestAnimationFrame(focus);
  };

  useEffect(() => {
    if (!fresh) return;
    const timer = window.setTimeout(() => setFresh(null), 900);
    return () => window.clearTimeout(timer);
  }, [fresh]);

  /* ---------------------------------------------------------------- controls */

  const goDay = (next: number) => {
    setOffset(next);
    setSelected(null);
    setDrag(null);
    const d = dateFor(next);
    say(`${WEEKDAYS[d.getDay()]}, ${MONTHS[d.getMonth()]} ${d.getDate()}.`);
  };
  const pickArea = (next: AreaId | 'all') => {
    setArea(next);
    setMenu(false);
    setSelected(null);
    say(next === 'all' ? 'All service areas.' : `${AREA_LABEL[next]} only.`);
  };
  const newVisit = () => {
    added.current += 1;
    const carer =
      listed.find((row) => row.kind === 'caregiver' && group === 'caregivers')?.id ?? 'ines';
    const from = offset === 0 ? Math.ceil(now / 30) * 30 : 9 * 60;
    let start = from;
    const draft: Visit = {
      id: `new-${added.current}`,
      caregiver: carer,
      client: 'New client',
      short: 'New visit',
      start,
      end: start + 60,
      care: 'Personal care',
      area: area === 'all' ? 'millbrook' : area,
      look: CAREGIVERS[0].look,
    };
    while (start + 60 <= DAY_END && !fits(draft, carer, start)) start += 30;
    if (start + 60 > DAY_END) {
      say('No free hour left today for a new visit.');
      return;
    }
    const visit = { ...draft, start, end: start + 60 };
    setView('day');
    setVisits((list) => [...list, visit]);
    setSelected(visit.id);
    setFresh(visit.id);
    say(`New visit added for ${CAREGIVERS.find((c) => c.id === carer)?.name}, ${long(start)}.`);
  };

  const onRootKey = (event: ReactKeyboardEvent<HTMLDivElement>) => {
    if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
      event.preventDefault();
      setDrawer(true);
      requestAnimationFrame(() => searchBox.current?.focus());
    }
    if (event.key === 'Escape') {
      if (open) {
        setSelected(null);
        opener.current?.focus();
      } else if (drawer) setDrawer(false);
    }
  };

  /* ---------------------------------------------------------------- the demo */

  const demoState = useEffectEvent(
    (
      action:
        | 'reset'
        | 'hover'
        | 'select'
        | 'area'
        | 'menu'
        | 'day'
        | 'legend'
        | 'drag'
        | 'drop'
        | 'release',
      value?: unknown,
    ) => {
      switch (action) {
        case 'reset':
          setDays({});
          setOffset(0);
          setView(defaultView);
          setGroup(defaultGroup);
          setArea('all');
          setQuery('');
          setLegend(null);
          setMenu(false);
          setDrag(null);
          setHover(null);
          setNow(parseClock(defaultNow));
          break;
        case 'hover':
          setHover(value as string | null);
          break;
        case 'select':
          setSelected(value as string | null);
          break;
        case 'area':
          setArea(value as AreaId | 'all');
          setMenu(false);
          break;
        case 'menu':
          setMenu(Boolean(value));
          break;
        case 'day':
          setSelected(null);
          setOffset(value as number);
          break;
        case 'legend':
          setLegend(value as Status | null);
          break;
        case 'drag': {
          const { visit, row, start, dx, dy } = value as {
            visit: Visit;
            row: string;
            start: number;
            dx: number;
            dy: number;
          };
          setDrag({
            id: visit.id,
            pointer: -1,
            x: 0,
            y: 0,
            dx,
            dy,
            moving: true,
            row,
            start,
            ok: fits(visit, row, start),
          });
          break;
        }
        case 'drop': {
          const { visit, row, start } = value as { visit: Visit; row: string; start: number };
          setDrag(null);
          if (fits(visit, row, start)) move(visit, row, start);
          break;
        }
        case 'release':
          // Only the demo's own drag: a reader's drag in progress stays theirs.
          setDrag((current) => (current?.pointer === -1 ? null : current));
          setHover(null);
          break;
      }
    },
  );
  const findVisit = useEffectEvent((client: string) => visits.find((v) => v.client === client));

  const demo = autoplay && playing && !defaultHighlight && !defaultQuery;
  useEffect(() => {
    const stage = root.current;
    const arrow = pointer.current;
    if (!demo || !stage || !arrow) return;
    const centre = (selector: string, dx = 0.5, dy = 0.5): Point | null => {
      const target = stage.querySelector<HTMLElement>(selector);
      if (!target || !target.getClientRects().length) return null;
      const box = stage.getBoundingClientRect();
      const rect = target.getBoundingClientRect();
      if (rect.bottom < box.top || rect.top > box.bottom) return null;
      return {
        x: rect.left + rect.width * dx - box.left,
        y: rect.top + rect.height * dy - box.top,
      };
    };
    const at = (name: string) => `[data-demo="${name}"]`;
    const visitAt = (client: string) => {
      const visit = findVisit(client);
      return visit ? `[data-visit="${CSS.escape(visit.id)}"]` : '[data-none]';
    };
    const stop = runDemo(stage, arrow, async ({ wait, glide, glyph }) => {
      const go = async (selector: string, ms = 650, dx?: number, dy?: number) => {
        const to = centre(selector, dx, dy);
        if (to) await glide(to, ms);
        return Boolean(to);
      };
      const press = async (selector: string, ms?: number, dx?: number, dy?: number) => {
        glyph('hand');
        const ok = await go(selector, ms, dx, dy);
        await wait(180);
        return ok;
      };
      for (;;) {
        demoState('reset');
        demoState('select', defaultSelected);
        await wait(1600);
        // Hover two visits, open one in progress, then the missed one.
        glyph('arrow');
        for (const client of ['Imogen Oakes', 'Tobias Wynn']) {
          const visit = findVisit(client);
          if (await go(visitAt(client), 700, 0.45, 0.55)) demoState('hover', visit?.id ?? null);
          await wait(700);
        }
        demoState('hover', null);
        if (await press(visitAt('Waldo Dace'), 650, 0.4, 0.5))
          demoState('select', findVisit('Waldo Dace')?.id ?? null);
        await wait(2200);
        if (await press(visitAt('Mirela Pask'), 650, 0.4, 0.5))
          demoState('select', findVisit('Mirela Pask')?.id ?? null);
        await wait(2200);
        demoState('select', null);
        // Drag a visit to another caregiver, half an hour later.
        const visit = findVisit('Delphine Yarrow');
        const fromBox = centre(visitAt('Delphine Yarrow'), 0.3, 0.5);
        const toRow = centre('[data-row="paloma"]', 0.5, 0.5);
        const lane = stage.querySelector<HTMLElement>('[data-lane]');
        if (visit && fromBox && toRow && lane) {
          glyph('arrow');
          await glide(fromBox, 700);
          demoState('hover', visit.id);
          await wait(300);
          glyph('hand');
          const box = stage.getBoundingClientRect();
          const laneRect = lane.getBoundingClientRect();
          const perMinute = (laneRect.width - 16 * scaleOf()) / SPAN;
          const scale = scaleOf();
          const target = { x: fromBox.x + 30 * perMinute, y: toRow.y };
          const dyScreen = target.y - fromBox.y;
          const steps = 14;
          for (let index = 1; index <= steps; index += 1) {
            const share = index / steps;
            const point = {
              x: fromBox.x + (target.x - fromBox.x) * share,
              y: fromBox.y + dyScreen * share,
            };
            await glide(point, 60);
            demoState('drag', {
              visit,
              row: share > 0.5 ? 'paloma' : visit.caregiver,
              start: visit.start + (share > 0.5 ? 30 : 0),
              dx: (point.x - fromBox.x) / 1,
              dy: (point.y - fromBox.y) / 1,
            });
          }
          void box;
          void scale;
          await wait(450);
          demoState('drop', { visit, row: 'paloma', start: visit.start + 30 });
          demoState('hover', null);
          await wait(1800);
        }
        // Filter an area, then back to all of them.
        if (await press(at('area'), 800)) {
          demoState('menu', true);
          await wait(500);
          if (await go(at('area-harbor'), 450)) {
            await wait(200);
            demoState('area', 'harbor');
          }
          await wait(2000);
          await press(at('area'), 500);
          demoState('menu', true);
          await wait(450);
          if (await go(at('area-all'), 400)) {
            await wait(200);
            demoState('area', 'all');
          }
          await wait(900);
        }
        // Tomorrow, and back to today.
        if (await press(at('next'), 700)) demoState('day', 1);
        await wait(2000);
        if (await press(at('today'), 500)) demoState('day', 0);
        await wait(1200);
        // Highlight what is in progress.
        if (await press(at('legend-in-progress'), 800)) demoState('legend', 'in-progress');
        await wait(2000);
        demoState('legend', null);
        await wait(600);
        // Back to the reference's open visit.
        glyph('arrow');
        await go(visitAt('Henrietta Vance'), 800, 0.4, 0.5);
        demoState('select', findVisit('Henrietta Vance')?.id ?? null);
        await wait(2600);
      }
    });
    return () => {
      stop();
      demoState('release');
    };
  }, [demo, root, defaultSelected]);

  /* ---------------------------------------------------------------- pieces */

  const nowShare = (Math.min(Math.max(now, DAY_START), DAY_END) - DAY_START) / SPAN;
  const showNow = offset === 0 && view === 'day';
  const clockLabel = `${WEEKDAYS[TODAY_DATE.getDay()].slice(0, 3)}, ${MONTHS[TODAY_DATE.getMonth()]} ${TODAY_DATE.getDate()} · ${long(Math.floor(now))}`;

  const areaCounts = useMemo(() => {
    const counts = Object.fromEntries(AREAS.map((a) => [a.id, 0])) as Record<AreaId, number>;
    for (const visit of visits) counts[visit.area] += 1;
    return counts;
  }, [visits]);

  const block = (visit: Visit, row: Row) => {
    const state = status(visit);
    const dragging = drag?.id === visit.id && drag.moving;
    const isOpen = selected === visit.id && open;
    const label =
      group === 'clients'
        ? initialed(CAREGIVERS.find((c) => c.id === visit.caregiver)?.name ?? '')
        : visit.short;
    const time =
      visit.end - visit.start <= 60
        ? short(visit.start)
        : `${short(visit.start)} - ${short(visit.end)}`;
    const style = {
      '--s': (visit.start - DAY_START) / SPAN,
      '--w': (visit.end - visit.start) / SPAN,
      ...(dragging
        ? { transform: `translate(${drag.dx / scaleOf()}px, ${drag.dy / scaleOf()}px)` }
        : null),
    } as CSSProperties;
    return (
      <button
        key={visit.id}
        type="button"
        className={styles.visit}
        style={style}
        data-visit={visit.id}
        data-part="visit"
        data-state={state}
        data-selected={selected === visit.id || undefined}
        data-hover={hover === visit.id || undefined}
        data-dragging={dragging || undefined}
        data-dim={
          row.dim?.has(visit.id) || (legend !== null && legend !== state) ? true : undefined
        }
        data-fresh={fresh === visit.id || undefined}
        aria-haspopup="dialog"
        aria-expanded={isOpen}
        aria-controls={isOpen ? `${id}-popover` : undefined}
        aria-label={`${visit.client}, ${long(visit.start)} to ${long(visit.end)}, ${STATE_LABEL[state]}${
          group === 'clients'
            ? `, with ${CAREGIVERS.find((c) => c.id === visit.caregiver)?.name}`
            : ''
        }`}
        aria-describedby={`${id}-move-hint`}
        onPointerDown={(event) => onBlockDown(event, visit, row.id)}
        onPointerMove={(event) => onBlockMove(event, visit, row.id)}
        onPointerUp={(event) => onBlockUp(event, visit, row.id)}
        onPointerCancel={() => setDrag(null)}
        onKeyDown={(event) => onBlockKey(event, visit, row.id)}
        onClick={(event) => {
          // Keyboard activation (pointer clicks are handled on pointer up).
          if (event.detail === 0) {
            opener.current = event.currentTarget;
            setSelected((current) => (current === visit.id ? null : visit.id));
          }
        }}
      >
        <span className={styles.visitName}>{label}</span>
        <span className={styles.visitTime}>{time}</span>
      </button>
    );
  };

  const links = (row: Row) =>
    row.visits.map((visit, index) => {
      const before = row.visits[index - 1];
      if (!before || visit.travel === undefined || group === 'clients') return null;
      const gap = visit.start - before.end;
      if (gap <= 0 || gap > 60) return null;
      if (drag?.moving && (drag.id === visit.id || drag.id === before.id)) return null;
      return (
        <span
          key={`link-${visit.id}`}
          className={styles.link}
          data-part="travel"
          style={
            {
              '--s': (before.end - DAY_START) / SPAN,
              '--w': gap / SPAN,
            } as CSSProperties
          }
          aria-hidden="true"
        >
          <span className={styles.linkLabel}>{visit.travel}m</span>
        </span>
      );
    });

  const ghost = (row: Row) => {
    if (!drag?.moving || drag.row !== row.id) return null;
    const visit = visits.find((v) => v.id === drag.id);
    if (!visit) return null;
    return (
      <span
        className={styles.ghost}
        data-ok={drag.ok || undefined}
        style={
          {
            '--s': (drag.start - DAY_START) / SPAN,
            '--w': (visit.end - visit.start) / SPAN,
          } as CSSProperties
        }
        aria-hidden="true"
      />
    );
  };

  const popoverCard = () => {
    if (!selectedVisit || !open) return null;
    const visit = selectedVisit;
    const state = status(visit);
    const carer = CAREGIVERS.find((c) => c.id === visit.caregiver) as Caregiver;
    const evv = checkIn(visit, state);
    return (
      <div
        ref={popover}
        id={`${id}-popover`}
        className={styles.popover}
        role="dialog"
        aria-label={`Visit: ${visit.client}`}
        data-part="popover"
      >
        <div className={styles.popHead}>
          <Avatar look={visit.look} className={styles.popAvatar} />
          <div className={styles.popWho}>
            <p className={styles.popName}>{visit.client}</p>
            <p className={styles.popCare}>
              {visit.care} · {AREA_LABEL[visit.area]}
            </p>
          </div>
          <span className={styles.pill} data-state={state}>
            <span className={styles.pillDot} aria-hidden="true" />
            {STATE_LABEL[state]}
          </span>
        </div>
        <dl className={styles.facts}>
          <div className={styles.fact}>
            <dt>Time</dt>
            <dd>
              {long(visit.start)} to {long(visit.end)}
            </dd>
          </div>
          <div className={styles.fact}>
            <dt>Caregiver</dt>
            <dd>
              <Avatar look={carer.look} className={styles.factAvatar} />
              {carer.name}
            </dd>
          </div>
          <div className={styles.fact}>
            <dt>EVV</dt>
            <dd data-ok={evv.ok || undefined} data-muted={evv.ok ? undefined : true}>
              {evv.ok ? <Icon name="check" className={styles.factCheck} /> : null}
              {evv.text}
            </dd>
          </div>
          <div className={styles.fact}>
            <dt>Note</dt>
            <dd
              data-note={visit.note ? true : undefined}
              data-muted={visit.note ? undefined : true}
            >
              {visit.note ? <span className={styles.noteDot} aria-hidden="true" /> : null}
              {visit.note ?? 'No notes'}
            </dd>
          </div>
        </dl>
        <div className={styles.popActions}>
          <button
            type="button"
            className={styles.primary}
            onClick={() => {
              onOpenVisit?.(visit);
              say(`Opening the visit with ${visit.client}.`);
            }}
          >
            Open visit
            <Icon name="chevronRight" className={styles.primaryIcon} />
          </button>
          <button
            type="button"
            className={styles.secondary}
            onClick={() => {
              onMessage?.(visit);
              say(`Message to ${firstName(carer.name)} started.`);
            }}
          >
            <Icon name="message" className={styles.secondaryIcon} />
            Message
          </button>
        </div>
      </div>
    );
  };

  const covered = (
    <div className={styles.row} role="row" data-kind="covered" data-part="coverage">
      <div role="rowheader" className={styles.whoCell}>
        <div className={styles.who}>
          <span className={styles.coverIcon} aria-hidden="true">
            <Icon name="checkCircle" className={styles.coverGlyph} />
          </span>
          <span className={styles.whoText}>
            <span className={styles.coverTitle}>All covered</span>
            <span className={styles.meta}>
              {view === 'week' ? weekCount : dayVisits} of {view === 'week' ? weekCount : dayVisits}{' '}
              visits
            </span>
          </span>
        </div>
      </div>
      <div role="cell" className={styles.lane}>
        <p className={styles.banner}>
          <Icon name="check" className={styles.bannerIcon} />
          <span className={styles.bannerText}>
            {view === 'week'
              ? `Nothing needs cover this week · ${weekCount} visits have a caregiver`
              : offset === 0
                ? "Nothing needs cover · Kwame's 3 visits went to Inés, Karim and Siobhán at 8:52 AM"
                : offset < 0
                  ? `Nothing needed cover · every one of ${dayVisits} visits had a caregiver`
                  : `Nothing needs cover · ${dayVisits} visits confirmed`}
          </span>
        </p>
      </div>
    </div>
  );

  const rowHead = (row: Row) => (
    <div className={styles.who}>
      <Avatar look={row.look} className={styles.avatar} />
      <span className={styles.whoText}>
        <span className={styles.name}>{row.name}</span>
        <span className={styles.meta}>
          {view === 'week' && row.kind === 'caregiver'
            ? `${CAREGIVERS.find((c) => c.id === row.id)?.role} · ${hoursOf(
                weekVisits.flat().filter((v) => v.caregiver === row.id),
              ).toFixed(1)} h`
            : row.meta}
        </span>
      </span>
    </div>
  );

  const weekCells = (row: Row) =>
    weekOffsets.map((day, index) => {
      const list = weekVisits[index].filter(
        (v) =>
          inArea(v) && (row.kind === 'client' ? v.client === row.name : v.caregiver === row.id),
      );
      const d = dateFor(day);
      const hours = hoursOf(list);
      return (
        <button
          key={day}
          type="button"
          className={styles.weekCell}
          data-today={day === 0 || undefined}
          data-empty={!list.length || undefined}
          style={{ '--fill': Math.min(hours / 9, 1) } as CSSProperties}
          aria-label={`${row.name}, ${WEEKDAYS[d.getDay()]} ${MONTHS[d.getMonth()]} ${d.getDate()}: ${
            list.length
          } visits, ${hours.toFixed(1)} hours. Open the day.`}
          onClick={() => {
            setView('day');
            goDay(day);
          }}
        >
          {list.length ? (
            <>
              <span className={styles.weekCount}>{list.length}</span>
              <span className={styles.weekHours}>{hours.toFixed(1)} h</span>
              <span className={styles.weekBar} aria-hidden="true" />
            </>
          ) : (
            <span className={styles.weekHours}>Off</span>
          )}
        </button>
      );
    });

  /* ---------------------------------------------------------------- render */

  return (
    <div
      ref={root}
      className={`${styles.root} ${className ?? ''}`}
      data-color-scheme={colorScheme}
      data-part="root"
      data-reduced={reduced || undefined}
      onPointerDownCapture={(event) => {
        if (event.isTrusted) take();
      }}
      onKeyDownCapture={take}
      onWheelCapture={take}
      onPointerMove={(event) => {
        if (event.pointerType !== 'touch') take();
      }}
      onKeyDown={onRootKey}
    >
      <div className={styles.viewport}>
        <div className={styles.canvas}>
          <div
            ref={frame}
            className={styles.frame}
            data-part="frame"
            data-collapsed={collapsed || undefined}
            data-drawer={drawer || undefined}
          >
            {/* ---------------------------------------------------- sidebar */}
            <div className={styles.scrim} aria-hidden="true" onClick={() => setDrawer(false)} />
            <nav
              id={`${id}-sidebar`}
              className={styles.sidebar}
              aria-label="Agency"
              data-part="sidebar"
            >
              <button
                type="button"
                className={styles.switcher}
                aria-label="Switch agency: Northfield Home Care"
              >
                <Mark className={styles.mark} />
                <span className={styles.switcherText}>
                  <span className={styles.product}>Rounds</span>
                  <span className={styles.agency}>Northfield Home Care</span>
                </span>
                <Icon name="switch" className={styles.switchIcon} />
              </button>
              <label className={styles.search}>
                <Icon name="search" className={styles.searchIcon} />
                <span className={styles.visuallyHidden}>Search caregivers and clients</span>
                <input
                  ref={searchBox}
                  className={styles.searchInput}
                  type="search"
                  placeholder="Search"
                  value={query}
                  onChange={(event) => {
                    setQuery(event.target.value);
                    setSelected(null);
                  }}
                />
                <kbd className={styles.kbd}>⌘K</kbd>
              </label>
              <div className={styles.navScroll}>
                {NAV.map((items, index) => (
                  <div key={SECTIONS[index]} className={styles.section}>
                    <p className={styles.sectionLabel}>{SECTIONS[index]}</p>
                    <ul className={styles.navList}>
                      {items.map((item) => (
                        <li key={item.id}>
                          <button
                            type="button"
                            className={styles.navItem}
                            aria-current={nav === item.id ? 'page' : undefined}
                            title={collapsed ? item.label : undefined}
                            onClick={() => {
                              setNav(item.id);
                              setDrawer(false);
                            }}
                          >
                            <Icon name={item.icon} className={styles.navIcon} />
                            <span className={styles.navLabel}>{item.label}</span>
                            {item.badge ? (
                              <span className={styles.badge} aria-label={`${item.badge} new`}>
                                {item.badge}
                              </span>
                            ) : null}
                          </button>
                        </li>
                      ))}
                    </ul>
                  </div>
                ))}
                <div className={styles.section}>
                  <div className={styles.sectionRow}>
                    <p className={styles.sectionLabel}>Service areas</p>
                    <button
                      type="button"
                      className={styles.addArea}
                      aria-label="Add a service area"
                      onClick={() => say('Adding service areas is up to an admin.')}
                    >
                      <Icon name="plus" className={styles.addIcon} />
                    </button>
                  </div>
                  <ul className={styles.navList}>
                    {AREAS.map((item) => (
                      <li key={item.id}>
                        <button
                          type="button"
                          className={styles.areaItem}
                          aria-pressed={area === item.id}
                          onClick={() => {
                            pickArea(area === item.id ? 'all' : item.id);
                            setDrawer(false);
                          }}
                        >
                          <span className={styles.areaSwatch} aria-hidden="true" />
                          <span className={styles.navLabel}>{item.label}</span>
                          <span className={styles.areaCount}>{areaCounts[item.id]}</span>
                        </button>
                      </li>
                    ))}
                  </ul>
                </div>
              </div>
              <div className={styles.sidebarFoot}>
                <ul className={styles.navList}>
                  <li>
                    <button type="button" className={styles.navItem} data-small>
                      <Icon name="help" className={styles.navIcon} />
                      <span className={styles.navLabel}>Help and support</span>
                    </button>
                  </li>
                  <li>
                    <button type="button" className={styles.navItem} data-small>
                      <Icon name="settings" className={styles.navIcon} />
                      <span className={styles.navLabel}>Settings</span>
                    </button>
                  </li>
                </ul>
                <button
                  type="button"
                  className={styles.user}
                  aria-label="Account: Renata Sousa, care coordinator"
                >
                  <Avatar
                    look={{
                      skin: '#d9a07a',
                      hair: '#2b201b',
                      cut: 'bun',
                      backdrop: '#e9e4d3',
                      shirt: '#3f4a5a',
                    }}
                    className={styles.userAvatar}
                  />
                  <span className={styles.switcherText}>
                    <span className={styles.product}>Renata</span>
                    <span className={styles.agency}>Care coordinator</span>
                  </span>
                  <Icon name="switch" className={styles.switchIcon} />
                </button>
              </div>
            </nav>

            {/* ---------------------------------------------------- main panel */}
            <main className={styles.panel} data-part="panel">
              <header className={styles.topbar} data-part="topbar">
                <button
                  type="button"
                  className={styles.toggle}
                  aria-label={collapsed ? 'Expand the sidebar' : 'Collapse the sidebar'}
                  aria-expanded={!collapsed}
                  aria-controls={`${id}-sidebar`}
                  onClick={() => setCollapsed((value) => !value)}
                >
                  <Icon name="sidebar" className={styles.toggleIcon} />
                </button>
                <button
                  type="button"
                  className={styles.menuButton}
                  aria-label="Menu"
                  aria-expanded={drawer}
                  aria-controls={`${id}-sidebar`}
                  onClick={() => setDrawer((value) => !value)}
                >
                  <Icon name={drawer ? 'close' : 'menu'} className={styles.toggleIcon} />
                </button>
                <span className={styles.divider} aria-hidden="true" />
                <nav aria-label="Breadcrumb" className={styles.crumbs}>
                  <span className={styles.crumb}>Operations</span>
                  <Icon name="chevronRight" className={styles.crumbIcon} />
                  <span className={styles.crumbHere} aria-current="page">
                    Schedule
                  </span>
                </nav>
                <span className={styles.clock} data-part="clock">
                  <span className={styles.clockDot} aria-hidden="true" />
                  <time className={styles.clockText}>{clockLabel}</time>
                </span>
                <button type="button" className={styles.iconButton} aria-label="Inbox" data-inbox>
                  <Icon name="inbox" className={styles.iconButtonGlyph} />
                </button>
                <button
                  type="button"
                  className={styles.iconButton}
                  aria-label={bell ? 'Notifications, 4 new' : 'Notifications'}
                  onClick={() => setBell(false)}
                >
                  <Icon name="bell" className={styles.iconButtonGlyph} />
                  {bell ? <span className={styles.bellDot} aria-hidden="true" /> : null}
                </button>
              </header>

              <div className={styles.head} data-part="header">
                <div className={styles.titles}>
                  <h2 className={styles.title}>Schedule</h2>
                  <p className={styles.subtitle}>
                    {view === 'week'
                      ? `${weekLabel} · ${weekCount} visits · ${CAREGIVERS.length} caregivers`
                      : `${WEEKDAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()} · ${dayVisits} visits · ${onShift} caregivers on shift`}
                  </p>
                </div>
                <div className={styles.controls}>
                  <div className={styles.stepper}>
                    <button
                      type="button"
                      className={styles.square}
                      aria-label={view === 'week' ? 'Previous week' : 'Previous day'}
                      onClick={() => goDay(offset - (view === 'week' ? 7 : 1))}
                    >
                      <Icon name="chevronLeft" className={styles.squareIcon} />
                    </button>
                    <button
                      type="button"
                      className={styles.today}
                      data-demo="today"
                      aria-pressed={offset === 0}
                      onClick={() => goDay(0)}
                    >
                      Today
                    </button>
                    <button
                      type="button"
                      className={styles.square}
                      data-demo="next"
                      aria-label={view === 'week' ? 'Next week' : 'Next day'}
                      onClick={() => goDay(offset + (view === 'week' ? 7 : 1))}
                    >
                      <Icon name="chevronRight" className={styles.squareIcon} />
                    </button>
                  </div>
                  <div className={styles.segments} role="group" aria-label="Range">
                    {(['day', 'week'] as const).map((item) => (
                      <button
                        key={item}
                        type="button"
                        className={styles.segment}
                        aria-pressed={view === item}
                        onClick={() => {
                          setView(item);
                          setSelected(null);
                        }}
                      >
                        {item === 'day' ? 'Day' : 'Week'}
                      </button>
                    ))}
                  </div>
                  <div className={styles.segments} role="group" aria-label="Rows">
                    {(['caregivers', 'clients'] as const).map((item) => (
                      <button
                        key={item}
                        type="button"
                        className={styles.segment}
                        aria-pressed={group === item}
                        onClick={() => {
                          setGroup(item);
                          setSelected(null);
                          setShowAll(false);
                        }}
                      >
                        {item === 'caregivers' ? 'Caregivers' : 'Clients'}
                      </button>
                    ))}
                  </div>
                  <div className={styles.areaPicker}>
                    <button
                      ref={areaButton}
                      type="button"
                      className={styles.select}
                      data-demo="area"
                      aria-haspopup="listbox"
                      aria-expanded={menu}
                      aria-controls={`${id}-areas`}
                      onClick={() => {
                        setMenuIndex(Math.max(0, AREAS.findIndex((a) => a.id === area) + 1));
                        setMenu((value) => !value);
                      }}
                      onKeyDown={(event) => {
                        if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
                          event.preventDefault();
                          setMenuIndex(Math.max(0, AREAS.findIndex((a) => a.id === area) + 1));
                          setMenu(true);
                        }
                      }}
                    >
                      <Icon name="pin" className={styles.selectIcon} />
                      <span className={styles.selectLabel}>
                        {area === 'all' ? 'All service areas' : AREA_LABEL[area]}
                      </span>
                      <Icon name="chevronDown" className={styles.selectChevron} />
                    </button>
                    {menu ? (
                      <ul
                        ref={areaMenu}
                        id={`${id}-areas`}
                        className={styles.menu}
                        role="listbox"
                        aria-label="Service area"
                        onKeyDown={(event) => {
                          const count = AREAS.length + 1;
                          if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
                            event.preventDefault();
                            setMenuIndex(
                              (index) =>
                                (index + (event.key === 'ArrowDown' ? 1 : -1) + count) % count,
                            );
                          } else if (event.key === 'Home' || event.key === 'End') {
                            event.preventDefault();
                            setMenuIndex(event.key === 'Home' ? 0 : count - 1);
                          } else if (event.key === 'Escape' || event.key === 'Tab') {
                            event.stopPropagation();
                            setMenu(false);
                            areaButton.current?.focus();
                          }
                        }}
                      >
                        {[{ id: 'all' as const, label: 'All service areas' }, ...AREAS].map(
                          (item, index) => (
                            <li
                              key={item.id}
                              role="option"
                              tabIndex={index === menuIndex ? 0 : -1}
                              aria-selected={area === item.id}
                              className={styles.option}
                              data-demo={`area-${item.id}`}
                              data-active={index === menuIndex || undefined}
                              onClick={() => {
                                pickArea(item.id);
                                areaButton.current?.focus();
                              }}
                              onKeyDown={(event) => {
                                if (event.key === 'Enter' || event.key === ' ') {
                                  event.preventDefault();
                                  pickArea(item.id);
                                  areaButton.current?.focus();
                                }
                              }}
                            >
                              <span>{item.label}</span>
                              <span className={styles.optionCount}>
                                {item.id === 'all' ? dayVisits : areaCounts[item.id]}
                              </span>
                              {area === item.id ? (
                                <Icon name="check" className={styles.optionCheck} />
                              ) : null}
                            </li>
                          ),
                        )}
                      </ul>
                    ) : null}
                  </div>
                  <button type="button" className={styles.newVisit} onClick={newVisit}>
                    <Icon name="plus" className={styles.newIcon} />
                    New visit
                  </button>
                </div>
              </div>

              {/* ---------------------------------------------------- the board */}
              <section
                ref={board}
                className={styles.board}
                aria-label="Schedule"
                data-part="board"
                data-view={view}
                data-legend={legend ?? undefined}
              >
                <div className={styles.legendRow} data-part="legend">
                  <p className={styles.boardDate}>
                    <Icon name="calendarSmall" className={styles.boardDateIcon} />
                    {view === 'week' ? weekLabel : dayLabel} · By{' '}
                    {group === 'clients' ? 'client' : 'caregiver'}
                  </p>
                  <div className={styles.legend} role="group" aria-label="Highlight a state">
                    {STATES.map((state) => (
                      <button
                        key={state.id}
                        type="button"
                        className={styles.legendItem}
                        data-demo={`legend-${state.id}`}
                        data-state={state.id}
                        aria-pressed={legend === state.id}
                        onClick={() =>
                          setLegend((current) => (current === state.id ? null : state.id))
                        }
                      >
                        <span className={styles.swatch} aria-hidden="true" />
                        {state.label}
                      </button>
                    ))}
                  </div>
                </div>

                <div ref={scroller} className={styles.scroller} data-part="timeline">
                  <div
                    className={styles.sheet}
                    role="table"
                    aria-label={`Visits by ${group === 'clients' ? 'client' : 'caregiver'}`}
                  >
                    <div className={styles.headRow} role="row">
                      <div className={styles.headWho} role="columnheader">
                        {group === 'clients' ? 'Client' : 'Caregiver'}
                        <span className={styles.headMore}>
                          {group === 'clients' ? ' · Visits today' : ' · Hours today'}
                        </span>
                      </div>
                      <div className={styles.headLane} role="columnheader" aria-label="Time of day">
                        {view === 'day'
                          ? HOURS.slice(0, 12).map((hour) => (
                              <span
                                key={hour}
                                className={styles.hour}
                                style={{ '--s': (hour - DAY_START) / SPAN } as CSSProperties}
                              >
                                {`${((Math.floor(hour / 60) + 11) % 12) + 1} ${hour < 720 ? 'AM' : 'PM'}`}
                              </span>
                            ))
                          : weekOffsets.map((day) => {
                              const d = dateFor(day);
                              return (
                                <span
                                  key={day}
                                  className={styles.weekDay}
                                  data-today={day === 0 || undefined}
                                >
                                  {WEEKDAYS[d.getDay()].slice(0, 3)} {d.getDate()}
                                </span>
                              );
                            })}
                        {showNow ? (
                          <span
                            className={styles.nowTrack}
                            style={{ '--now': nowShare } as CSSProperties}
                            aria-hidden="true"
                          >
                            <span className={styles.nowMark}>
                              <span className={styles.nowChip}>{short(Math.floor(now))}</span>
                            </span>
                          </span>
                        ) : null}
                      </div>
                    </div>

                    <div className={styles.body} role="rowgroup">
                      {covered}
                      {listed.map((row) => (
                        <div
                          key={row.id}
                          role="row"
                          className={styles.row}
                          data-row={row.kind === 'out' ? undefined : row.id}
                          data-kind={row.kind}
                          data-part="row"
                          data-target={
                            drag?.moving && drag.row === row.id
                              ? drag.ok
                                ? 'ok'
                                : 'no'
                              : undefined
                          }
                          data-lifted={
                            (drag?.moving && row.visits.some((v) => v.id === drag.id)) || undefined
                          }
                        >
                          <div role="rowheader" className={styles.whoCell}>
                            {rowHead(row)}
                          </div>
                          <div role="cell" className={styles.lane} data-lane>
                            {row.kind === 'out' ? (
                              <p className={styles.banner} data-kind="out">
                                <Icon name="info" className={styles.bannerIcon} />
                                <span className={styles.bannerText}>
                                  Unavailable today · sick, reported 8:31 AM from the app · his 3
                                  visits were reassigned at 8:52 AM
                                </span>
                              </p>
                            ) : view === 'week' ? (
                              <div className={styles.week}>{weekCells(row)}</div>
                            ) : (
                              <>
                                {links(row)}
                                {ghost(row)}
                                {row.visits.map((visit) => block(visit, row))}
                              </>
                            )}
                          </div>
                        </div>
                      ))}
                      {!listed.length ? (
                        <p className={styles.empty}>Nothing matches. Try another area or search.</p>
                      ) : null}
                      {view === 'day' ? (
                        <div className={styles.lines} aria-hidden="true">
                          {HOURS.map((hour) => (
                            <span
                              key={hour}
                              className={styles.line}
                              style={{ '--s': (hour - DAY_START) / SPAN } as CSSProperties}
                            />
                          ))}
                        </div>
                      ) : null}
                      {showNow ? (
                        <span
                          className={styles.nowTrack}
                          data-body
                          style={{ '--now': nowShare } as CSSProperties}
                          aria-hidden="true"
                          data-part="now-line"
                        >
                          <span className={styles.nowMark} />
                        </span>
                      ) : null}
                    </div>
                  </div>
                </div>

                <div className={styles.foot}>
                  {canExpand ? (
                    <button
                      type="button"
                      className={styles.footButton}
                      aria-expanded={showAll}
                      onClick={() => setShowAll((value) => !value)}
                    >
                      Showing {listed.length} of {total} {noun}
                      <span className={styles.footHint}>
                        {showAll ? ' · Show fewer' : ' · Show all'}
                      </span>
                    </button>
                  ) : (
                    <p className={styles.footText}>
                      Showing {listed.length} of {total} {noun}
                    </p>
                  )}
                  <p className={styles.footText} data-end>
                    Dotted links are travel time between visits
                  </p>
                </div>
                {popoverCard()}
              </section>
            </main>
          </div>
        </div>
      </div>
      <p id={`${id}-move-hint`} className={styles.visuallyHidden}>
        Drag, or press Alt with the arrow keys, to move it to another caregiver or time.
      </p>
      <p className={styles.visuallyHidden} role="status" aria-live="polite">
        {said}
      </p>
      <DemoPointer
        ref={pointer}
        className={styles.pointer}
        arrowClass={styles.pointerArrow}
        handClass={styles.pointerHand}
      />
    </div>
  );
}
```

### care-schedule/care-schedule.module.css

```css
/* Care schedule (care-schedule.tsx): a warm paper frame with a white panel, one near-black for
   what is happening now, greys for what is done, red for missed and amber for at risk; by night
   a warm charcoal frame with the same roles.

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

   Wide (the root 880 px and up): the frame is the 1440 × 1025 design scaled to the root's width
   by a transform (tan(atan2()) turns a length over a length into a number), so it is laid out
   once and looks the same at every size. Narrow: a container query reflows it: the sidebar
   becomes a drawer, the controls wrap and the timeline scrolls sideways inside its card.

   The timeline is shares of the day: each block, link and line is placed by --s (where it
   starts) and --w (how long it lasts), so the same rules serve every width. The now line moves
   by a transform. */

@font-face {
  font-family: CareInter;
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  src:
    local('Inter Variable'),
    url('/assets/inter-latin-wght-3100e775.woff2') format('woff2');
}
@font-face {
  font-family: CareMono;
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  src: url('/assets/library-geist-mono-latin.woff2') format('woff2');
}

.root {
  --frame: #f1f0ec;
  --frame-line: #e0dfda;
  --panel: #fff;
  --panel-line: #e9e8e4;
  --card: #fff;
  --card-line: #e6e5e1;
  --board: #f3f2ef;
  --sheet-line: #ebeae6;
  --grid: #dcdbd6;
  --soft: #faf9f7;
  --control: #f2f1ee;
  --ink: #1b1a18;
  --ink-2: #3d3c39;
  --ink-3: #75736e;
  --ink-4: #8e8c87;
  --ink-5: #b9b7b1;
  --solid: #1a1817;
  --on-solid: #fff;
  --on-solid-2: #b9b6b0;
  --done: #ecebe6;
  --done-ink: #46443f;
  --done-time: #8a8883;
  --plan-line: #dcdbd6;
  --miss: #fdece6;
  --miss-line: #e1857a;
  --miss-ink: #c0392b;
  --risk: #fbf1e3;
  --risk-line: #e2b56d;
  --risk-ink: #8f5f14;
  --good: #2e7a4f;
  --good-bg: #e4efe6;
  --good-dot: #2f9e5e;
  --note: #e09a26;
  --badge: #e5442f;
  --mark: #1a1817;
  --on-mark: #fff;
  --swatch: #bdbcb7;
  --ok: #2f9e5e;
  --ok-bg: rgb(47 158 94 / 0.08);
  --no-bg: rgb(192 57 43 / 0.07);
  --scrim: rgb(20 18 16 / 0.32);
  --shadow-small: rgb(30 25 20 / 0.06);
  --shadow: rgb(30 25 20 / 0.13);
  --focus: #3b73f5;
  --pointer: #1c1c1e;
  --pointer-edge: #fff;
  --pointer-shadow: rgb(0 0 0 / 0.25);
  position: relative;
  width: 100%;
  min-width: 0;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --frame: light-dark(#f1f0ec, #121110);
    --frame-line: light-dark(#e0dfda, #262422);
    --panel: light-dark(#fff, #1a1917);
    --panel-line: light-dark(#e9e8e4, #2a2826);
    --card: light-dark(#fff, #22201e);
    --card-line: light-dark(#e6e5e1, #34312e);
    --board: light-dark(#f3f2ef, #151413);
    --sheet-line: light-dark(#ebeae6, #2c2a27);
    --grid: light-dark(#dcdbd6, #3a3734);
    --soft: light-dark(#faf9f7, #1f1d1b);
    --control: light-dark(#f2f1ee, #252321);
    --ink: light-dark(#1b1a18, #f3f1ed);
    --ink-2: light-dark(#3d3c39, #d9d6d0);
    --ink-3: light-dark(#75736e, #a29f98);
    --ink-4: light-dark(#8e8c87, #85827b);
    --ink-5: light-dark(#b9b7b1, #5d5a55);
    --solid: light-dark(#1a1817, #f3f1ed);
    --on-solid: light-dark(#fff, #161513);
    --on-solid-2: light-dark(#b9b6b0, #6a665f);
    --done: light-dark(#ecebe6, #2d2b28);
    --done-ink: light-dark(#46443f, #cfccc5);
    --done-time: light-dark(#8a8883, #8c8881);
    --plan-line: light-dark(#dcdbd6, #46423e);
    --miss: light-dark(#fdece6, #3a1f1b);
    --miss-line: light-dark(#e1857a, #b0574c);
    --miss-ink: light-dark(#c0392b, #ff8f80);
    --risk: light-dark(#fbf1e3, #35291a);
    --risk-line: light-dark(#e2b56d, #a77b33);
    --risk-ink: light-dark(#8f5f14, #f0c27a);
    --good: light-dark(#2e7a4f, #6fd39a);
    --good-bg: light-dark(#e4efe6, #1d3326);
    --good-dot: light-dark(#2f9e5e, #57c785);
    --note: light-dark(#e09a26, #f0b04a);
    --badge: light-dark(#e5442f, #f0604c);
    --mark: light-dark(#1a1817, #f3f1ed);
    --on-mark: light-dark(#fff, #161513);
    --swatch: light-dark(#bdbcb7, #57534e);
    --ok: light-dark(#2f9e5e, #57c785);
    --ok-bg: light-dark(rgb(47 158 94 / 0.08), rgb(87 199 133 / 0.1));
    --no-bg: light-dark(rgb(192 57 43 / 0.07), rgb(255 143 128 / 0.1));
    --scrim: light-dark(rgb(20 18 16 / 0.32), rgb(0 0 0 / 0.55));
    --shadow-small: light-dark(rgb(30 25 20 / 0.06), rgb(0 0 0 / 0.3));
    --shadow: light-dark(rgb(30 25 20 / 0.13), rgb(0 0 0 / 0.5));
    --focus: light-dark(#3b73f5, #7ba4fe);
  }
}
/* The colorScheme prop. Without it, nothing is set: color-scheme is inherited from the page. */
.root[data-color-scheme='light'] {
  color-scheme: light;
}
.root[data-color-scheme='dark'] {
  color-scheme: dark;
}
.root[data-color-scheme='system'] {
  color-scheme: light dark;
}

/* The box the layout answers to (container queries and units). */
.viewport {
  width: 100%;
  container: care / inline-size;
}

/* Holds the scaled design's place: its proportions at the viewport's width. */
.canvas {
  position: relative;
  aspect-ratio: 1440 / 1025;
}

/* The 1440 × 1025 design, scaled to the viewport's width. */
.frame {
  position: absolute;
  top: 0;
  left: 0;
  isolation: isolate;
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  box-sizing: border-box;
  width: 1440px;
  height: 1025px;
  overflow: hidden;
  border: 1px solid var(--frame-line);
  border-radius: 18px;
  color: var(--ink);
  background: var(--frame);
  font-family:
    CareInter,
    Inter,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    sans-serif;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -0.003em;
  transform: scale(tan(atan2(100cqw, 1440px)));
  transform-origin: 0 0;
  -webkit-font-smoothing: antialiased;
}
.frame[data-collapsed] {
  grid-template-columns: 68px minmax(0, 1fr);
}
:where(.frame) *,
:where(.frame) *::before,
:where(.frame) *::after {
  box-sizing: border-box;
}
:where(.frame) :where(p, h2, ul, dl, dd) {
  margin: 0;
  padding: 0;
  font: inherit;
}
:where(.frame) :where(ul) {
  list-style: none;
}
:where(.frame) :where(button, input) {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  color: inherit;
  background: transparent;
  font: inherit;
  letter-spacing: inherit;
  text-align: inherit;
}
:where(.frame) :where(button) {
  cursor: pointer;
}
:where(.frame) :where(svg) {
  display: block;
  flex: none;
  max-width: none;
}
:where(.frame) :where(button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* ------------------------------------------------------------------ sidebar */

.scrim {
  display: none;
}

.sidebar {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: 17px 12px 16px 16px;
}
.switcher,
.user {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 56px;
  padding: 0 12px 0 8px;
  border: 1px solid var(--card-line);
  border-radius: 10px;
  background: var(--card);
  box-shadow: 0 1px 2px var(--shadow-small);
}
.mark {
  width: 32px;
  height: 32px;
}
.switcherText {
  display: grid;
  margin-left: -1px;
  flex: 1;
  min-width: 0;
}
.product {
  overflow: hidden;
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 18px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.agency {
  overflow: hidden;
  color: var(--ink-3);
  font-size: 12px;
  line-height: 16px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.switchIcon {
  width: 17px;
  height: 17px;
  color: var(--ink-2);
}

.search {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 35px;
  margin-top: 11px;
  padding: 0 7px 0 7px;
  border: 1px solid var(--card-line);
  border-radius: 9px;
  background: var(--card);
  cursor: text;
}
.search:focus-within {
  border-color: var(--ink-4);
}
.searchIcon {
  width: 17px;
  height: 17px;
  color: var(--ink-3);
}
.searchInput {
  flex: 1;
  min-width: 0;
  height: 100%;
  color: var(--ink);
  font-size: 13.5px;
  outline: none;
}
.searchInput::placeholder {
  color: var(--ink-4);
  opacity: 1;
}
.searchInput::-webkit-search-cancel-button {
  display: none;
}
.searchInput:focus-visible {
  outline: none;
}
.kbd {
  display: grid;
  place-items: center;
  height: 21px;
  padding: 0 5px;
  border: 1px solid var(--card-line);
  border-radius: 5px;
  color: var(--ink-4);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 10.5px;
}

.navScroll {
  flex: 1;
  min-height: 0;
  margin-top: 11px;
}
.section + .section {
  margin-top: 13px;
}
.sectionRow + .navList {
  margin-top: -4px;
}
.sectionRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.sectionLabel {
  height: 30px;
  padding: 7px 0 0 10px;
  color: var(--ink-3);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
  line-height: 16px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.addArea {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin: 3px 2px 0 0;
  border-radius: 6px;
  color: var(--ink-3);
}
.addArea:hover {
  background: var(--control);
  color: var(--ink);
}
.addIcon {
  width: 16px;
  height: 16px;
}
.navItem,
.areaItem {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 38px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: 9px;
  color: var(--ink-2);
  font-size: 14px;
  white-space: nowrap;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}
.navItem[data-small] {
  height: 36px;
}
.navItem:hover,
.areaItem:hover {
  background: rgb(0 0 0 / 0.035);
  color: var(--ink);
}
.navItem[aria-current='page'] {
  border-color: var(--card-line);
  background: var(--card);
  color: var(--ink);
  font-weight: 500;
  box-shadow: 0 1px 2px var(--shadow-small);
}
.navIcon {
  width: 18px;
  height: 18px;
}
.navLabel {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.badge {
  display: grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 5px;
  background: var(--badge);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
}
.areaItem {
  height: 32px;
  color: var(--ink-2);
}
.areaItem[aria-pressed='true'] {
  background: var(--card);
  color: var(--ink);
  border-color: var(--card-line);
}
.areaSwatch {
  width: 9px;
  height: 9px;
  margin: 0 4px;
  border-radius: 2px;
  background: var(--swatch);
}
.areaItem[aria-pressed='true'] .areaSwatch {
  background: var(--ink);
}
.areaCount {
  color: var(--ink-4);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
}

.sidebarFoot {
  display: grid;
  gap: 10px;
}
.userAvatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
}

/* The collapsed sidebar: an icon rail. */
.frame[data-collapsed] .sidebar {
  padding-right: 12px;
  padding-left: 12px;
}
.frame[data-collapsed]
  :is(.switcherText, .switchIcon, .search, .navLabel, .sectionLabel, .areaCount, .addArea, .badge) {
  display: none;
}
.frame[data-collapsed] :is(.switcher, .user) {
  justify-content: center;
  padding: 0;
}
.frame[data-collapsed] .section + .section {
  margin-top: 0;
  padding-top: 10px;
  border-top: 1px solid var(--frame-line);
}
.frame[data-collapsed] .navScroll {
  margin-top: 12px;
}
.frame[data-collapsed] :is(.navItem, .areaItem) {
  justify-content: center;
  padding: 0;
}

/* ------------------------------------------------------------------ panel */

.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  margin: 8px 8px 8px 0;
  border: 1px solid var(--panel-line);
  border-radius: 14px;
  background: var(--panel);
}

.topbar {
  display: flex;
  align-items: center;
  height: 57px;
  padding: 0 18px 0 22px;
  border-bottom: 1px solid var(--panel-line);
}
.toggle,
.menuButton {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin-left: -4px;
  border-radius: 7px;
  color: var(--ink-3);
}
.toggle:hover,
.menuButton:hover {
  background: var(--control);
  color: var(--ink);
}
.menuButton {
  display: none;
}
.toggleIcon {
  width: 19px;
  height: 19px;
}
.divider {
  width: 1px;
  height: 18px;
  margin: 0 8px 0 4px;
  background: var(--panel-line);
}
.crumbs {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 13px;
  white-space: nowrap;
}
.crumb {
  color: var(--ink-4);
}
.crumbIcon {
  width: 13px;
  height: 13px;
  color: var(--ink-4);
}
.crumbHere {
  color: var(--ink);
  font-weight: 500;
}
.clock {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  margin-right: 10px;
  padding: 0 12px 0 10px;
  border-radius: 15px;
  background: var(--control);
  color: var(--ink-2);
  white-space: nowrap;
}
.clockDot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--good-dot);
  box-shadow: 0 0 0 0 var(--good-dot);
  animation: care-live 2.4s ease-out infinite;
}
@keyframes care-live {
  0% {
    box-shadow: 0 0 0 0 rgb(47 158 94 / 0.45);
  }
  70%,
  100% {
    box-shadow: 0 0 0 5px rgb(47 158 94 / 0);
  }
}
.clockText {
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11.5px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.iconButton {
  position: relative;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--card-line);
  border-radius: 10px;
  background: var(--card);
  color: var(--ink);
  transition: background-color 0.15s ease;
}
.iconButton + .iconButton {
  margin-left: 8px;
}
.iconButton:hover {
  background: var(--control);
}
.iconButtonGlyph {
  width: 18px;
  height: 18px;
}
.bellDot {
  position: absolute;
  top: 5px;
  right: 6px;
  width: 7px;
  height: 7px;
  border: 1.5px solid var(--card);
  border-radius: 50%;
  background: var(--badge);
  box-sizing: content-box;
}

/* The title and the controls. */
.head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 23px 0 24px;
}
.titles {
  min-width: 0;
  padding-top: 2px;
}
.title {
  margin: 0;
  color: var(--ink);
  font-size: 22px;
  font-weight: 500;
  line-height: 28px;
  letter-spacing: -0.015em;
}
.subtitle {
  margin-top: 3px;
  color: var(--ink-3);
  font-size: 13px;
  line-height: 18px;
  white-space: nowrap;
}
.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 9px;
  padding-top: 8px;
}
.stepper {
  display: flex;
  gap: 5px;
}
.square,
.today,
.select {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 36px;
  border: 1px solid var(--card-line);
  border-radius: 9px;
  background: var(--card);
  color: var(--ink);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  transition: background-color 0.15s ease;
}
.square {
  width: 36px;
}
.today {
  width: 62px;
}
.square:hover,
.today:hover,
.select:hover {
  background: var(--control);
}
.today[aria-pressed='false'] {
  border-color: var(--ink-5);
}
.squareIcon {
  width: 16px;
  height: 16px;
}
.segments {
  display: flex;
  gap: 2px;
  height: 36px;
  padding: 3px;
  border-radius: 10px;
  background: var(--control);
}
.segment {
  padding: 0 9px;
  border: 1px solid transparent;
  border-radius: 7px;
  color: var(--ink-4);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  transition:
    color 0.15s ease,
    background-color 0.15s ease;
}
.segment:hover {
  color: var(--ink);
}
.segment[aria-pressed='true'] {
  border-color: var(--card-line);
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 1px 2px var(--shadow-small);
}
.areaPicker {
  position: relative;
}
.select {
  gap: 7px;
  padding: 0 10px 0 10px;
  font-weight: 500;
}
.selectIcon {
  width: 17px;
  height: 17px;
  color: var(--ink-2);
}
.selectChevron {
  width: 15px;
  height: 15px;
  margin-left: 0;
  color: var(--ink-3);
}
.menu {
  position: absolute;
  z-index: 6;
  top: calc(100% + 6px);
  right: 0;
  width: 220px;
  padding: 5px;
  border: 1px solid var(--card-line);
  border-radius: 11px;
  background: var(--card);
  box-shadow:
    0 12px 32px var(--shadow),
    0 2px 6px var(--shadow-small);
  animation: care-pop 0.16s ease-out;
}
.option {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 10px;
  border-radius: 7px;
  color: var(--ink-2);
  font-size: 13.5px;
  cursor: pointer;
  outline: none;
}
.option > span:first-child {
  flex: 1;
}
.option:hover,
.option[data-active] {
  background: var(--control);
  color: var(--ink);
}
.option:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}
.optionCount {
  color: var(--ink-4);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
}
.optionCheck {
  width: 15px;
  height: 15px;
}
.newVisit {
  display: flex;
  align-items: center;
  gap: 7px;
  height: 36px;
  padding: 0 13px 0 11px;
  border-radius: 9px;
  background: var(--solid);
  color: var(--on-solid);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  transition: opacity 0.15s ease;
}
.newVisit:hover {
  opacity: 0.86;
}
.newIcon {
  width: 17px;
  height: 17px;
}

/* ------------------------------------------------------------------ the board */

.board {
  position: relative;
  display: flex;
  flex: 0 1 auto;
  flex-direction: column;
  min-height: 0;
  margin: 19px 23px 6px 24px;
  padding: 0 4px;
  border-radius: 13px;
  background: var(--board);
}
.legendRow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 16px;
  min-height: 41px;
  padding: 2px 7px 0 7px;
}
.boardDate,
.legendItem,
.headRow,
.meta,
.footText,
.footButton {
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.045em;
  text-transform: uppercase;
}
.boardDate {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--ink-3);
  white-space: nowrap;
}
.boardDateIcon {
  width: 15px;
  height: 15px;
}
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
}
.legendItem {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 0;
  color: var(--ink-4);
  white-space: nowrap;
  transition: color 0.15s ease;
}
.legendItem:hover,
.legendItem[aria-pressed='true'] {
  color: var(--ink);
}
.swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
}
.legendItem[data-state='confirmed'] .swatch {
  border: 1px solid var(--plan-line);
  background: var(--card);
}
.legendItem[data-state='in-progress'] .swatch {
  background: var(--solid);
}
.legendItem[data-state='completed'] .swatch {
  background: var(--done);
}
.legendItem[data-state='missed'] .swatch {
  border: 1px solid var(--miss-line);
  background: var(--miss);
}
.legendItem[data-state='at-risk'] .swatch {
  border: 1px solid var(--risk-line);
  background: var(--risk);
}

/* The table: a white sheet with the caregivers on the left and the day on the right. */
.scroller {
  position: relative;
  flex: 0 1 auto;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--card-line);
  border-radius: 9px;
  background: var(--card);
  scrollbar-width: thin;
}
.sheet {
  --who: 208px;
  --pad: 8px;
  position: relative;
  min-width: 100%;
}
.headRow {
  position: sticky;
  z-index: 4;
  top: 0;
  display: flex;
  height: 32px;
  border-bottom: 1px solid var(--sheet-line);
  background: var(--card);
  color: var(--ink-4);
}
.headWho {
  position: sticky;
  z-index: 2;
  left: 0;
  flex: none;
  width: var(--who);
  padding: 7px 0 0 16px;
  border-right: 1px solid var(--sheet-line);
  background: var(--card);
  white-space: nowrap;
}
.headLane {
  position: relative;
  flex: 1;
}
.hour {
  position: absolute;
  top: 7px;
  left: calc(var(--pad) + var(--s) * (100% - 2 * var(--pad)) + 5px);
  white-space: nowrap;
}
.weekDay {
  display: inline-block;
  width: calc(100% / 7);
  padding: 9px 0 0 12px;
  white-space: nowrap;
}
.weekDay[data-today] {
  color: var(--ink);
}

.body {
  position: relative;
}
.row {
  position: relative;
  display: flex;
  height: 57px;
  border-bottom: 1px solid var(--sheet-line);
  transition: background-color 0.15s ease;
}
.row[data-kind='covered'] {
  height: 63px;
  background: var(--soft);
}
.row[data-kind='out'] {
  height: 56px;
  border-bottom: 0;
}
.row[data-target='ok'] {
  background: var(--ok-bg);
}
.row[data-target='no'] {
  background: var(--no-bg);
}
.whoCell {
  position: sticky;
  z-index: 4;
  left: 0;
  flex: none;
  width: var(--who);
  border-right: 1px solid var(--sheet-line);
  background: inherit;
}
.row:not([data-kind='covered']) .whoCell {
  background: var(--card);
}
.who {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 100%;
  padding: 0 10px 0 14px;
}
.avatar {
  width: 29px;
  height: 29px;
  border-radius: 50%;
}
.whoText {
  display: grid;
  min-width: 0;
}
.name {
  overflow: hidden;
  color: var(--ink);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 18px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.meta {
  overflow: hidden;
  color: var(--ink-4);
  line-height: 15px;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.row[data-kind='out'] .name {
  color: var(--ink-3);
  font-weight: 400;
}
.row[data-kind='out'] .meta {
  color: var(--miss-ink);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.coverIcon {
  display: grid;
  place-items: center;
  width: 31px;
  height: 31px;
  border-radius: 7px;
  background: var(--good-bg);
  color: var(--good);
}
.coverGlyph {
  width: 18px;
  height: 18px;
}
.coverTitle {
  color: var(--good);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 18px;
}

.lane {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
}
.row[data-lifted] .lane {
  z-index: 2;
}
.banner {
  position: absolute;
  inset: 11px 10px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 14px;
  border: 1px dashed var(--grid);
  border-radius: 8px;
  background: var(--card);
  color: var(--ink-3);
  font-size: 13px;
  white-space: nowrap;
}
.banner[data-kind='out'] {
  inset: 8px 10px;
  background: var(--soft);
}
.bannerIcon {
  width: 15px;
  height: 15px;
  color: var(--good);
}
.banner[data-kind='out'] .bannerIcon {
  width: 17px;
  height: 17px;
  color: var(--ink-3);
}
.bannerText {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Hour lines, under the rows' content. */
.lines {
  position: absolute;
  z-index: 0;
  inset: 0 0 0 var(--who);
  pointer-events: none;
}
.line {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--pad) + var(--s) * (100% - 2 * var(--pad)));
  width: 1px;
  background: repeating-linear-gradient(to bottom, var(--grid) 0 3px, transparent 3px 6px);
}

/* A visit. */
.visit {
  position: absolute;
  top: 9px;
  left: calc(var(--pad) + var(--s) * (100% - 2 * var(--pad)));
  display: grid;
  align-content: center;
  width: calc(var(--w) * (100% - 2 * var(--pad)) - 2px);
  height: 39px;
  padding: 0 8px;
  overflow: hidden;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--done);
  color: var(--done-ink);
  touch-action: none;
  cursor: grab;
  transition:
    opacity 0.2s ease,
    box-shadow 0.15s ease,
    border-color 0.15s ease,
    transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.visitName {
  overflow: hidden;
  font-size: 13px;
  font-weight: 500;
  line-height: 16px;
  white-space: nowrap;
  text-overflow: clip;
}
.visitTime {
  overflow: hidden;
  color: var(--done-time);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11.5px;
  line-height: 16px;
  white-space: nowrap;
}
.visit[data-state='confirmed'] {
  border-color: var(--plan-line);
  background: var(--card);
  color: var(--ink);
}
.visit[data-state='confirmed'] .visitTime {
  color: var(--ink-4);
}
.visit[data-state='in-progress'] {
  background: var(--solid);
  color: var(--on-solid);
}
.visit[data-state='in-progress'] .visitTime {
  color: var(--on-solid-2);
}
.visit[data-state='missed'] {
  border: 1px dashed var(--miss-line);
  background: var(--miss);
  color: var(--miss-ink);
}
.visit[data-state='missed'] .visitTime {
  color: var(--miss-ink);
}
.visit[data-state='at-risk'] {
  border-color: var(--risk-line);
  background: var(--risk);
  color: var(--risk-ink);
}
.visit[data-state='at-risk'] .visitTime {
  color: var(--risk-ink);
  opacity: 0.8;
}
.visit:hover,
.visit[data-hover] {
  z-index: 2;
  box-shadow: 0 3px 10px var(--shadow);
}
.visit[data-state='completed']:hover,
.visit[data-state='completed'][data-hover],
.visit[data-state='confirmed']:hover,
.visit[data-state='confirmed'][data-hover] {
  border-color: var(--ink-5);
}
.visit[data-selected] {
  z-index: 2;
  border: 1.5px solid var(--ink);
  padding: 0 7.5px;
}
.visit[data-selected][data-state='in-progress'] {
  box-shadow:
    0 0 0 2px var(--card),
    0 0 0 3.5px var(--ink);
}
.visit[data-dim] {
  opacity: 0.28;
}
.visit[data-dragging] {
  z-index: 5;
  cursor: grabbing;
  box-shadow: 0 10px 24px var(--shadow);
  transition: box-shadow 0.15s ease;
}
.visit[data-fresh] {
  animation: care-fresh 0.9s ease-out;
}
@keyframes care-fresh {
  0% {
    box-shadow: 0 0 0 0 rgb(47 158 94 / 0.55);
  }
  100% {
    box-shadow: 0 0 0 7px rgb(47 158 94 / 0);
  }
}
.visit:focus-visible {
  z-index: 3;
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Where a dragged visit would land. */
.ghost {
  position: absolute;
  top: 9px;
  left: calc(var(--pad) + var(--s) * (100% - 2 * var(--pad)));
  width: calc(var(--w) * (100% - 2 * var(--pad)) - 2px);
  height: 39px;
  border: 1.5px dashed var(--miss-line);
  border-radius: 6px;
  pointer-events: none;
}
.ghost[data-ok] {
  border-color: var(--ok);
}

/* Travel between two visits: a dotted link with its minutes. */
.link {
  position: absolute;
  top: 28px;
  left: calc(var(--pad) + var(--s) * (100% - 2 * var(--pad)));
  display: grid;
  place-items: center;
  width: calc(var(--w) * (100% - 2 * var(--pad)) - 2px);
  height: 1px;
  background: radial-gradient(circle at 1px 0.5px, var(--ink-5) 0.75px, transparent 1px) 2px 0 / 4px
    1px repeat-x;
  pointer-events: none;
}
.linkLabel {
  padding: 0 4px;
  background: var(--card);
  color: var(--ink-4);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
  line-height: 14px;
}

/* The now line and its chip: a full-width track moved by --now (0 at 7 AM, 1 at 7 PM). */
.nowTrack {
  position: absolute;
  z-index: 3;
  top: 0;
  right: var(--pad);
  bottom: 0;
  left: var(--pad);
  transform: translateX(calc(var(--now) * 100%));
  pointer-events: none;
  transition: transform 1s linear;
}
.nowTrack[data-body] {
  left: calc(var(--who) + var(--pad));
}
/* Only the track's left edge is seen: the rest is clipped away. */
.headLane,
.body {
  overflow: clip;
}
.nowMark {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
}
.nowTrack[data-body] .nowMark::before {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -0.75px;
  width: 1.5px;
  background: var(--ink);
  content: '';
}
.nowChip {
  position: absolute;
  top: 7px;
  left: 0;
  display: grid;
  place-items: center;
  height: 18px;
  padding: 0 4px;
  border-radius: 4px;
  background: var(--solid);
  color: var(--on-solid);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  transform: translateX(-50%);
}

/* The week: seven days, a cell each. */
.week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  height: 100%;
  padding: 8px;
}
.weekCell {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 8px 0;
  overflow: hidden;
  border: 1px solid var(--plan-line);
  border-radius: 6px;
  background: var(--card);
  transition: border-color 0.15s ease;
}
.weekCell:hover {
  border-color: var(--ink-4);
}
.weekCell[data-today] {
  border-color: var(--ink);
}
.weekCell[data-empty] {
  border-style: dashed;
  background: transparent;
}
.weekCount {
  font-size: 13px;
  font-weight: 600;
}
.weekHours {
  color: var(--ink-4);
  font-family: CareMono, ui-monospace, monospace;
  font-size: 11px;
}
.weekBar {
  position: absolute;
  right: 8px;
  bottom: 7px;
  left: 8px;
  height: 4px;
  border-radius: 2px;
  background: var(--done);
}
.weekBar::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--solid);
  transform: scaleX(var(--fill));
  transform-origin: 0 0;
  content: '';
}

.empty {
  padding: 24px 16px;
  color: var(--ink-3);
  font-size: 13px;
}

.foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 16px;
  min-height: 37px;
  padding: 0 7px 5px;
}
.footText,
.footButton {
  color: var(--ink-4);
  white-space: nowrap;
}
.footButton:hover {
  color: var(--ink);
}
.footHint {
  opacity: 0;
  transition: opacity 0.15s ease;
}
.footButton:hover .footHint,
.footButton:focus-visible .footHint,
.footButton[aria-expanded='true'] .footHint {
  opacity: 1;
}

/* ------------------------------------------------------------------ the popover */

.popover {
  position: absolute;
  z-index: 8;
  top: 0;
  left: 0;
  width: 343px;
  padding: 16px;
  border: 1px solid var(--card-line);
  border-radius: 14px;
  background: var(--card);
  box-shadow:
    0 16px 40px var(--shadow),
    0 2px 6px var(--shadow-small);
  animation: care-pop 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes care-pop {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}
.popHead {
  display: flex;
  align-items: center;
  gap: 12px;
}
.popAvatar {
  width: 40px;
  height: 40px;
  border-radius: 9px;
}
.popWho {
  flex: 1;
  min-width: 0;
}
.popName {
  overflow: hidden;
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 19px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.popCare {
  overflow: hidden;
  color: var(--ink-4);
  font-size: 12.5px;
  line-height: 17px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.pill {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 25px;
  padding: 0 10px 0 9px;
  border: 1px solid var(--card-line);
  border-radius: 13px;
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 500;
  white-space: nowrap;
}
.pillDot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--good-dot);
}
.pill[data-state='in-progress'] .pillDot {
  background: var(--ink);
}
.pill[data-state='confirmed'] .pillDot {
  background: var(--ink-5);
}
.pill[data-state='missed'] {
  color: var(--miss-ink);
}
.pill[data-state='missed'] .pillDot {
  background: var(--miss-ink);
}
.pill[data-state='at-risk'] .pillDot {
  background: var(--note);
}
.facts {
  margin-top: 14px;
  padding: 0 12px;
  border: 1px solid var(--sheet-line);
  border-radius: 10px;
  background: var(--soft);
}
.fact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  height: 33px;
  font-size: 13px;
}
.fact + .fact {
  border-top: 1px solid var(--sheet-line);
}
.fact dt {
  color: var(--ink-3);
}
.fact dd {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  overflow: hidden;
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
}
.factAvatar {
  width: 20px;
  height: 20px;
  border-radius: 50%;
}
.factCheck {
  width: 15px;
  height: 15px;
  color: var(--good);
}
.noteDot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--note);
}
.fact dd[data-muted] {
  color: var(--ink-3);
  font-weight: 400;
}
.popActions {
  display: flex;
  gap: 8px;
  margin-top: 14px;
}
.primary,
.secondary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 36px;
  border-radius: 9px;
  font-size: 13.5px;
  font-weight: 500;
  white-space: nowrap;
  transition: opacity 0.15s ease;
}
.primary {
  flex: 1;
  background: var(--solid);
  color: var(--on-solid);
}
.primary:hover {
  opacity: 0.86;
}
.primaryIcon {
  width: 15px;
  height: 15px;
}
.secondary {
  width: 102px;
  border: 1px solid var(--card-line);
  background: var(--card);
  color: var(--ink);
}
.secondary:hover {
  background: var(--control);
}
.secondaryIcon {
  width: 17px;
  height: 17px;
}

/* ------------------------------------------------------------------ the demo pointer */

.pointer {
  width: 22px;
  height: 22px;
}
.pointerHand {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .pointerArrow {
  visibility: hidden;
}
.pointer[data-glyph='hand'] .pointerHand {
  visibility: visible;
}

/* ------------------------------------------------------------------ reduced motion */

.root[data-reduced] .frame *,
.root[data-reduced] .frame *::before,
.root[data-reduced] .frame *::after {
  transition-duration: 0s !important;
  animation: none !important;
}

/* ------------------------------------------------------------------ narrow layout */

@container care (width < 880px) {
  .canvas {
    aspect-ratio: auto;
  }
  .frame {
    position: relative;
    display: block;
    width: 100%;
    height: auto;
    transform: none;
    border-radius: 14px;
  }
  .panel {
    margin: 0;
    border: 0;
    border-radius: 14px;
  }
  .toggle {
    display: none;
  }
  .menuButton {
    display: grid;
  }
  /* The sidebar as a drawer, over a scrim. */
  .sidebar {
    position: absolute;
    z-index: 10;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(280px, 86%);
    border-right: 1px solid var(--frame-line);
    background: var(--frame);
    transform: translateX(-102%);
    visibility: hidden;
    transition:
      transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1),
      visibility 0s linear 0.28s;
  }
  .frame[data-drawer] .sidebar {
    transform: none;
    visibility: visible;
    transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .scrim {
    position: absolute;
    z-index: 9;
    inset: 0;
    display: block;
    background: var(--scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.28s ease;
  }
  .frame[data-drawer] .scrim {
    opacity: 1;
    pointer-events: auto;
  }
  .navScroll {
    overflow: auto;
  }
  .topbar {
    padding: 0 12px 0 14px;
  }
  .head {
    flex-direction: column;
    padding: 20px 14px 0;
  }
  .subtitle {
    white-space: normal;
  }
  .controls {
    justify-content: flex-start;
    padding-top: 2px;
  }
  .board {
    margin: 18px 10px 10px;
  }
  .scroller {
    max-height: none;
  }
  .sheet {
    --who: 168px;
    min-width: 980px;
  }
  .who {
    padding-left: 12px;
  }
}
@container care (width < 560px) {
  .crumbs .crumb,
  .crumbs .crumbIcon {
    display: none;
  }
  .clock {
    margin-left: auto;
    padding: 0 10px;
  }
  .crumbs {
    flex: 0 1 auto;
  }
  .topbar {
    gap: 0;
  }
  .clockText {
    font-size: 10px;
  }
  .iconButton[data-inbox] {
    display: none;
  }
  .sheet {
    --who: 132px;
  }
  .headMore {
    display: none;
  }
  .popover {
    width: min(343px, calc(100% - 16px));
  }
  .popHead {
    flex-wrap: wrap;
  }
  .popWho {
    flex-basis: calc(100% - 52px);
  }
  .pill {
    margin-left: 52px;
  }
  .avatar {
    display: none;
  }
  .who {
    gap: 8px;
  }
  .coverIcon {
    display: none;
  }
  .legend {
    gap: 2px 12px;
  }
  .foot [data-end] {
    display: none;
  }
}
@container care (width < 400px) {
  .clock {
    display: none;
  }
  .crumbs {
    flex: 1;
  }
  .areaPicker,
  .newVisit {
    flex: 1;
  }
  .select {
    width: 100%;
  }
  .newVisit {
    justify-content: center;
  }
  .menu {
    right: auto;
    left: 0;
  }
}
```

### care-schedule/data.ts

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

/*
 * The invented agency behind the schedule: its caregivers, today's 48 visits and the clients they
 * go to, and how any other day of the week is made from today (each caregiver's day slides by up
 * to half an hour and loses a visit now and then, so the schedule changes but stays plausible).
 * Times are minutes after midnight; the day runs from 7 AM to 7 PM.
 */

export type AreaId = 'millbrook' | 'harbor' | 'elm' | 'fallow';
export const AREAS: { id: AreaId; label: string }[] = [
  { id: 'millbrook', label: 'Millbrook' },
  { id: 'harbor', label: 'Harbor Point' },
  { id: 'elm', label: 'Elm Ridge' },
  { id: 'fallow', label: 'West Fallow' },
];

/** How an illustrated avatar is drawn: skin, hair colour and cut, and the backdrop. */
export type Look = {
  skin: string;
  hair: string;
  cut: 'short' | 'bun' | 'long' | 'curly' | 'crop' | 'bald' | 'wavy';
  backdrop: string;
  shirt: string;
  beard?: boolean;
};

export type Caregiver = {
  id: string;
  name: string;
  role: 'CNA' | 'HHA' | 'PCA' | 'RN';
  look: Look;
  /** Called out today: the row says so and holds no visits. */
  out?: { reason: string };
};

export type Status = 'confirmed' | 'in-progress' | 'completed' | 'missed' | 'at-risk';

export type Visit = {
  id: string;
  caregiver: string;
  client: string;
  /** The name a block has room for: "Margit E.", or a first name. */
  short: string;
  start: number;
  end: number;
  care: string;
  area: AreaId;
  /** Minutes on the road from the visit before it, drawn as a dotted link. */
  travel?: number;
  /** Fixed outcomes; any other visit is confirmed, in progress or completed by the clock. */
  flag?: 'missed' | 'at-risk';
  note?: string;
  /** How far from the door the check-in was, in feet. */
  distance?: number;
  /** Moved here from the caregiver who called out. */
  reassigned?: boolean;
  look: Look;
};

const SKIN = ['#f1c7a5', '#e0a982', '#c58a62', '#9a6643', '#6e4630', '#f5d3bb'];
const HAIR = ['#2b201b', '#4a3226', '#7a5434', '#b98a52', '#d9d4cc', '#a8a39b', '#1b1715'];
const BACKDROP = ['#e6e0d6', '#dfe6e0', '#e3e0ea', '#ece2d8', '#dde4ea', '#e9e4d3'];
const SHIRT = ['#5b6b7a', '#8a6d5a', '#4f6b5c', '#7a5c74', '#3f4a5a', '#a07a4c', '#6a7d8f'];

const look = (
  skin: number,
  hair: number,
  cut: Look['cut'],
  backdrop: number,
  shirt: number,
  beard = false,
): Look => ({
  skin: SKIN[skin],
  hair: HAIR[hair],
  cut,
  backdrop: BACKDROP[backdrop],
  shirt: SHIRT[shirt],
  beard,
});

export const CAREGIVERS: Caregiver[] = [
  { id: 'ines', name: 'Inés Valderrama', role: 'CNA', look: look(1, 1, 'bun', 0, 0) },
  { id: 'adaeze', name: 'Adaeze Okafor', role: 'HHA', look: look(4, 6, 'curly', 1, 3) },
  { id: 'tomas', name: 'Tomás Ribeiro', role: 'CNA', look: look(2, 0, 'short', 2, 4, true) },
  { id: 'paloma', name: 'Paloma Ortega', role: 'CNA', look: look(1, 0, 'long', 3, 1) },
  { id: 'selam', name: 'Selam Tesfaye', role: 'HHA', look: look(3, 6, 'wavy', 4, 2) },
  { id: 'freya', name: 'Freya Lindholm', role: 'PCA', look: look(5, 3, 'bun', 5, 6) },
  { id: 'yuki', name: 'Yuki Tanabe', role: 'RN', look: look(0, 6, 'crop', 1, 0) },
  { id: 'karim', name: 'Karim Nasser', role: 'HHA', look: look(2, 1, 'short', 0, 5) },
  { id: 'siobhan', name: 'Siobhán Farrell', role: 'CNA', look: look(5, 2, 'wavy', 2, 2) },
  { id: 'anders', name: 'Anders Holmqvist', role: 'PCA', look: look(0, 4, 'bald', 3, 4, true) },
  { id: 'mireille', name: 'Mireille Dubois', role: 'CNA', look: look(1, 2, 'long', 4, 3) },
  { id: 'rafael', name: 'Rafael Duarte', role: 'HHA', look: look(2, 0, 'crop', 5, 0, true) },
  { id: 'hanne', name: 'Hanne Kjær', role: 'PCA', look: look(5, 3, 'short', 1, 6) },
  {
    id: 'kwame',
    name: 'Kwame Asante',
    role: 'HHA',
    look: look(4, 6, 'crop', 0, 1),
    out: { reason: 'Called out' },
  },
];

/** The caregivers listed until the reader asks for all of them (the rest come after). */
export const FIRST_ROWS = 10;

const CARE = [
  'Personal care',
  'Companionship',
  'Medication reminder',
  'Meal prep',
  'Respite care',
  'Skilled nursing',
  'Bathing assist',
  'Mobility support',
];

const t = (clock: string) => {
  const [hours, minutes] = clock.split(':').map(Number);
  return hours * 60 + minutes;
};

type Row = [
  client: string,
  short: string,
  start: string,
  end: string,
  area: AreaId,
  extra?: Partial<Pick<Visit, 'travel' | 'flag' | 'note' | 'reassigned' | 'distance'>>,
];

const DAY: Record<string, Row[]> = {
  ines: [
    ['Margit Ellery', 'Margit E.', '7:30', '9:00', 'millbrook'],
    [
      'Henrietta Vance',
      'Henrietta V.',
      '10:00',
      '12:00',
      'millbrook',
      { travel: 8, note: 'Follow-up to review', distance: 38 },
    ],
    ['Osric Penhallow', 'Osric', '12:30', '13:30', 'elm'],
    [
      'Leonora Fitch',
      'Leonora F.',
      '14:30',
      '16:00',
      'millbrook',
      { travel: 18, reassigned: true },
    ],
    ['Bram Pellow', 'Bram P.', '16:30', '18:00', 'harbor', { travel: 15 }],
  ],
  adaeze: [
    ['Cosmo Rendell', 'Cosmo R.', '7:00', '8:30', 'fallow'],
    ['Agathe Plaskett', 'Agathe P.', '15:30', '17:00', 'fallow'],
    ['Rosamund Keel', 'Rosamund K.', '17:30', '19:00', 'elm', { travel: 12 }],
  ],
  tomas: [
    ['Fenella Upward', 'Fenella U.', '7:00', '8:30', 'harbor'],
    ['Lucian Varga', 'Lucian V.', '9:00', '10:30', 'harbor', { travel: 24 }],
    ['Ambrose Wexley', 'Ambrose W.', '11:00', '12:45', 'millbrook', { travel: 15 }],
    [
      'Ottilia Brandt',
      'Ottilia',
      '13:45',
      '15:00',
      'fallow',
      { flag: 'at-risk', note: 'Caregiver not en route' },
    ],
    ['Delphine Yarrow', 'Delphine Y.', '16:00', '17:30', 'fallow'],
  ],
  paloma: [
    ['Edwin Tolley', 'Edwin T.', '8:00', '9:30', 'elm'],
    ['Gisela Sorrel', 'Gisela S.', '10:00', '11:30', 'elm', { travel: 14 }],
    ['Odalys Crane', 'Odalys', '12:00', '13:30', 'elm', { travel: 12 }],
    ['Imogen Oakes', 'Imogen O.', '14:30', '16:00', 'fallow'],
  ],
  selam: [
    ['Ivo Stannard', 'Ivo', '7:00', '8:00', 'harbor'],
    ['Josefa Vint', 'Josefa V.', '8:30', '10:00', 'harbor', { travel: 13 }],
    ['Linus Barrow', 'Linus B.', '13:30', '15:00', 'millbrook'],
    ['Solveig Greer', 'Solveig G.', '16:00', '17:30', 'fallow'],
  ],
  freya: [
    ['Wren Tallis', 'Wren T.', '9:00', '10:30', 'elm'],
    ['Percy Dunmore', 'Percy D.', '15:30', '17:00', 'millbrook'],
    ['Rhea Colvin', 'Rhea C.', '17:30', '19:00', 'millbrook', { travel: 9 }],
  ],
  yuki: [
    ['Abel Marsh', 'Abel', '9:30', '10:30', 'fallow'],
    ['Greer Hollins', 'Greer', '11:00', '12:00', 'fallow', { travel: 17 }],
    ['Tobias Wynn', 'Tobias', '13:00', '14:00', 'harbor'],
    ['Nell Ashby', 'Nell', '14:30', '15:30', 'harbor', { travel: 12 }],
    ['Maren Quill', 'Maren', '16:00', '17:00', 'elm', { travel: 10 }],
  ],
  karim: [
    ['Bettina Lowe', 'Bettina L.', '7:00', '8:30', 'millbrook'],
    ['Viggo Carrow', 'Viggo C.', '9:30', '11:00', 'fallow'],
    ['Ada Merriman', 'Ada', '11:30', '12:30', 'fallow', { travel: 15 }],
    ['Waldo Dace', 'Waldo D.', '13:00', '15:00', 'harbor', { travel: 11, distance: 22 }],
    ['Lorne Nettle', 'Lorne N.', '15:30', '17:00', 'millbrook', { reassigned: true }],
  ],
  siobhan: [
    ['Dagny Lisle', 'Dagny L.', '7:00', '8:30', 'elm'],
    ['Rupert Ames', 'Rupert A.', '9:00', '11:00', 'elm', { travel: 19 }],
    [
      'Mirela Pask',
      'Mirela P.',
      '11:30',
      '13:00',
      'fallow',
      { travel: 12, flag: 'missed', note: 'Client not home · call family' },
    ],
    ['Cassius Hale', 'Cassius H.', '13:30', '15:30', 'millbrook', { travel: 16, distance: 41 }],
    ['Esme Tarrant', 'Esme T.', '16:00', '17:30', 'millbrook', { travel: 6, reassigned: true }],
  ],
  anders: [
    ['Winnifred Toll', 'Winnifred T.', '7:00', '8:30', 'harbor'],
    ['Petronella Sax', 'Petronella S.', '14:40', '16:10', 'fallow'],
    ['Anselm Rook', 'Anselm', '18:00', '19:00', 'millbrook'],
  ],
  mireille: [
    ['Corin Mayhew', 'Corin M.', '8:00', '9:30', 'harbor'],
    ['Tamsin Reed', 'Tamsin R.', '14:00', '15:30', 'harbor'],
  ],
  rafael: [
    ['Ezra Bellamy', 'Ezra B.', '9:00', '10:00', 'fallow'],
    ['Lavinia Knoll', 'Lavinia K.', '15:00', '16:30', 'millbrook'],
  ],
  hanne: [
    ['Hugo Farrant', 'Hugo F.', '10:00', '11:30', 'millbrook'],
    ['Isolde Wray', 'Isolde W.', '16:00', '17:00', 'harbor'],
  ],
  kwame: [],
};

/** A small, stable hash: the same text always gives the same number. */
export function hash(text: string) {
  let value = 2166136261;
  for (let index = 0; index < text.length; index += 1) {
    value ^= text.charCodeAt(index);
    value = Math.imul(value, 16777619);
  }
  return value >>> 0;
}

const clientLook = (name: string): Look => {
  const h = hash(name);
  const cuts: Look['cut'][] = ['short', 'bun', 'wavy', 'crop', 'bald', 'curly'];
  return {
    skin: SKIN[h % SKIN.length],
    // Mostly grey and white: the clients are older.
    hair: [HAIR[4], HAIR[5], HAIR[4], HAIR[3]][(h >>> 3) % 4],
    cut: cuts[(h >>> 5) % cuts.length],
    backdrop: BACKDROP[(h >>> 7) % BACKDROP.length],
    shirt: SHIRT[(h >>> 9) % SHIRT.length],
    beard: (h >>> 11) % 5 === 0,
  };
};

/** Today's visits. */
export const TODAY: Visit[] = Object.entries(DAY).flatMap(([caregiver, rows]) =>
  rows.map(([client, short, start, end, area, extra], index) => ({
    id: `${caregiver}-${index}`,
    caregiver,
    client,
    short,
    start: t(start),
    end: t(end),
    care: CARE[hash(client) % CARE.length],
    area,
    look: clientLook(client),
    ...extra,
    ...(client === 'Henrietta Vance' ? { care: 'Personal care' } : null),
  })),
);

/** The day in the reference: Monday, October 5, at 1:38 PM. */
export const TODAY_DATE = new Date(2026, 9, 5);
export const START_NOW = t('13:38');
export const DAY_START = t('7:00');
export const DAY_END = t('19:00');

/** When the caregiver who called out today usually sees the three clients who went to others. */
const KWAME: Record<string, [string, string]> = {
  'Leonora Fitch': ['9:00', '10:30'],
  'Lorne Nettle': ['11:00', '12:30'],
  'Esme Tarrant': ['14:00', '15:30'],
};

/**
 * Another day's visits, `offset` days from today: each caregiver's day slides by -30, 0 or +30
 * minutes, a visit now and then is dropped, weekends are lighter, and the caregiver who called
 * out today is back with the three visits that went to others.
 */
export function visitsFor(offset: number): Visit[] {
  if (offset === 0) return TODAY;
  const weekday = (TODAY_DATE.getDay() + offset + 700) % 7;
  const weekend = weekday === 0 || weekday === 6;
  return TODAY.flatMap((visit) => {
    const h = hash(`${offset}:${visit.id}`);
    const caregiver = visit.reassigned ? 'kwame' : visit.caregiver;
    // The whole day slides together, and only as far as it still fits between 7 AM and 7 PM.
    const day = TODAY.filter((other) => other.caregiver === visit.caregiver);
    let slide = ((hash(`${offset}:${caregiver}`) % 3) - 1) * 30;
    if (day.some((other) => other.start + slide < DAY_START || other.end + slide > DAY_END))
      slide = 0;
    // His own three visits, back with him at his usual hours.
    const usual = visit.reassigned ? KWAME[visit.client] : undefined;
    const start = usual ? t(usual[0]) : visit.start + slide;
    const end = usual ? t(usual[1]) : visit.end + slide;
    if (h % (weekend ? 2 : 7) === 0) return [];
    const flag: Visit['flag'] =
      offset < 0 && h % 17 === 1 ? 'missed' : offset === 1 && h % 13 === 2 ? 'at-risk' : undefined;
    return [
      {
        ...visit,
        id: `${visit.id}@${offset}`,
        caregiver,
        start,
        end,
        flag,
        reassigned: false,
        note: flag === 'missed' ? 'Client not home · call family' : undefined,
      },
    ];
  });
}

/** Travel minutes between two neighbouring visits, for links the reader makes by dragging. */
export function travelBetween(a: Visit, b: Visit) {
  return 5 + (hash(`${a.client}>${b.client}`) % 21);
}
```

### care-schedule/avatar.tsx

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

import type { Look } from './data';

/*
 * An illustrated portrait, never a photo: a backdrop, shoulders, a face and a haircut, from the
 * person's `look`. Drawn on a 40-unit grid; the stylesheet sets its size and its round or
 * rounded-square crop. Decorative: the name always stands next to it.
 */

const HAIR: Record<Look['cut'], { back?: string; front: string }> = {
  short: {
    front:
      'M11.5 17.5c-.6-6.4 3.5-9.8 8.5-9.8s9.2 3.4 8.5 9.8c-1.6-2.9-4.2-4.6-8.5-4.6-4.2 0-6.9 1.7-8.5 4.6z',
  },
  crop: { front: 'M12 16.5c0-5.3 3.4-8 8-8s8 2.7 8 8c-2.2-1.8-4.8-2.5-8-2.5s-5.8.7-8 2.5z' },
  bun: {
    back: 'M20 3.6a3.6 3.6 0 1 1 0 7.2 3.6 3.6 0 0 1 0-7.2z',
    front:
      'M11.4 19c-.9-6.8 3.2-10.6 8.6-10.6s9.5 3.8 8.6 10.6c-1.1-3.6-3.9-6.2-8.6-6.2s-7.5 2.6-8.6 6.2z',
  },
  long: {
    back: 'M10.4 30c-1.6-4.6-1.9-10.4-.2-14.6C12 10.9 15.6 8.4 20 8.4s8 2.5 9.8 7c1.7 4.2 1.4 10-.2 14.6z',
    front:
      'M11.6 19.5c.2-6.4 3.7-10 8.4-10s8.2 3.6 8.4 10c-2.4-2.8-4.7-5-8.4-5.6-1.1 2.4-4.6 4.6-8.4 5.6z',
  },
  wavy: {
    back: 'M10.6 26c-2.2-3.4-2.3-8.6-.6-12 2-4 5.6-6 10-6s8 2 10 6c1.7 3.4 1.6 8.6-.6 12z',
    front:
      'M11.4 19c-.4-6.4 3.6-10.2 8.6-10.2s9 3.8 8.6 10.2c-1.2-2.6-2.6-4.4-4.6-5.4-1.8 1.6-5.8 3.6-12.6 5.4z',
  },
  curly: {
    back: 'M9.4 21.5c-2.6-2-2.2-6.4.3-7.8-.3-3.2 2.4-5.8 5.5-5.4 1.4-2.4 6.2-2.6 7.9-.4 3.1-.6 5.9 1.9 5.6 5.1 2.6 1.4 2.9 5.7.3 7.8z',
    front: 'M11.6 17.4c1.4-2.9 4.6-4.4 8.4-4.4s7 1.5 8.4 4.4c.5-4.4-3.2-8-8.4-8s-8.9 3.6-8.4 8z',
  },
  bald: { front: 'M12.2 15.6c1.2-1.6 2.2-2.6 2.8-2.6M27.8 15.6c-1.2-1.6-2.2-2.6-2.8-2.6' },
};

export function Avatar({ look, className }: { look: Look; className?: string }) {
  const hair = HAIR[look.cut];
  return (
    <svg className={className} viewBox="0 0 40 40" aria-hidden="true" focusable="false">
      <rect width="40" height="40" fill={look.backdrop} />
      {hair.back ? <path d={hair.back} fill={look.hair} /> : null}
      <path d="M5 42c.6-7.6 6.6-11.6 15-11.6S34.4 34.4 35 42z" fill={look.shirt} />
      <path d="M16.6 26.2h6.8v5.2c-1 1.4-2.1 2-3.4 2s-2.4-.6-3.4-2z" fill={look.skin} />
      <path d="M16.6 28.6c2.2 1 4.6 1 6.8 0v-2.4h-6.8z" fill="#000" opacity="0.12" />
      <ellipse cx="20" cy="19.2" rx="7.6" ry="8.6" fill={look.skin} />
      <ellipse cx="12.4" cy="20" rx="1.3" ry="1.9" fill={look.skin} />
      <ellipse cx="27.6" cy="20" rx="1.3" ry="1.9" fill={look.skin} />
      {look.beard ? (
        <path
          d="M12.8 20.6c.4 5.2 3.4 7.4 7.2 7.4s6.8-2.2 7.2-7.4c-1.6 2.6-4 3.2-7.2 3.2s-5.6-.6-7.2-3.2z"
          fill={look.hair}
        />
      ) : null}
      {look.cut === 'bald' ? (
        <path
          d={hair.front}
          fill="none"
          stroke={look.hair}
          strokeWidth="2.2"
          strokeLinecap="round"
        />
      ) : (
        <path d={hair.front} fill={look.hair} />
      )}
      <circle cx="17.1" cy="19.6" r="0.95" fill="#2a211c" />
      <circle cx="22.9" cy="19.6" r="0.95" fill="#2a211c" />
      <path
        d="M18 23.6c1.2.8 2.8.8 4 0"
        fill="none"
        stroke="#7a4a3a"
        strokeWidth="0.9"
        strokeLinecap="round"
        opacity="0.75"
      />
    </svg>
  );
}
```

### care-schedule/icons.tsx

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

import type { ReactNode } from 'react';

/*
 * The schedule's icons: line drawings on a 20-unit grid (framed to its middle 15 units) in the current colour, sized by the
 * stylesheet (the lab forces unsized SVGs to 16 px). All of them are decorative: the control
 * that holds one carries the name.
 */

const paths: Record<string, ReactNode> = {
  switch: (
    <>
      <path d="M7 15V5M4.5 7.5 7 5l2.5 2.5" />
      <path d="M13 5v10m-2.5-2.5L13 15l2.5-2.5" />
    </>
  ),
  search: (
    <>
      <circle cx="9" cy="9" r="5.25" />
      <path d="m13 13 3.5 3.5" />
    </>
  ),
  dashboard: (
    <>
      <rect x="3.5" y="3.5" width="5.25" height="5.25" rx="1.4" />
      <rect x="11.25" y="3.5" width="5.25" height="5.25" rx="1.4" />
      <rect x="3.5" y="11.25" width="5.25" height="5.25" rx="1.4" />
      <rect x="11.25" y="11.25" width="5.25" height="5.25" rx="1.4" />
    </>
  ),
  calendar: (
    <>
      <rect x="3.25" y="4.25" width="13.5" height="12.5" rx="2.5" />
      <path d="M3.25 8.25h13.5M7 2.75v3M13 2.75v3" />
      <path
        d="M6.75 11.25h.01M10 11.25h.01M13.25 11.25h.01M6.75 14h.01M10 14h.01"
        strokeWidth="1.5"
      />
    </>
  ),
  calendarSmall: (
    <>
      <rect x="3.5" y="4" width="13" height="12.5" rx="2.6" />
      <path d="M3.5 8h13M7 11.25h6M7 13.75h3.5" />
    </>
  ),
  visits: (
    <>
      <path d="M7 3.75H5.75a1.5 1.5 0 0 0-1.5 1.5v10.5a1.5 1.5 0 0 0 1.5 1.5h4.5" />
      <path d="M13 3.75h1.25a1.5 1.5 0 0 1 1.5 1.5V10" />
      <rect x="7" y="2.5" width="6" height="2.75" rx="1" />
      <path d="M7.25 9h5.5M7.25 12h3" />
      <path d="m12.25 15.25 1.6 1.6 3.15-3.35" />
    </>
  ),
  bell: (
    <>
      <path d="M5.25 13.75V9a4.75 4.75 0 0 1 9.5 0v4.75l1.25 1.5H4z" />
      <path d="M8.25 17.25a2 2 0 0 0 3.5 0" />
    </>
  ),
  caregivers: (
    <>
      <circle cx="8" cy="7" r="2.75" />
      <path d="M3.25 16c.4-2.6 2.3-4.25 4.75-4.25s4.35 1.65 4.75 4.25z" />
      <path d="M12.5 4.5a2.6 2.6 0 0 1 0 5M14.25 11.9c1.4.6 2.3 2 2.5 4.1h-2" />
    </>
  ),
  clients: (
    <>
      <path d="M3.75 9.25 10 3.75l6.25 5.5" />
      <path d="M5.25 8v7.25a1.25 1.25 0 0 0 1.25 1.25h7a1.25 1.25 0 0 0 1.25-1.25V8" />
    </>
  ),
  shield: (
    <>
      <path d="M10 2.75 15.75 5v4.6c0 3.4-2.3 6.1-5.75 7.65C6.55 15.7 4.25 13 4.25 9.6V5z" />
      <path d="m7.5 10 1.75 1.75 3.25-3.5" />
    </>
  ),
  reports: (
    <>
      <rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5" />
      <path d="M7 13.25V10M10 13.25v-6.5M13 13.25V9" />
    </>
  ),
  plus: <path d="M10 4v12M4 10h12" />,
  help: (
    <>
      <circle cx="10" cy="10" r="7" />
      <path d="M7.9 8a2.15 2.15 0 0 1 4.15.75c0 1.4-2.05 1.75-2.05 3" />
      <path d="M10 14.1h.01" strokeWidth="1.6" />
    </>
  ),
  settings: (
    <>
      <circle cx="10" cy="10" r="2.4" />
      <path d="M10 2.75 11.3 4.6l2.2-.5.6 2.2 2.1.95-.75 2.15 1.3 1.85-1.85 1.3.15 2.25-2.25.15-1.3 1.85L10 16.15l-2.15.75-1.3-1.85-2.25-.15.15-2.25L2.6 11.35l1.3-1.85-.75-2.15 2.1-.95.6-2.2 2.2.5z" />
    </>
  ),
  sidebar: (
    <>
      <rect x="2.75" y="3.75" width="14.5" height="12.5" rx="2.5" />
      <path d="M7.75 3.75v12.5M4.75 7h1M4.75 9.5h1" />
    </>
  ),
  chevronRight: <path d="m8 5 5 5-5 5" />,
  chevronLeft: <path d="m12 5-5 5 5 5" />,
  chevronDown: <path d="m5.5 8 4.5 4.5L14.5 8" />,
  inbox: (
    <>
      <rect x="3.25" y="3.75" width="13.5" height="12.5" rx="2.75" />
      <path d="M3.25 11h3.5l1 1.75h4.5l1-1.75h3.5" />
    </>
  ),
  pin: (
    <>
      <path d="M10 17.25s5.25-4.6 5.25-9a5.25 5.25 0 0 0-10.5 0c0 4.4 5.25 9 5.25 9z" />
      <circle cx="10" cy="8.25" r="1.9" />
    </>
  ),
  check: <path d="m4.75 10.25 3.25 3.25 7.25-7.5" />,
  checkCircle: (
    <>
      <circle cx="10" cy="10" r="7" />
      <path d="m7.1 10.1 2 2 3.8-4" />
    </>
  ),
  info: (
    <>
      <circle cx="10" cy="10" r="7" />
      <path d="M10 9.25v4" />
      <path d="M10 6.6h.01" strokeWidth="1.6" />
    </>
  ),
  message: (
    <>
      <path d="M10 3.5a6.5 6.5 0 0 1 0 13c-1.15 0-2.25-.3-3.2-.85L3.5 16.5l.85-3.2A6.5 6.5 0 0 1 10 3.5z" />
      <path d="M7.25 9h5.5M7.25 11.75h3.5" />
    </>
  ),
  menu: <path d="M3.5 6h13M3.5 10h13M3.5 14h13" />,
  close: <path d="m5 5 10 10M15 5 5 15" />,
};

export type IconName = keyof typeof paths;

export function Icon({ name, className }: { name: IconName; className?: string }) {
  return (
    <svg
      className={className}
      viewBox="2.5 2.5 15 15"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.15"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      focusable="false"
    >
      {paths[name]}
    </svg>
  );
}

/** The agency's mark: a roof over a door, cut out of a rounded square by the stylesheet. */
export function Mark({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 32 32" aria-hidden="true" focusable="false">
      <rect width="32" height="32" rx="8" fill="var(--mark)" />
      <path
        d="M9 15.5 16 9.5l7 6"
        fill="none"
        stroke="var(--on-mark)"
        strokeWidth="2.6"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
      <circle cx="16" cy="19.5" r="2.6" fill="var(--on-mark)" />
    </svg>
  );
}
```

### care-schedule/demo-pointer.tsx

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

'use client';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### care-schedule/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. `visible` and `reduced` are handed out too, for
 * the clock (it stops off screen) and the transitions.
 */
export function useAutoplay<T extends HTMLElement>(idle = 6000) {
  const ref = useRef<T>(null);
  const [visible, setVisible] = useState(false);
  const [reduced, setReduced] = useState(true);
  const [taken, setTaken] = useState(false);
  const timer = useRef(0);

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

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

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