# App collection

> Four phone screens in a 2 × 2 grid, each with its own entrance: a step ring, a streak card and two assistant orbs.

A collection of four app screens on phones, cropped by tinted tiles. A step tracker sweeps its ring round while the count is written digit by digit; a streak card lights its second day; a purple glass assistant grows in and settles over its pricing page; a dark frosted assistant gathers its lights and opens its eyes. Each screen idles once in, the orbs look towards the pointer, a tap on a phone plays it again and a tap on an orb makes it blink. Light and dark stage.

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

Category: Mobile · Contents.

## Anatomy

1. **Stage**: The quiet surface round the grid: a light-dark() fill that follows the page or colorScheme, and the container its two-column layout answers to.
2. **Tile**: A tinted cell drawn at 540 × 688 and scaled as a whole to its column; the whole tile is a button that replays its phone.
3. **Phone**: A CSS-drawn phone round a 390 × 844-point screen, at 1.08 times, cropped by its tile at the height it is shot at.
4. **Step count**: Written digit by digit (1, 9, the comma, 5, 3, 8) inside a half ring that sweeps to 80.5% in 360 ms with its needle.
5. **Lit day**: One of three weeks of day cells on a 45-point pitch: the second day, whose grey flame crossfades to a lit one with a small pop.
6. **Purple orb**: A rendered violet glass sphere with internal ribbons and a glossy dark face; it grows in, floats over its shadow, looks at the pointer and blinks when tapped.
7. **Pricing plan**: The last line of the purple screen to rise: the plan, its price and a radio mark.
8. **Dark orb**: A rendered smoked-glass shell with violet and warm reflections; it gathers three sparks, gently turns and opens its live eyes from dots into pills.

## When to use

- A showcase of a few app screens side by side: a case study, a portfolio section or a launch page that says “here is what the app looks like”.
- Screens that are worth seeing move: each phone plays its own entrance, so a still shot would lose what makes them good.
- A fixed set of two to four screens that read at a glance, each on its own tinted tile, cropped the way a photographed phone is.
- A hero or section that should feel alive without asking anything: it plays on its own, and a tap only replays a screen.

## When not to use

- A working prototype people should click through. Link to the real build or an interactive prototype instead.
- More than four screens, or screens that change often. Use a scrolling gallery of static screenshots.
- Explaining a flow step by step. Use a stepper or a sequence of annotated screens, where the order is the point.
- Showing one screen in detail. Use a single large device frame, with room for callouts around it.
- Pages that must stay still (print, slides exported as images). Use stills of the final frames instead.

## Installation

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

```sh
npx shadcn@latest add https://brunodelorence.com/r/app-collection.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.

Copy these media files to the same paths under your public folder:

- https://brunodelorence.com/experiments/app-collection/purple-orb-v2.webp
- https://brunodelorence.com/experiments/app-collection/dark-orb-v2.webp
- https://brunodelorence.com/experiments/app-collection/streak-badge-v2.webp
- https://brunodelorence.com/experiments/app-collection/tracker-gold-v2.webp
- https://brunodelorence.com/experiments/app-collection/tracker-silver-v2.webp
- https://brunodelorence.com/experiments/app-collection/streak-fire-v2.webp
- https://brunodelorence.com/experiments/app-collection/streak-grey-v2.webp
- https://brunodelorence.com/experiments/app-collection/iridescent-star-v2.webp
- https://brunodelorence.com/experiments/app-collection/purple-satellites-v2.webp

## Usage

```tsx
import { AppCollection } from './app-collection';

export function Showcase() {
  return <AppCollection autoplay={false} colorScheme="system" />;
}
```

## States

- **At rest**: Every screen in its final frame: what the entrances end on, and all that is drawn under reduced motion.
- **Entrance, 0.3 s**: The ring starts its sweep, the second day is still grey, the purple orb is half grown and the sparks fly in.
- **Entrance, 0.7 s**: The count is half written, the chip and headline rise, the lights gather round the dark orb with its eyes still shut.
- **Looking at the pointer**: Both orbs lean their faces towards the pointer, here up and to the left: `gaze` of -1, -1.
- **Blink**: Half way through the blink a tap on an orb sets off: the eyes squashed to a tenth of their height.
- **Dark stage**: `colorScheme="dark"`: the stage and tiles turn dark; the four screens keep their own designs.
- **One column**: Under 600 px of its own width the tiles stack, up to 420 px wide (the top two shown).

## How it works

- Each tile is drawn at 540 × 688 px and scaled as a whole to its column (a ResizeObserver works out the factor). Inside it a phone, drawn in CSS around a 390 × 844-point screen, sits at about 1.08 times and is cropped by the tile, each at the height it is shot at. Two columns from 600 px wide, one below.
- Every entrance is CSS keyframes with fill-mode both and delays timed frame by frame, and every part rests in its final state. A screen plays again when it is remounted with a new key: a tap on its tile does it, and so does the autoplay, each phone on its own period (6.2 to 7.6 s), staggered.
- The step ring is one SVG annulus with rounded ends, filled through a mask whose stroke sweeps 80.5% of a half circle in 360 ms while the needle turns with it; its glow is a blurred copy under the same mask. The count is six spans that rise and darken one after another (1, 9, the comma, 5, 3, 8), then the pills and the goal line follow.
- The streak card crossfades its second day between two transparent 3D flame renders with a small pop. A rendered iridescent glass hexagon sways behind live blinking eyes, and the button’s rendered star turns.
- The purple orb is a transparent raster render of violet glass, internal ribbons and a glossy black face. Its two glowing eyes remain live layers, so gaze and blinking still work. It grows in with squash and stretch, floats over a breathing shadow, and is surrounded by rendered glass miniatures; the pricing page rises line by line.
- The dark orb is a transparent render of an irregular smoked-glass shell with a glossy black center and violet and warm reflections. It gently turns and compresses at rest. Three sparks fly into it as it grows, and its live eyes open from dots into pills.
- Both orbs and their eyes lean towards the pointer (up to 9 points for the eyes), set as custom properties outside React; both blink now and then, and a tap on one makes it blink at once.
- The stage and the tiles are light-dark() tokens; the four screens keep their own designs in both schemes. The loops pause off screen or in a hidden tab, and under reduced motion every screen is drawn at rest.

## API reference

- **autoplay** (`boolean`, default `true`): While on screen and untouched, each phone plays its entrance again on its own period. Any tap hands control to the reader for six seconds. Either way, every screen plays once when the collection first comes into view.
- **colorScheme** (`'light' | 'dark' | 'system'`): Light or dark stage and tiles, or the reader’s; without it, the page’s color-scheme. The screens keep their own colours.
- **defaultState** (`{ at?: number; gaze?: { x: number; y: number }; blink?: boolean }`): Opens held still, with the autoplay off: every animation paused the given milliseconds (at) into its entrance, or at rest without it; both orbs looking towards gaze (-1 to 1 on each axis); and, with blink, their eyes half closed. A tap on a phone plays it live again.
- **className** (`string`): Added to the stage.

## Accessibility

- The four phones are a labelled list; each screen’s text is real text (the step count is read as “19,538 steps”, each weekday says whether its goal was reached), while status bars, orbs, icons and art are hidden from assistive technology.
- Each tile is a button named “Replay the step tracker” and so on, and each orb is a button (“Poke the purple assistant”); all show a focus ring and work with Enter and Space. What happens is announced in a polite live region.
- Taps use plain clicks, so mouse, touch and keyboard behave alike; the orbs’ gaze follows a mouse or pen and rests when the pointer leaves.
- Under reduced motion nothing animates: every screen is drawn in its final state and the autoplay stays off.

## Source

### app-collection/app-collection.tsx

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

'use client';

import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
  type PointerEvent,
} from 'react';
import { DarkScreen, PurpleScreen, StreakScreen, TrackerScreen } from './screens';
import { useAutoplay } from './use-autoplay';
import styles from './app-collection.module.css';

/*
 * App collection: four phones in a 2 × 2 grid of tinted tiles, each showing one app screen with
 * its own entrance: a step tracker whose ring sweeps round while its count is written digit by
 * digit, a streak card whose second day lights up, a purple glass assistant that grows in and
 * settles with its pricing page rising under it, and a dark frosted assistant that gathers its
 * lights and opens its eyes.
 *
 * - Each tile is drawn at 540 × 688 px and scaled as a whole to its column (a ResizeObserver works
 *   out the factor); the phones inside are 390 × 844-point screens at 1.08 times, cropped by their
 *   tile the way a shot of a phone is. Two columns from 600 px wide, one below.
 * - The entrances are CSS keyframes with delays timed frame by frame at 30 fps. A
 *   screen plays its entrance again when it is remounted (a new `key`): tapping a phone does it,
 *   and so does the autoplay, each phone on its own period, while the collection is on screen.
 * - Once in, every screen idles: the ring's glow breathes and the lit days flicker, the star on
 *   the button turns, the glass orb floats over its shadow among drifting icons, the frosted orb
 *   wobbles with its lights wheeling round its rim; both orbs blink now and then.
 * - The orbs look at the pointer: their faces lean up to 7 points towards it (one transform
 *   written outside React). Tapping an orb makes it blink.
 * - Light or dark: the stage and tiles are light-dark() tokens (the colorScheme prop, or the
 *   page's color-scheme); the screens keep their own designs. Off screen or in a hidden tab the
 *   loops pause; under reduced motion every screen is drawn at rest.
 * - defaultState opens it held still (for docs and stills): every animation paused at one moment
 *   of the entrance (or at rest), the orbs looking one way, or mid-blink; the autoplay stays off.
 *   A tap on a phone plays it live again.
 */

/** A still to open on: the screens held at one moment, with no autoplay. */
export type AppCollectionState = {
  /** Milliseconds into every entrance to hold the screens at; left out, they are held at rest. */
  at?: number;
  /** Where both orbs look, -1 to 1 on each axis (x to the right, y down). */
  gaze?: { x: number; y: number };
  /** Holds both orbs' eyes half way through a blink. */
  blink?: boolean;
};

const TILE_W = 540;

type Key = 'tracker' | 'streak' | 'purple' | 'dark';
const PHONES: { key: Key; name: string; period: number; offset: number }[] = [
  { key: 'tracker', name: 'step tracker', period: 7000, offset: 0 },
  { key: 'streak', name: 'streak card', period: 6200, offset: 900 },
  { key: 'purple', name: 'purple assistant', period: 7600, offset: 1800 },
  { key: 'dark', name: 'dark assistant', period: 6600, offset: 2700 },
];

export function AppCollection({
  className,
  autoplay = true,
  colorScheme,
  defaultState,
}: {
  className?: string;
  autoplay?: boolean;
  colorScheme?: 'light' | 'dark' | 'system';
  /** Opens held still in this state, with the autoplay off. */
  defaultState?: AppCollectionState;
}) {
  const [held] = useState(defaultState);
  const entering = held?.at !== undefined ? 1 : 0;
  const { ref: root, take, playing, visible, reduced } = useAutoplay<HTMLDivElement>(6000);
  // How many times each screen has played its entrance: 0 draws it at rest.
  const [runs, setRuns] = useState<Record<Key, number>>({
    tracker: entering,
    streak: entering,
    purple: entering,
    dark: entering,
  });
  const [blinks, setBlinks] = useState(() =>
    held?.blink ? { purple: 1, dark: 1 } : { purple: 0, dark: 0 },
  );
  const [said, setSaid] = useState('');
  const started = useRef(!!held);
  const purpleOrb = useRef<HTMLSpanElement>(null);
  const darkOrb = useRef<HTMLSpanElement>(null);

  const replay = useCallback((key: Key) => {
    setRuns((current) => ({ ...current, [key]: current[key] + 1 }));
  }, []);

  // The first time the collection comes on screen, every phone plays its entrance.
  useEffect(() => {
    if (!visible || reduced || started.current) return;
    started.current = true;
    setRuns({ tracker: 1, streak: 1, purple: 1, dark: 1 });
  }, [visible, reduced]);

  // A held state: every animation paused at its moment, and the orbs turned to their gaze.
  useLayoutEffect(() => {
    const element = root.current;
    if (!held || !element) return;
    for (const animation of element.getAnimations({ subtree: true })) {
      animation.pause();
      const target = (animation.effect as KeyframeEffect | null)?.target;
      const blinking =
        held.blink && target instanceof Element && target.parentElement?.closest('[data-blink]');
      animation.currentTime = blinking ? 150 : (held.at ?? 0);
    }
    for (const orb of [purpleOrb.current, darkOrb.current]) {
      orb?.style.setProperty('--gx', String(held.gaze?.x ?? 0));
      orb?.style.setProperty('--gy', String(held.gaze?.y ?? 0));
    }
  }, [held, root]);

  // The autoplay: each phone plays again on its own period, staggered.
  useEffect(() => {
    if (!autoplay || held || !playing) return;
    const timers: number[] = [];
    const intervals: number[] = [];
    for (const phone of PHONES) {
      timers.push(
        window.setTimeout(
          () => {
            replay(phone.key);
            intervals.push(window.setInterval(() => replay(phone.key), phone.period));
          },
          phone.period - 2600 + phone.offset,
        ),
      );
    }
    return () => {
      timers.forEach(window.clearTimeout);
      intervals.forEach(window.clearInterval);
    };
  }, [autoplay, held, playing, replay]);

  // The tiles' scale, from the width of one tile.
  useEffect(() => {
    const element = root.current;
    const tile = element?.querySelector<HTMLElement>('[data-part="tile"]');
    if (!element || !tile) return;
    const observer = new ResizeObserver(() => {
      element.style.setProperty('--k', String(tile.clientWidth / TILE_W));
    });
    observer.observe(tile);
    return () => observer.disconnect();
  }, [root]);

  // The orbs lean towards the pointer.
  const look = (event: PointerEvent<HTMLDivElement>) => {
    if (reduced) return;
    for (const orb of [purpleOrb.current, darkOrb.current]) {
      if (!orb) continue;
      const box = orb.getBoundingClientRect();
      const dx = (event.clientX - (box.left + box.width / 2)) / (box.width * 2.2);
      const dy = (event.clientY - (box.top + box.height / 2)) / (box.height * 2.2);
      const clamp = (v: number) => Math.max(-1, Math.min(1, v));
      orb.style.setProperty('--gx', clamp(dx).toFixed(3));
      orb.style.setProperty('--gy', clamp(dy).toFixed(3));
    }
  };
  const rest = () => {
    for (const orb of [purpleOrb.current, darkOrb.current]) {
      orb?.style.setProperty('--gx', '0');
      orb?.style.setProperty('--gy', '0');
    }
  };

  const tap = (key: Key, name: string) => {
    take();
    if (reduced) return;
    replay(key);
    setSaid(`Replaying the ${name}.`);
  };
  const poke = (key: 'purple' | 'dark') => {
    take();
    if (reduced) return;
    setBlinks((current) => ({ ...current, [key]: current[key] + 1 }));
    setSaid(`The ${key} assistant blinks.`);
  };

  return (
    <div
      ref={root}
      className={`${styles.root} ${className ?? ''}`}
      data-color-scheme={colorScheme}
      data-paused={!visible || undefined}
      data-part="root"
      onPointerMove={look}
      onPointerLeave={rest}
    >
      <ul className={styles.grid} aria-label="Four app screens">
        {PHONES.map((phone, index) => (
          <li
            key={phone.key}
            className={styles.tile}
            data-tile={index + 1}
            data-part="tile"
            aria-label={phone.name}
          >
            <button
              type="button"
              className={styles.replay}
              data-for={phone.key}
              onClick={() => tap(phone.key, phone.name)}
              aria-label={`Replay the ${phone.name}`}
            />
            <div className={styles.tileInner}>
              <div className={styles.press}>
                <div
                  className={styles.phone}
                  data-phone={phone.key}
                  data-part="phone"
                  data-run={runs[phone.key] > 0 ? '' : undefined}
                >
                  <span className={styles.buttons} aria-hidden="true" />
                  <div className={styles.bezel}>
                    {phone.key === 'tracker' && <TrackerScreen key={runs.tracker} />}
                    {phone.key === 'streak' && <StreakScreen key={runs.streak} />}
                    {phone.key === 'purple' && (
                      <PurpleScreen
                        key={runs.purple}
                        orbRef={purpleOrb}
                        blink={blinks.purple}
                        onPoke={() => poke('purple')}
                      />
                    )}
                    {phone.key === 'dark' && (
                      <DarkScreen
                        key={runs.dark}
                        orbRef={darkOrb}
                        blink={blinks.dark}
                        onPoke={() => poke('dark')}
                      />
                    )}
                  </div>
                </div>
              </div>
            </div>
          </li>
        ))}
      </ul>
      <p className={styles.visuallyHidden} aria-live="polite">
        {said}
      </p>
    </div>
  );
}
```

### app-collection/screens.tsx

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

'use client';

import { useId, type CSSProperties, type Ref } from 'react';
import styles from './app-collection.module.css';

/*
 * The four screens of the collection, each laid out at 390 × 844 points (CSS px inside the
 * phone): a step tracker, a streak card, a purple assistant's pricing page and a dark
 * assistant's greeting. Everything is placed absolutely at the points it sits at on a phone.
 * Their motion is CSS only: each part names its entrance keyframes and a delay, so remounting a
 * screen (a new `key`) plays it again; the idle loops start once the entrance is over.
 */

/* SVG ids unique to each screen, so several collections can share a page. */
const useUid = () => 'ac' + useId().replace(/[^a-zA-Z0-9]/g, '');

/** Generated transparent renders; text, eyes and interactions stay live above the art. */
function Artwork({
  name,
  className,
  style,
}: {
  name: string;
  className?: string;
  style?: CSSProperties;
}) {
  return (
    <img
      className={className}
      style={style}
      src={`/experiments/app-collection/${name}-v2.webp`}
      width={512}
      height={512}
      alt=""
      draggable={false}
    />
  );
}

/* ---------- Status bar ---------- */

function StatusBar({
  time,
  tone,
  island = false,
  arrow = false,
  top,
}: {
  time: string;
  tone: 'ink' | 'white' | 'violet';
  island?: boolean;
  arrow?: boolean;
  top: number;
}) {
  return (
    <div className={styles.status} data-tone={tone} style={{ top }} aria-hidden="true">
      <span className={styles.time}>
        {time}
        {arrow && (
          <svg className={styles.arrowIcon} viewBox="0 0 12 12">
            <path d="M11 1 1.6 5.3l4.3.8.8 4.3Z" fill="currentColor" />
          </svg>
        )}
      </span>
      {island && <span className={styles.island} />}
      <span className={styles.statusIcons}>
        <svg className={styles.signal} viewBox="0 0 18 12">
          {[3.6, 6, 8.4, 11.2].map((h, i) => (
            <rect
              key={i}
              x={i * 4.7}
              y={12 - h}
              width="3.2"
              height={h}
              rx="0.9"
              fill="currentColor"
              opacity={tone === 'violet' && i > 1 ? 0.35 : 1}
            />
          ))}
        </svg>
        {tone === 'violet' ? (
          <svg className={styles.link} viewBox="0 0 18 12">
            <rect
              x="1.2"
              y="1.6"
              width="9.6"
              height="7.6"
              rx="3.8"
              fill="none"
              stroke="currentColor"
              strokeWidth="1.7"
            />
            <rect
              x="7.2"
              y="2.8"
              width="9.6"
              height="7.6"
              rx="3.8"
              fill="none"
              stroke="currentColor"
              strokeWidth="1.7"
            />
          </svg>
        ) : (
          <svg className={styles.wifi} viewBox="0 0 16 12">
            <path
              d="M8 11.4 5.6 8.9a3.4 3.4 0 0 1 4.8 0ZM3.5 6.8a6.4 6.4 0 0 1 9 0l1.6-1.6a8.6 8.6 0 0 0-12.2 0ZM.6 3.9a10.5 10.5 0 0 1 14.8 0L16 3.3V3A12.6 12.6 0 0 0 0 3v.3Z"
              fill="currentColor"
            />
          </svg>
        )}
        {tone === 'violet' ? (
          <span className={styles.batteryPill}>100</span>
        ) : (
          <svg className={styles.battery} viewBox="0 0 27 13">
            <rect
              x="0.6"
              y="0.6"
              width="23"
              height="11.8"
              rx="3.6"
              fill="none"
              stroke="currentColor"
              strokeOpacity="0.4"
              strokeWidth="1.1"
            />
            <rect x="2.2" y="2.2" width="19.8" height="8.6" rx="2.2" fill="currentColor" />
            <path d="M25 4.4v4.2a2.2 2.2 0 0 0 0-4.2Z" fill="currentColor" opacity="0.45" />
          </svg>
        )}
      </span>
    </div>
  );
}

/* ---------- 1. Step tracker ---------- */

const WEEK = [
  { day: 'M', date: '05', lit: true },
  { day: 'T', date: '06', lit: false },
  { day: 'W', date: '07', lit: false },
  { day: 'T', date: '08', lit: false },
  { day: 'F', date: '09', lit: true },
  { day: 'S', date: '10', lit: true, today: true },
  { day: 'S', date: '11', lit: false },
];

function FlameShoe({ lit }: { lit: boolean }) {
  return (
    <span className={styles.flameShoe} data-lit={lit || undefined} aria-hidden="true">
      <Artwork name={lit ? 'tracker-gold' : 'tracker-silver'} />
    </span>
  );
}

/* The gauge's ring: a half annulus round the point (195, 398), 115 to 158 pt, with its ends
   rounded off by 10 pt. */
const CX = 195;
const CY = 398;
const RING = (() => {
  const R = 158;
  const r = 115;
  const c = 10;
  return [
    `M${CX - R} ${CY - c}`,
    `A${R} ${R} 0 0 1 ${CX + R} ${CY - c}`,
    `Q${CX + R} ${CY} ${CX + R - c} ${CY}`,
    `L${CX + r + c} ${CY}`,
    `Q${CX + r} ${CY} ${CX + r} ${CY - c}`,
    `A${r} ${r} 0 0 0 ${CX - r} ${CY - c}`,
    `Q${CX - r} ${CY} ${CX - r - c} ${CY}`,
    `L${CX - R + c} ${CY}`,
    `Q${CX - R} ${CY} ${CX - R} ${CY - c}Z`,
  ].join(' ');
})();
const SWEEP = `M${CX - 137} ${CY} A137 137 0 0 1 ${CX + 137} ${CY}`;
const DIGITS = ['1', '9', ',', '5', '3', '8'];

export function TrackerScreen() {
  const u = useUid();
  return (
    <div className={`${styles.screen} ${styles.tracker}`} data-part="screen">
      <StatusBar time="9:41" tone="ink" top={20} />
      <div className={styles.trHeader} data-part="date">
        <span className={styles.trDay}>10</span>
        <span className={styles.trMonth}>Jan</span>
        <svg className={styles.trChevron} viewBox="0 0 14 8" aria-hidden="true">
          <path d="m1.5 1.5 5.5 5 5.5-5" fill="none" stroke="currentColor" strokeWidth="1.9" />
        </svg>
        <svg className={styles.trCalendar} viewBox="0 0 18 19" aria-hidden="true">
          <rect
            x="1.2"
            y="2.4"
            width="15.6"
            height="15.4"
            rx="3.6"
            fill="none"
            stroke="currentColor"
            strokeWidth="2.2"
          />
          <path d="M1.2 6h15.6v1.4H1.2Z" fill="currentColor" />
          <path d="M4.2 2.4h9.6V6.4H4.2Z" fill="currentColor" />
          <path d="M5.4 0.6v3M12.6 0.6v3" stroke="currentColor" strokeWidth="2" />
        </svg>
        <svg className={styles.trPeople} viewBox="0 0 26 17" aria-hidden="true">
          <circle cx="13" cy="4" r="3.6" fill="currentColor" />
          <circle cx="5.2" cy="5.6" r="2.8" fill="currentColor" />
          <circle cx="20.8" cy="5.6" r="2.8" fill="currentColor" />
          <path d="M6.6 16.6c0-4.4 2.8-7.2 6.4-7.2s6.4 2.8 6.4 7.2Z" fill="currentColor" />
          <path
            d="M0 15.6c0-3.4 2-5.4 4.8-5.4 1.2 0 2.2.3 3 1-1.2 1.2-2 2.8-2.2 4.4ZM26 15.6c0-3.4-2-5.4-4.8-5.4-1.2 0-2.2.3-3 1 1.2 1.2 2 2.8 2.2 4.4Z"
            fill="currentColor"
          />
        </svg>
      </div>
      <ol className={styles.week} data-part="week" aria-label="This week">
        {WEEK.map((d, i) => (
          <li
            key={d.date}
            className={styles.weekDay}
            style={{ left: 42 + i * 50.83, animationDelay: `${0.17 + i * 0.045}s` }}
            data-today={d.today || undefined}
            aria-label={`${d.date} January${d.lit ? ', goal reached' : ''}${d.today ? ', today' : ''}`}
          >
            <span className={styles.weekLetter} aria-hidden="true">
              {d.day}
            </span>
            <FlameShoe lit={d.lit} />
            <span className={styles.weekDate} aria-hidden="true">
              {d.date}
            </span>
            {d.today && <span className={styles.todayDot} aria-hidden="true" />}
          </li>
        ))}
      </ol>
      <div className={styles.gauge} data-part="gauge">
        <svg className={styles.gaugeArt} viewBox="0 0 390 420" data-part="ring" aria-hidden="true">
          <defs>
            <linearGradient id={`${u}track`} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stopColor="#f3fbff" />
              <stop offset="1" stopColor="#d8f1fc" />
            </linearGradient>
            <linearGradient id={`${u}fill`} x1="0" y1="1" x2="1" y2="0">
              <stop offset="0" stopColor="#6fdcfb" />
              <stop offset="0.5" stopColor="#66d4fb" />
              <stop offset="1" stopColor="#59c8f8" />
            </linearGradient>
            <mask id={`${u}sweep`} maskUnits="userSpaceOnUse" x="0" y="0" width="390" height="420">
              <path
                className={styles.sweep}
                d={SWEEP}
                pathLength={100}
                fill="none"
                stroke="#fff"
                strokeWidth="64"
              />
            </mask>
            <filter id={`${u}glow`} x="-30%" y="-30%" width="160%" height="160%">
              <feGaussianBlur stdDeviation="11" />
            </filter>
            <filter id={`${u}soft`} x="-20%" y="-20%" width="140%" height="140%">
              <feGaussianBlur stdDeviation="4" />
            </filter>
            <clipPath id={`${u}ring`}>
              <path d={RING} />
            </clipPath>
          </defs>
          <g className={styles.gaugeGlow}>
            <path d={RING} fill="#9fe2ff" mask={`url(#${u}sweep)`} filter={`url(#${u}glow)`} />
          </g>
          <path d={RING} fill={`url(#${u}track)`} />
          <g mask={`url(#${u}sweep)`}>
            <path d={RING} fill={`url(#${u}fill)`} />
            <g clipPath={`url(#${u}ring)`}>
              <circle
                cx={CX}
                cy={CY}
                r="117"
                fill="none"
                stroke="#d6f6ff"
                strokeWidth="10"
                filter={`url(#${u}soft)`}
                opacity="0.75"
              />
              <circle
                cx={CX}
                cy={CY}
                r="157"
                fill="none"
                stroke="#3fb7f0"
                strokeWidth="6"
                filter={`url(#${u}soft)`}
                opacity="0.35"
              />
            </g>
          </g>
          <g className={styles.needle}>
            <path d={`M${CX - 140} ${CY - 3.6}L${CX - 140} ${CY + 3.6}L${CX - 114} ${CY}Z`} />
          </g>
        </svg>
        <span className={styles.gaugeMin} aria-hidden="true">
          0
        </span>
        <span className={styles.gaugeMax} aria-hidden="true">
          8,000
        </span>
        <p className={styles.steps} data-part="number">
          <span className={styles.stepsNumber} data-part="count" aria-hidden="true">
            {DIGITS.map((digit, i) => (
              <span
                key={i}
                className={styles.digit}
                style={{ animationDelay: `${[0.35, 0.43, 0.55, 0.7, 0.86, 1][i]}s` }}
              >
                {digit}
              </span>
            ))}
          </span>
          <span className={styles.stepsUnit}>
            <span className={styles.visuallyHidden}>19,538 </span>steps
          </span>
        </p>
        <p className={styles.goal}>Daily goal: 8,000</p>
        <p className={styles.passed}>Goal passed · 244%</p>
      </div>
      <ul className={styles.stats} data-part="stats">
        <li className={styles.pill} style={{ left: 21, width: 112, animationDelay: '0.86s' }}>
          <svg className={styles.pillIcon} viewBox="0 0 18 18" aria-hidden="true">
            <rect
              x="-1"
              y="6"
              width="20"
              height="6.4"
              rx="1.6"
              transform="rotate(-45 9 9)"
              fill="none"
              stroke="#46b8dc"
              strokeWidth="1.7"
            />
            <path
              d="m6 9.4 1.6 1.6M8.4 7l1.6 1.6M10.8 4.6l1.6 1.6"
              stroke="#46b8dc"
              strokeWidth="1.5"
              strokeLinecap="round"
            />
          </svg>
          13.05 km
        </li>
        <li className={styles.pill} style={{ left: 147, width: 109, animationDelay: '0.92s' }}>
          <svg className={styles.pillIcon} viewBox="0 0 18 18" aria-hidden="true">
            <path
              d="M9.4 1.4c2.4 3 5.4 5.2 5.4 9.2 0 3.4-2.6 5.8-5.8 5.8S3.2 14 3.2 10.8c0-2.4 1.2-4.2 2.6-5.6.2 1.8.8 2.8 1.8 3.2.2-2.6.6-4.6 1.8-7Z"
              fill="none"
              stroke="#46b8dc"
              strokeWidth="1.7"
              strokeLinejoin="round"
            />
          </svg>
          808 kcal
        </li>
        <li className={styles.pill} style={{ left: 277, width: 93, animationDelay: '0.98s' }}>
          <svg className={styles.pillIcon} viewBox="0 0 18 18" aria-hidden="true">
            <path
              d="M9 11.6V2M5.4 5.2 9 1.6l3.6 3.6M6 7.6H3.4v8.6h11.2V7.6H12"
              fill="none"
              stroke="#111"
              strokeWidth="1.6"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
          Share
        </li>
      </ul>
      <span className={styles.homeBar} data-tone="ink" aria-hidden="true" />
    </div>
  );
}

/* ---------- 2. Streak card ---------- */

type Cell = 'flame' | 'sleep' | 'lit' | 'empty';
const STREAKS: Cell[] = [
  'flame',
  'lit',
  'empty',
  'empty',
  'empty',
  'empty',
  'empty',
  'flame',
  'sleep',
  'sleep',
  'flame',
  'flame',
  'flame',
  'sleep',
  'sleep',
  'flame',
  'flame',
  'flame',
  'sleep',
  'flame',
  'sleep',
];

export function StreakScreen() {
  const u = useUid();
  return (
    <div className={`${styles.screen} ${styles.streak}`} data-part="screen">
      <span className={styles.stGrid} aria-hidden="true" />
      <StatusBar time="9:41" tone="ink" top={20} />
      <p className={styles.stTitleBar}>Rewards</p>
      <div className={styles.unlock} data-part="unlock">
        <span className={styles.unlockTile} aria-hidden="true">
          <svg className={styles.unlockStar} viewBox="0 0 20 20">
            <defs>
              <linearGradient id={`${u}dark-star`} x1="0" y1="0" x2="1" y2="1">
                <stop offset="0" stopColor="#262a28" />
                <stop offset="1" stopColor="#7e8582" />
              </linearGradient>
            </defs>
            <path
              d="M10 0c.8 5.4 4.6 9.2 10 10-5.4.8-9.2 4.6-10 10-.8-5.4-4.6-9.2-10-10 5.4-.8 9.2-4.6 10-10Z"
              fill={`url(#${u}dark-star)`}
            />
          </svg>
          <span className={styles.unlockLock}>
            <svg viewBox="0 0 8 9">
              <rect x="0.6" y="3.6" width="6.8" height="5" rx="1.2" fill="#fff" />
              <path d="M2 3.8V2.6a2 2 0 0 1 4 0v1.2" fill="none" stroke="#fff" strokeWidth="1.2" />
            </svg>
          </span>
          <span className={styles.unlockCurl} />
        </span>
        Unlock New Bots
      </div>
      <section className={styles.card} data-part="card" aria-label="Best trade yet">
        <span className={styles.badgePlate} aria-hidden="true" />
        <span className={styles.badge} data-part="badge" aria-hidden="true">
          <Artwork name="streak-badge" className={styles.badgeSpin} />
          <span className={styles.badgeEyes}>
            <i />
            <i />
          </span>
        </span>
        <h3 className={styles.stTitle}>Best Trade Yet!</h3>
        <p className={styles.stSub}>Your best trade so far</p>
        <ul className={styles.cells} data-part="streaks" aria-label="Streak: 1 of 21 days lit">
          {STREAKS.map((cell, i) => (
            <li
              key={i}
              className={styles.cellDot}
              data-cell={cell}
              style={{ left: 44 + (i % 7) * 45 - 15, top: 576 - 424 + Math.floor(i / 7) * 45 }}
              aria-hidden="true"
            >
              {cell === 'flame' && <Artwork name="streak-grey" />}
              {cell === 'sleep' && (
                <svg viewBox="0 0 16 16">
                  <path
                    d="M1.6 8.2q2.4 1.6 4.8 0M9.6 8.2q2.4 1.6 4.8 0"
                    fill="none"
                    stroke="#cfcfcf"
                    strokeWidth="1.8"
                    strokeLinecap="round"
                  />
                </svg>
              )}
              {cell === 'lit' && (
                <>
                  <Artwork name="streak-grey" className={styles.cellGrey} />
                  <Artwork name="streak-fire" className={styles.cellFire} />
                </>
              )}
            </li>
          ))}
        </ul>
        <span className={styles.generate} data-part="button">
          <Artwork name="iridescent-star" className={styles.genStar} />
          Generate New Plan
        </span>
      </section>
      <span className={styles.homeBar} data-tone="ink" aria-hidden="true" />
    </div>
  );
}

/* ---------- 3. Purple assistant ---------- */

// The seven miniatures share one transparent atlas; each keeps its reference size and motion.
const SATELLITES = [
  { x: 103, y: 94, size: 18, crop: [83, 7, 100, 100] },
  { x: 282, y: 94, size: 20, crop: [525, 0, 107, 109] },
  { x: 342, y: 88, size: 15, crop: [672, 49, 96, 78] },
  { x: 50, y: 144, size: 15, crop: [0, 116, 92, 94] },
  { x: 356, y: 172, size: 12, crop: [663, 145, 81, 81] },
  { x: 95, y: 204, size: 22, crop: [88, 212, 114, 98] },
  { x: 309, y: 201, size: 18, crop: [557, 200, 110, 113] },
];

function Laurel({ flip }: { flip?: boolean }) {
  return (
    <svg
      className={styles.laurel}
      viewBox="0 0 10 16"
      style={flip ? { transform: 'scaleX(-1)' } : undefined}
      aria-hidden="true"
    >
      <path
        d="M7.6 15.4C4 13.6 2.6 10 3.4 5.6"
        fill="none"
        stroke="currentColor"
        strokeWidth="1"
        strokeLinecap="round"
      />
      <ellipse
        cx="2.6"
        cy="4"
        rx="1.3"
        ry="2.5"
        transform="rotate(-18 2.6 4)"
        fill="currentColor"
      />
      <ellipse
        cx="5.6"
        cy="6.2"
        rx="1.2"
        ry="2.3"
        transform="rotate(40 5.6 6.2)"
        fill="currentColor"
      />
      <ellipse
        cx="2"
        cy="8.6"
        rx="1.2"
        ry="2.3"
        transform="rotate(-40 2 8.6)"
        fill="currentColor"
      />
      <ellipse
        cx="5.6"
        cy="10.6"
        rx="1.2"
        ry="2.3"
        transform="rotate(50 5.6 10.6)"
        fill="currentColor"
      />
      <ellipse
        cx="2.8"
        cy="12.6"
        rx="1.2"
        ry="2.2"
        transform="rotate(-60 2.8 12.6)"
        fill="currentColor"
      />
    </svg>
  );
}

export function PurpleScreen({
  orbRef,
  onPoke,
  blink,
}: {
  orbRef: Ref<HTMLSpanElement>;
  onPoke: () => void;
  blink: number;
}) {
  return (
    <div className={`${styles.screen} ${styles.purple}`} data-part="screen">
      <StatusBar time="9:41" tone="violet" island arrow top={14} />
      <span className={styles.floaters} aria-hidden="true">
        {SATELLITES.map(({ x, y, size, crop: [left, top, width, height] }, i) => (
          <span
            key={i}
            className={styles.floater}
            style={{
              left: x - size / 2,
              top: y - (size * height) / width / 2,
              width: size,
              height: (size * height) / width,
              animationDelay: `${0.34 + (i % 6) * 0.035}s, ${1 + i * 0.37}s`,
            }}
          >
            <Artwork
              name="purple-satellites"
              className={styles.floaterArt}
              style={{
                width: (768 * size) / width,
                height: (313 * size) / width,
                left: (-left * size) / width,
                top: (-top * size) / width,
              }}
            />
          </span>
        ))}
      </span>
      <span className={styles.p3Shadow} aria-hidden="true" />
      <span className={styles.p3Float} aria-hidden="true">
        <span className={styles.p3Enter}>
          <span className={styles.p3Look} ref={orbRef} data-part="orb">
            <Artwork name="purple-orb" className={styles.p3Sphere} />
            <span className={styles.p3Face}>
              <span className={styles.p3Eyes} key={blink} data-blink={blink > 0 || undefined}>
                <i />
                <i />
              </span>
            </span>
          </span>
        </span>
      </span>
      <button
        type="button"
        className={styles.poke}
        style={{ left: 129, top: 89, width: 132, height: 132 }}
        onClick={(event) => {
          event.stopPropagation();
          onPoke();
        }}
        aria-label="Poke the purple assistant"
      />
      <span className={styles.chip} data-part="chip">
        <Laurel />
        Pricing
        <Laurel flip />
      </span>
      <h3 className={styles.p3Head} data-part="headline">
        <span className={styles.p3Line1}>Your assistant</span>
        <span className={styles.p3Line2}>for anything.</span>
      </h3>
      <p className={styles.p3Tags}>
        Health<span aria-hidden="true">·</span>Productivity<span aria-hidden="true">·</span>
        Finance
      </p>
      <div className={styles.plan} data-part="price">
        <span className={styles.planName}>Monthly</span>
        <span className={styles.planPrice}>
          $16.99<small>/mo</small>
        </span>
        <span className={styles.planRadio} aria-hidden="true" />
      </div>
      <span className={styles.homeBar} data-tone="violet" aria-hidden="true" />
    </div>
  );
}

/* ---------- 4. Dark assistant ---------- */

const STARS = [
  [161, 63],
  [175, 69],
  [190, 47],
  [289, 179],
  [316, 199],
  [306, 260],
  [108, 305],
  [264, 346],
  [240, 253],
  [87, 150],
];

export function DarkScreen({
  orbRef,
  onPoke,
  blink,
}: {
  orbRef: Ref<HTMLSpanElement>;
  onPoke: () => void;
  blink: number;
}) {
  return (
    <div className={`${styles.screen} ${styles.dark}`} data-part="screen">
      <span className={styles.dots} aria-hidden="true" />
      <StatusBar time="10:10" tone="white" top={16} />
      <span className={styles.back} aria-hidden="true">
        <svg viewBox="0 0 18 18">
          <path
            d="M16 9H2.4M8.4 2.6 2 9l6.4 6.4"
            fill="none"
            stroke="currentColor"
            strokeWidth="1.6"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </span>
      <span className={styles.stars} aria-hidden="true">
        {STARS.map(([x, y], i) => (
          <i key={i} style={{ left: x, top: y, animationDelay: `${(i * 0.53) % 3}s` }} />
        ))}
      </span>
      <span className={styles.halo} aria-hidden="true" />
      <span className={styles.sparks} aria-hidden="true">
        <i data-tone="red" />
        <i data-tone="blue" />
        <i data-tone="red2" />
      </span>
      <span className={styles.p4Enter} aria-hidden="true">
        <span className={styles.p4Look} ref={orbRef} data-part="orb">
          <Artwork name="dark-orb" className={styles.p4Blob} />
          <span className={styles.p4Eyes} key={blink} data-blink={blink > 0 || undefined}>
            <i />
            <i />
          </span>
        </span>
      </span>
      <button
        type="button"
        className={styles.poke}
        style={{ left: 103, top: 115, width: 182, height: 188 }}
        onClick={(event) => {
          event.stopPropagation();
          onPoke();
        }}
        aria-label="Poke the dark assistant"
      />
      <h3 className={styles.p4Text}>
        Hey There!{' '}
        <span>
          What’s On
          <br />
          Your Mind Today
        </span>
      </h3>
      <span className={styles.homeBar} data-tone="white" aria-hidden="true" />
    </div>
  );
}
```

### app-collection/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 returned too, so
 * the looping art can pause off screen and stand still under reduced motion.
 */
export function useAutoplay<T extends HTMLElement>(idle = 6000) {
  const ref = useRef<T>(null);
  const [visible, setVisible] = useState(false);
  const [reduced, setReduced] = useState(true);
  const [taken, setTaken] = useState(false);
  const timer = useRef(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    let onScreen = false;
    const update = () => setVisible(onScreen && !document.hidden);
    const observer = new IntersectionObserver(
      ([entry]) => {
        onScreen = entry.isIntersecting;
        update();
      },
      { threshold: 0.05 },
    );
    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 };
}
```

### app-collection/app-collection.module.css

```css
/* App collection (app-collection.tsx): a quiet stage holding a 2 × 2 grid of tinted tiles, each
   with a phone cropped by its edges. A tile is drawn at 540 × 688 px and scaled as a whole (--k,
   set from the tile's width); the phones inside are 390 × 844-point screens at 1.08 times, so
   every measure inside a screen is in its points.

   Colours: the stage and the tiles are light-dark(light, dark) tokens, so they follow the page's
   color-scheme or the colorScheme prop (data-color-scheme). The four screens keep their own
   designs in both schemes, so their colours are written where they are used.

   Motion: each part's entrance is a keyframes animation with fill-mode both, and its resting
   style is its final state, so a screen remounted plays its entrance again and reduced motion
   simply draws every screen at rest. Only opacity, transforms and a few SVG strokes animate. */

.root {
  --stage: #f4f4f4;
  --edge: rgb(0 0 0 / 0.05);
  --tile-1: #e6e6e6;
  --tile-2: #dedede;
  --tile-3: #efefef;
  --tile-4: #fdfdfd;
  --focus: #0a84ff;
  --k: 0.6;
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 24px;
  box-sizing: border-box;
  container-type: inline-size;
  background: var(--stage);
  border-radius: 18px;
  font-family:
    -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', Inter, 'Segoe UI', Roboto,
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --stage: light-dark(#f4f4f4, #0c0c0d);
    --edge: light-dark(rgb(0 0 0 / 0.05), rgb(255 255 255 / 0.07));
    --tile-1: light-dark(#e6e6e6, #1c1c1e);
    --tile-2: light-dark(#dedede, #17171a);
    --tile-3: light-dark(#efefef, #212124);
    --tile-4: light-dark(#fdfdfd, #111113);
  }
}
.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;
}

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: min(100%, 680px);
  margin: 0;
  padding: 0;
  list-style: none;
  border-radius: 14px;
  overflow: hidden;
  overflow: clip;
  box-shadow: 0 0 0 1px var(--edge);
}
@container (max-width: 599px) {
  .grid {
    grid-template-columns: 1fr;
    width: min(100%, 420px);
  }
}
@container (max-width: 400px) {
  .root {
    padding: 16px;
  }
}

.tile {
  position: relative;
  aspect-ratio: 540 / 688;
  overflow: hidden;
  overflow: clip;
  contain: layout paint;
  background: var(--tile-1);
}
.tile[data-tile='2'] {
  background: var(--tile-2);
}
.tile[data-tile='3'] {
  background: var(--tile-3);
}
.tile[data-tile='4'] {
  background: var(--tile-4);
}

/* The whole tile is the replay button; the phone above it lets taps through, but for its orbs. */
.replay {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  touch-action: manipulation;
}
.replay:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -4px;
  border-radius: 4px;
}
.tileInner {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 540px;
  height: 688px;
  transform: scale(var(--k));
  transform-origin: 0 0;
  pointer-events: none;
}
.press {
  position: absolute;
  inset: 0;
  transition: scale 0.25s cubic-bezier(0.3, 0.7, 0.3, 1);
}
.tile:has(.replay:active) .press {
  scale: 0.985;
}

/* ---------- The phone ---------- */

.phone {
  position: absolute;
  --s: 1.08;
  left: calc((540px - 420px * var(--s)) / 2);
  width: 420px;
  height: 874px;
  box-sizing: border-box;
  padding: 6px;
  border-radius: 69px;
  transform: scale(var(--s));
  transform-origin: 0 0;
  background: linear-gradient(
    140deg,
    #e4e4e4 0%,
    #a9a9a9 22%,
    #ececec 48%,
    #9c9c9c 76%,
    #d6d6d6 100%
  );
  box-shadow:
    inset 0 0 0 1px #7d7d7d,
    inset 0 0 0 2px #e6e6e6,
    inset 0 0 0 3.2px #b1b1b1,
    inset 0 0 0 5px #8e8e8e,
    0 18px 44px rgb(0 0 0 / 0.14),
    0 2px 6px rgb(0 0 0 / 0.08);
}
/* Each phone at the scale and height it is shot at in its tile. */
.phone[data-phone='tracker'] {
  --s: 1.082;
  top: 65.8px;
}
.phone[data-phone='streak'] {
  --s: 1.105;
  top: -340.2px;
}
.phone[data-phone='purple'] {
  --s: 1.087;
  top: 77.7px;
}
.phone[data-phone='dark'] {
  --s: 1.072;
  top: 59.9px;
}
.buttons,
.buttons::before,
.buttons::after {
  position: absolute;
  width: 4px;
  border-radius: 2px 0 0 2px;
  background: linear-gradient(90deg, #8e8e8e, #cfcfcf);
}
.buttons {
  top: 204px;
  left: -3px;
  height: 15px;
  box-shadow: 418px 78px 0 0 #a6a6a6;
}
.buttons::before {
  content: '';
  top: 46px;
  left: 0;
  height: 64px;
}
.buttons::after {
  content: '';
  top: 134px;
  left: 0;
  height: 56px;
}
.bezel {
  width: 408px;
  height: 862px;
  box-sizing: border-box;
  padding: 9px;
  border-radius: 63px;
  background: #050505;
}

.screen {
  position: relative;
  width: 390px;
  height: 844px;
  overflow: hidden;
  overflow: clip;
  border-radius: 54px;
  isolation: isolate;
}
.screen :where(p, h3, ul, ol) {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The status bar, the island and the home indicator. */
.status {
  position: absolute;
  left: 0;
  width: 390px;
  height: 22px;
  color: #000;
}
.status[data-tone='white'] {
  color: #fff;
}
.status[data-tone='violet'] {
  color: #24113f;
}
.time {
  position: absolute;
  left: 38px;
  top: 0;
  display: flex;
  align-items: center;
  gap: 3px;
  width: 66px;
  justify-content: center;
  font-size: 17px;
  font-weight: 600;
  line-height: 22px;
  letter-spacing: -0.3px;
}
.arrowIcon {
  width: 12px;
  height: 12px;
}
.island {
  position: absolute;
  top: -3px;
  left: 140px;
  width: 110px;
  height: 32px;
  border-radius: 16px;
  background: #000;
}
.statusIcons {
  position: absolute;
  right: 33px;
  top: 5px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.signal {
  width: 18px;
  height: 12px;
}
.wifi {
  width: 16px;
  height: 12px;
}
.link {
  width: 18px;
  height: 12px;
}
.battery {
  width: 27px;
  height: 13px;
}
.batteryPill {
  display: grid;
  place-items: center;
  width: 25px;
  height: 13px;
  border-radius: 4px;
  background: #c3a2f0;
  color: #f4ecff;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
}
.homeBar {
  position: absolute;
  left: 128px;
  bottom: 8px;
  width: 134px;
  height: 5px;
  border-radius: 3px;
  background: #000;
}
.homeBar[data-tone='white'] {
  background: #fff;
}
.homeBar[data-tone='violet'] {
  background: #2a1450;
}
.visuallyHidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- 1. Step tracker ---------- */

.tracker {
  background: #fdfdfd;
  color: #0b0b0b;
}
.trHeader {
  position: absolute;
  inset: 0 0 auto 0;
  height: 110px;
}
.trDay {
  position: absolute;
  left: 18px;
  top: 61px;
  font-size: 50px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -3px;
}
.trMonth {
  position: absolute;
  left: 75px;
  top: 83.5px;
  font-size: 24px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.4px;
}
.trChevron {
  position: absolute;
  left: 120px;
  top: 95px;
  width: 13px;
  height: 7.5px;
}
.trCalendar {
  position: absolute;
  left: 302px;
  top: 76px;
  width: 18px;
  height: 19px;
}
.trPeople {
  position: absolute;
  left: 333px;
  top: 77px;
  width: 26px;
  height: 17px;
}
.week {
  position: absolute;
  inset: 0;
}
.weekDay {
  position: absolute;
  top: 0;
  width: 44px;
  height: 220px;
  margin-left: -22px;
}
.weekLetter,
.weekDate {
  position: absolute;
  left: 0;
  width: 44px;
  text-align: center;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.weekLetter {
  top: 123px;
  font-size: 10.5px;
  font-weight: 600;
  color: #8d8d8d;
}
.weekDate {
  top: 194px;
  font-size: 12px;
  font-weight: 500;
  color: #9d9d9d;
}
.weekDay[data-today] .weekDate {
  color: #111;
  font-weight: 600;
}
.todayDot {
  position: absolute;
  left: 19.5px;
  top: 211.5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #e0962a;
}
.flameShoe {
  position: absolute;
  left: 3.5px;
  top: 142.5px;
  width: 37px;
  height: 37px;
  overflow: visible;
}
.flameShoe img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.gauge {
  position: absolute;
  inset: 0;
}
.gaugeArt {
  position: absolute;
  left: 0;
  top: 0;
  width: 390px;
  height: 420px;
  overflow: visible;
}
.gaugeGlow {
  opacity: 0.6;
}
.sweep {
  stroke-dasharray: 80.5 200;
}
.needle {
  fill: #121212;
  transform-box: view-box;
  transform-origin: 195px 398px;
  transform: rotate(144.9deg);
}
.gaugeMin,
.gaugeMax {
  position: absolute;
  top: 405px;
  width: 40px;
  margin-left: -20px;
  text-align: center;
  font-size: 7px;
  line-height: 1;
  color: #bdbdbd;
}
.gaugeMin {
  left: 42px;
}
.gaugeMax {
  left: 340px;
}
.steps {
  position: absolute;
  inset: 0;
}
.stepsNumber {
  position: absolute;
  left: 0;
  top: 323.5px;
  width: 390px;
  text-align: center;
  font-size: 48px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -2.2px;
}
.digit {
  display: inline-block;
}
.stepsUnit {
  position: absolute;
  left: 0;
  top: 390.5px;
  width: 390px;
  text-align: center;
  font-size: 12px;
  line-height: 1;
  color: #9a9a9a;
}
.goal,
.passed {
  position: absolute;
  left: 0;
  width: 390px;
  text-align: center;
  line-height: 1;
}
.goal {
  top: 410.5px;
  font-size: 13px;
  font-weight: 500;
  color: #111;
}
.passed {
  top: 434px;
  font-size: 12.5px;
  font-weight: 500;
  color: #c6ae22;
}
.stats {
  position: absolute;
  inset: 0;
}
.pill {
  position: absolute;
  top: 476px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border-radius: 19px;
  background: #fff;
  box-shadow:
    0 4px 14px rgb(0 0 0 / 0.055),
    0 0 0 0.5px rgb(0 0 0 / 0.025);
  font-size: 12.5px;
  font-weight: 500;
  color: #111;
  white-space: nowrap;
}
.pillIcon {
  width: 17px;
  height: 17px;
  flex: none;
}

/* ---------- 2. Streak card ---------- */

.streak {
  background: linear-gradient(180deg, #e7f1ec 0%, #edf3ef 300px, #f1f2f1 420px, #eeeeee 100%);
  color: #395169;
}
.stGrid {
  position: absolute;
  inset: 0 0 auto 0;
  height: 440px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='23.5' height='23.5'%3E%3Cpath d='M8 11.75h7.5M11.75 8v7.5' stroke='%23d9e2dd' stroke-width='1'/%3E%3C/svg%3E");
  background-size: 23.5px 23.5px;
  background-position: -4px 2px;
  -webkit-mask: linear-gradient(180deg, #000 40%, transparent 96%);
  mask: linear-gradient(180deg, #000 40%, transparent 96%);
}
.stTitleBar {
  position: absolute;
  top: 60px;
  left: 0;
  width: 390px;
  text-align: center;
  font-size: 17px;
  font-weight: 600;
  color: #24313d;
}
.unlock {
  position: absolute;
  left: 113px;
  top: 319px;
  width: 164px;
  height: 50px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  padding-left: 54px;
  border-radius: 14px;
  background: rgb(250 252 251 / 0.72);
  box-shadow:
    0 0 0 1px rgb(255 255 255 / 0.9),
    0 8px 18px rgb(40 60 50 / 0.07);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.05px;
  color: #a2aba7;
  white-space: nowrap;
}
.unlockTile {
  position: absolute;
  left: 6px;
  top: 5.5px;
  width: 39px;
  height: 39px;
  border-radius: 10px;
  background: #e8ebe9;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.03);
}
.unlockTile::before {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 8px;
  background: #f8f9f8;
}
.unlockStar {
  position: absolute;
  left: 10px;
  top: 10px;
  width: 19px;
  height: 19px;
}
.unlockLock {
  position: absolute;
  right: -4px;
  top: -4px;
  display: grid;
  place-items: center;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #f09a4e;
  box-shadow: 0 0 0 1.5px #f3f5f4;
}
.unlockLock svg {
  width: 6px;
  height: 7px;
}
.unlockCurl {
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 9px;
  height: 9px;
  border-radius: 0 0 6px 0;
  background: linear-gradient(135deg, transparent 50%, #dcdedd 50%, #f0f1f0 80%);
}
.card {
  position: absolute;
  left: 15px;
  top: 411px;
  width: 360px;
  height: 390px;
  border-radius: 26px;
  background: #fdfdfd;
  box-shadow: 0 10px 30px rgb(30 50 40 / 0.05);
}
.badgePlate {
  position: absolute;
  left: 141px;
  top: -20px;
  width: 74px;
  height: 74px;
  border-radius: 50%;
  background: #fdfdfd;
}
.badge {
  position: absolute;
  left: 152px;
  top: -12px;
  width: 52px;
  height: 58px;
}
.badgeSpin {
  position: absolute;
  inset: -3px;
  width: 58px;
  height: 64px;
  object-fit: contain;
  transform-origin: center;
}
.badgeEyes {
  position: absolute;
  left: 0;
  top: 0;
  width: 52px;
  height: 58px;
}
.badgeEyes i {
  position: absolute;
  top: 24.5px;
  width: 4.2px;
  height: 9px;
  border-radius: 3px;
  background: #fff;
  transform-origin: 50% 50%;
}
.badgeEyes i:first-child {
  left: 20.5px;
}
.badgeEyes i:last-child {
  left: 27.5px;
}
.stTitle {
  position: absolute;
  left: 0;
  top: 56.5px;
  width: 360px;
  text-align: center;
  font-size: 23px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.1px;
  color: #395169;
}
.stSub {
  position: absolute;
  left: 27px;
  top: 97px;
  width: 304px;
  padding-top: 17px;
  border-top: 1px solid #ededed;
  text-align: center;
  font-size: 13.5px;
  line-height: 1;
  color: #b2b2b2;
}
.cells {
  position: absolute;
  inset: 0;
}
.cellDot {
  position: absolute;
  display: grid;
  place-items: center;
  width: 35px;
  height: 35px;
  border-radius: 50%;
  background: #f2f2f2;
}
.cellDot svg,
.cellDot img {
  width: 16px;
  height: 16px;
  object-fit: contain;
  grid-area: 1 / 1;
}
.cellDot[data-cell='empty'] {
  background: #f8f8f8;
}
.cellDot[data-cell='lit'] {
  background: radial-gradient(closest-side, #fde9e3 50%, #f6eeeb 100%);
}
.cellDot .cellFire {
  width: 18px;
  height: 18px;
  filter: drop-shadow(0 1.5px 2px rgb(255 120 60 / 0.35));
}
.generate {
  position: absolute;
  left: 21px;
  top: 310px;
  width: 316px;
  height: 63px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding-right: 4px;
  box-sizing: border-box;
  border-radius: 21px;
  background: linear-gradient(180deg, #1f1f1f 0%, #050505 60%, #000 100%);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.1),
    0 6px 16px rgb(0 0 0 / 0.16);
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -0.1px;
  color: #fff;
}
.genStar {
  width: 25px;
  height: 25px;
}

/* ---------- 3. Purple assistant ---------- */

.purple {
  background: linear-gradient(
    180deg,
    #c098fb 0%,
    #c9a5fb 110px,
    #d5b9fc 200px,
    #dfcafa 280px,
    #e8dafb 350px,
    #ece1fc 430px,
    #eee4fc 100%
  );
  color: #5b18c8;
}
.floaters {
  position: absolute;
  inset: 0;
}
.floater {
  position: absolute;
  overflow: hidden;
  opacity: 0.72;
}
.floaterArt {
  position: absolute;
  max-width: none;
}
.p3Shadow {
  position: absolute;
  left: 120px;
  top: 233px;
  width: 150px;
  height: 34px;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgb(160 92 240 / 0.62),
    rgb(176 120 244 / 0.25) 60%,
    transparent
  );
  filter: blur(3px);
}
.p3Float {
  position: absolute;
  left: 129px;
  top: 89px;
  width: 132px;
  height: 132px;
}
.p3Enter,
.p3Look {
  position: absolute;
  inset: 0;
}
.p3Look {
  translate: calc(var(--gx, 0) * 4px) calc(var(--gy, 0) * 4px);
  transition: translate 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.p3Sphere {
  position: absolute;
  inset: -2px;
  width: 136px;
  height: 136px;
  object-fit: contain;
  filter: drop-shadow(0 7px 12px rgb(79 18 165 / 0.15));
}
.p3Face {
  position: absolute;
  inset: 0;
}
.p3Eyes {
  position: absolute;
  inset: 0;
  translate: calc(var(--gx, 0) * 5px) calc(var(--gy, 0) * 4px);
  transition: translate 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.p3Eyes i {
  position: absolute;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #fffef6;
  box-shadow:
    0 0 4px 1.5px rgb(250 230 255 / 0.95),
    0 0 12px 3px rgb(190 120 255 / 0.75);
}
.p3Eyes i:first-child {
  left: 76px;
  top: 64px;
}
.p3Eyes i:last-child {
  left: 98px;
  top: 61px;
}
.poke {
  position: absolute;
  z-index: 2;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  touch-action: manipulation;
}
.poke:focus-visible {
  outline: 2.5px solid var(--focus);
  outline-offset: 4px;
}
.chip {
  position: absolute;
  left: 146px;
  top: 279px;
  width: 98px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border-radius: 16px;
  background: linear-gradient(180deg, #fbf7ff, #f1e8ff);
  box-shadow:
    0 0 0 1px rgb(255 255 255 / 0.8),
    0 5px 14px rgb(120 60 210 / 0.16);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.1px;
  color: #5a2bb4;
}
.laurel {
  width: 9px;
  height: 15px;
  color: #c4a6f0;
}
.p3Head {
  position: absolute;
  inset: 0;
  font-size: 35px;
  line-height: 1;
  letter-spacing: -0.4px;
}

.p3Line1,
.p3Line2 {
  position: absolute;
  left: 0;
  width: 390px;
  text-align: center;
}
.p3Line1 {
  top: 330px;
  font-weight: 600;
  color: #b394ea;
}
.p3Line2 {
  top: 372px;
  font-weight: 700;
  color: #5b18c8;
}
.p3Tags {
  position: absolute;
  left: 0;
  top: 424.5px;
  width: 390px;
  display: flex;
  justify-content: center;
  gap: 9px;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1;
  color: #8b67cf;
}
.p3Tags span {
  color: #b9a0e6;
}
.plan {
  position: absolute;
  left: 15px;
  top: 467px;
  width: 342px;
  height: 104px;
  box-sizing: border-box;
  border-radius: 20px;
  background: rgb(255 255 255 / 0.26);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.75);
}
.planName {
  position: absolute;
  left: 21px;
  top: 14px;
  font-size: 12.5px;
  line-height: 1;
  color: #a689df;
}
.planPrice {
  position: absolute;
  left: 21px;
  top: 35px;
  font-size: 28px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.6px;
  color: #5b18c8;
}
.planPrice small {
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.2px;
}
.planRadio {
  position: absolute;
  left: 297px;
  top: 27px;
  width: 22px;
  height: 22px;
  box-sizing: border-box;
  border-radius: 50%;
  border: 1.6px solid #b9a3e8;
}

/* ---------- 4. Dark assistant ---------- */

.dark {
  background: linear-gradient(180deg, #131313 0%, #0b0b0b 140px, #020202 330px, #000 100%);
  color: #fff;
}
.dots {
  position: absolute;
  inset: 0 0 auto 0;
  height: 360px;
  background-image: radial-gradient(circle, rgb(255 255 255 / 0.09) 0.6px, transparent 0.9px);
  background-size: 5px 5px;
  -webkit-mask: linear-gradient(180deg, #000 10%, transparent 90%);
  mask: linear-gradient(180deg, #000 10%, transparent 90%);
}
.back {
  position: absolute;
  left: 14px;
  top: 57px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #1b1b1b;
  color: #f5f5f5;
}
.back svg {
  width: 18px;
  height: 18px;
}
.stars {
  position: absolute;
  inset: 0;
}
.stars i {
  position: absolute;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #fff;
  opacity: 0.55;
}
.halo {
  position: absolute;
  left: 72px;
  top: 93px;
  width: 242px;
  height: 228px;
  border-radius: 44% 47% 45% 48% / 47% 44% 49% 45%;
  background: radial-gradient(closest-side, #101010 74%, #0c0c0c 88%, rgb(12 12 12 / 0));
  box-shadow:
    inset 0 0 0 1px rgb(255 255 255 / 0.025),
    0 0 40px rgb(0 0 0 / 0.6);
}

.sparks {
  position: absolute;
  left: 194px;
  top: 209px;
}
.sparks i {
  position: absolute;
  width: 30px;
  height: 26px;
  margin: -13px 0 0 -15px;
  border-radius: 50%;
  filter: blur(4px);
  opacity: 0;
}
.sparks i[data-tone='red'] {
  background: radial-gradient(closest-side, #ff6a4c, #e2302a 70%, transparent);
}
.sparks i[data-tone='red2'] {
  background: radial-gradient(closest-side, #ff8a6c, #ff3a2a 70%, transparent);
}
.sparks i[data-tone='blue'] {
  background: radial-gradient(closest-side, #4a3cff, #2a1ad0 70%, transparent);
}
.p4Enter {
  position: absolute;
  left: 103px;
  top: 115px;
  width: 182px;
  height: 188px;
}
.p4Look {
  position: absolute;
  inset: 0;
  translate: calc(var(--gx, 0) * 4px) calc(var(--gy, 0) * 4px);
  transition: translate 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.p4Blob {
  position: absolute;
  inset: -8px;
  width: 198px;
  height: 204px;
  object-fit: contain;
}

.p4Eyes {
  position: absolute;
  left: 91px;
  top: 94px;
  translate: calc(var(--gx, 0) * 9px) calc(var(--gy, 0) * 8px);
  transition: translate 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.p4Eyes i {
  position: absolute;
  top: -7px;
  width: 8px;
  height: 10px;
  border-radius: 9px;
  background: linear-gradient(180deg, #fff 0%, #f1f1f1 55%, #c9c9c9 100%);
  box-shadow:
    inset 0 -2px 3px rgb(0 0 0 / 0.25),
    0 0 6px rgb(255 255 255 / 0.25);
}
.p4Eyes i:first-child {
  left: -11.5px;
}
.p4Eyes i:last-child {
  left: 4.5px;
}
.p4Text {
  position: absolute;
  left: 0;
  top: 390px;
  width: 390px;
  text-align: center;
  font-size: 28px;
  font-weight: 400;
  line-height: 33px;
  letter-spacing: -0.55px;
  color: #fff;
}
.p4Text span {
  color: #858585;
}

/* ---------- Motion ---------- */

/* Entrances, only once a screen has been asked to play (data-run), and every loop, only while
   motion is welcome. Paused off screen. */
@media (prefers-reduced-motion: no-preference) {
  /* 1. The step tracker. */
  .phone[data-run] .trHeader {
    animation: ac-fade-up 0.2s 0.05s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .weekDay {
    animation: ac-day 0.22s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .gaugeArt,
  .phone[data-run] .gaugeMin,
  .phone[data-run] .gaugeMax {
    animation: ac-fade 0.16s 0.3s linear both;
  }
  .phone[data-run] .sweep {
    animation: ac-sweep 0.36s 0.3s cubic-bezier(0.25, 0.65, 0.3, 1) both;
  }
  .phone[data-run] .needle {
    animation: ac-needle 0.36s 0.3s cubic-bezier(0.25, 0.65, 0.3, 1) both;
  }
  .phone[data-run] .digit {
    animation: ac-digit 0.2s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .stepsUnit,
  .phone[data-run] .goal {
    animation: ac-fade 0.2s 0.36s linear both;
  }
  .phone[data-run] .pill {
    animation: ac-pill 0.24s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .passed {
    animation: ac-fade-up 0.24s 0.96s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .gaugeGlow {
    animation: ac-breathe 3.2s 1.4s ease-in-out infinite;
  }
  .flameShoe[data-lit] img {
    animation: ac-flicker 1.3s ease-in-out infinite alternate;
    transform-origin: center bottom;
  }
  .weekDay:nth-child(5) .flameShoe img {
    animation-delay: -0.4s;
  }
  .weekDay:nth-child(6) .flameShoe img {
    animation-delay: -0.9s;
  }
  .todayDot {
    animation: ac-dot 2.4s 1.4s ease-in-out infinite;
  }

  /* 2. The streak card. */
  .phone[data-run] .cellFire {
    animation: ac-fade 0.38s 0.62s ease-out both;
  }
  .phone[data-run] .cellGrey {
    animation: ac-fade-out 0.38s 0.62s ease-out both;
  }
  .phone[data-run] .cellDot[data-cell='lit'] {
    animation: ac-pop 0.5s 0.75s cubic-bezier(0.3, 0.7, 0.3, 1) both;
  }
  .genStar {
    animation: ac-turn 2.6s linear infinite;
  }
  .badgeSpin {
    animation: ac-sway 5s ease-in-out infinite alternate;
  }
  .badgeEyes i {
    animation: ac-blink 4.2s 1.15s infinite;
  }

  /* 3. The purple assistant. */
  .phone[data-run] .p3Enter {
    animation: ac-orb-in 0.62s linear both;
  }
  .phone[data-run] .p3Shadow {
    animation:
      ac-shadow-in 0.6s cubic-bezier(0.25, 0.6, 0.3, 1) both,
      ac-shadow 2.8s 0.9s ease-in-out infinite;
  }
  .p3Shadow {
    animation: ac-shadow 2.8s 0.9s ease-in-out infinite;
  }
  .p3Float {
    animation: ac-float 2.8s 0.9s ease-in-out infinite;
  }
  .floater {
    animation: ac-drift 3.4s ease-in-out infinite alternate;
  }
  .phone[data-run] .floater {
    animation:
      ac-floater-in 0.32s cubic-bezier(0.3, 0.7, 0.3, 1.3) both,
      ac-drift 3.4s ease-in-out infinite alternate;
  }
  .p3Eyes i {
    animation: ac-blink 4.6s 2s infinite;
  }
  .p3Eyes[data-blink] i,
  .p4Eyes[data-blink] i {
    animation:
      ac-blink-once 0.3s ease-in-out,
      ac-blink 4.6s 2.4s infinite;
  }
  .phone[data-run] .chip {
    animation: ac-rise 0.3s 0.58s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .p3Line1 {
    animation: ac-rise 0.32s 0.62s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .p3Line2 {
    animation: ac-rise 0.32s 0.68s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .p3Tags {
    animation: ac-rise 0.3s 0.8s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
  .phone[data-run] .plan {
    animation: ac-plan 0.36s 0.88s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }

  /* 4. The dark assistant. */
  .phone[data-run] .halo {
    animation:
      ac-fade 0.3s linear both,
      ac-morph-halo 7s 0.3s ease-in-out infinite alternate;
  }
  .halo {
    animation: ac-morph-halo 7s ease-in-out infinite alternate;
  }
  .phone[data-run] .sparks i[data-tone='red'] {
    animation: ac-spark-a 0.42s cubic-bezier(0.4, 0, 0.6, 1) both;
  }
  .phone[data-run] .sparks i[data-tone='blue'] {
    animation: ac-spark-b 0.42s cubic-bezier(0.4, 0, 0.6, 1) both;
  }
  .phone[data-run] .sparks i[data-tone='red2'] {
    animation: ac-spark-c 0.42s cubic-bezier(0.4, 0, 0.6, 1) both;
  }
  .phone[data-run] .p4Enter {
    animation: ac-orb4-in 0.42s linear both;
  }
  .p4Blob {
    animation: ac-morph 5s ease-in-out infinite alternate;
  }
  .phone[data-run] .p4Eyes {
    animation: ac-eyes-open 0.3s 1.12s cubic-bezier(0.3, 0.7, 0.35, 1.25) both;
  }
  .p4Eyes i {
    animation: ac-blink 3.8s 2.4s infinite;
  }
  .stars i {
    animation: ac-twinkle 3s ease-in-out infinite;
  }

  .root[data-paused] .screen,
  .root[data-paused] .screen * {
    animation-play-state: paused;
  }
}

@keyframes ac-fade {
  from {
    opacity: 0;
  }
}
@keyframes ac-fade-out {
  to {
    opacity: 0;
  }
}
@keyframes ac-fade-up {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
}
@keyframes ac-day {
  from {
    opacity: 0;
    transform: translateY(4px) scale(0.9);
  }
}
@keyframes ac-sweep {
  from {
    stroke-dasharray: 0 200;
  }
}
@keyframes ac-needle {
  from {
    transform: rotate(0deg);
  }
}
@keyframes ac-digit {
  from {
    opacity: 0;
    color: #b8b8b8;
    transform: translateY(5px) scale(0.92);
  }
  50% {
    opacity: 0.6;
    color: #8a8a8a;
  }
}
@keyframes ac-pill {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}
@keyframes ac-breathe {
  50% {
    opacity: 0.9;
  }
}
@keyframes ac-flicker {
  from {
    transform: scale(0.94, 0.9);
  }
  to {
    transform: scale(1.04, 1.06);
  }
}
@keyframes ac-dot {
  50% {
    transform: scale(1.4);
    opacity: 0.7;
  }
}
@keyframes ac-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.14);
  }
  100% {
    transform: scale(1);
  }
}
@keyframes ac-turn {
  to {
    rotate: 360deg;
  }
}
@keyframes ac-sway {
  from {
    transform: rotate(-24deg) scale(1.04);
  }
  to {
    transform: rotate(28deg) scale(0.98);
  }
}
@keyframes ac-blink {
  0%,
  92%,
  100% {
    transform: scaleY(1);
  }
  95% {
    transform: scaleY(0.12);
  }
}
@keyframes ac-blink-once {
  50% {
    transform: scaleY(0.1);
  }
}
@keyframes ac-orb-in {
  0% {
    opacity: 0;
    transform: translate(4px, 12px) scale(0.24);
    animation-timing-function: ease-out;
  }
  16% {
    opacity: 1;
    transform: translate(3px, 10px) scale(0.4);
    animation-timing-function: linear;
  }
  32% {
    transform: translate(2px, 7px) scale(0.62);
  }
  48% {
    transform: translate(1px, 4px) scale(0.92, 0.84);
    animation-timing-function: ease-out;
  }
  65% {
    transform: translateY(1px) scale(1.12, 0.98);
    animation-timing-function: ease-in-out;
  }
  82% {
    transform: scale(0.99, 1.01);
  }
  100% {
    transform: none;
  }
}
@keyframes ac-shadow-in {
  from {
    opacity: 0;
    transform: scale(0.3);
  }
}
@keyframes ac-shadow {
  50% {
    transform: scale(0.88);
    opacity: 0.8;
  }
}
@keyframes ac-float {
  50% {
    transform: translateY(-6px);
  }
}
@keyframes ac-floater-in {
  from {
    opacity: 0;
    transform: scale(0.4);
  }
}
@keyframes ac-drift {
  from {
    translate: 0 -2px;
  }
  to {
    translate: 0 2.5px;
  }
}
@keyframes ac-rise {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}
@keyframes ac-plan {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
}
@keyframes ac-spark-a {
  from {
    opacity: 1;
    transform: translate(-62px, 26px);
  }
  70% {
    opacity: 0.9;
  }
  to {
    opacity: 0;
    transform: translate(-20px, 8px) scale(0.6);
  }
}
@keyframes ac-spark-b {
  from {
    opacity: 0.8;
    transform: translate(-58px, -24px);
  }
  to {
    opacity: 0;
    transform: translate(-30px, -8px) scale(0.6);
  }
}
@keyframes ac-spark-c {
  from {
    opacity: 1;
    transform: translate(52px, -12px);
  }
  60% {
    opacity: 1;
    transform: translate(46px, -10px);
  }
  to {
    opacity: 0;
    transform: translate(40px, -10px) scale(0.7);
  }
}
@keyframes ac-orb4-in {
  0% {
    transform: scale(0.28);
    opacity: 0.85;
  }
  25% {
    transform: scale(0.34);
    opacity: 1;
    animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
  }
  72% {
    transform: scale(1.03);
    animation-timing-function: ease-out;
  }
  100% {
    transform: none;
  }
}
@keyframes ac-eyes-open {
  from {
    transform: scale(0.26);
  }
}
@keyframes ac-morph {
  to {
    transform: rotate(3deg) scale(1.015, 0.985);
  }
}
@keyframes ac-morph-halo {
  from {
    border-radius: 44% 47% 45% 48% / 47% 44% 49% 45%;
  }
  to {
    border-radius: 48% 43% 49% 44% / 43% 48% 45% 49%;
  }
}
@keyframes ac-twinkle {
  50% {
    opacity: 0.12;
  }
}
```