# Loading cats

> Four game-like loading loops with a black cat in each, all in plain CSS animation.

Four grey tiles that wait with you. A cat bounces on a trampoline, the O of LOAD stretches into a progress bar and then drops out of the word, a pile of cats falls through a trapdoor and tumbles back in, and a bar fills in four steps while cats peek in from the corners. Every loop is plain CSS keyframes.

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

Category: Feedback.

## Anatomy

1. **Stage**: The panel the tiles sit on, near-white or near-black at night. A click anywhere on it pauses or plays every loop; it is a group named by `label`.
2. **Tile**: A grey card on a 400 × 298 grid: everything inside it is placed, sized and timed in 400ths of its width, so it scales as one.
3. **Cat**: One drawing for every cat: a mochi body, pointed ears, a hooked tail, and open, sleeping or star eyes. It squashes and stretches to its box.
4. **Trampoline pad**: Gives under the landing cat: it widens, sags into a bowl and turns see-through over it, then throws it out of the top.
5. **Pill**: The O of LOAD. It stretches into a bar while a shine sweeps it three times, then drops out of the word and falls back in.
6. **Trapdoor**: Two leaves under the pile that swing down to let the cats through, then shut so they can drop back in one by one.
7. **Caption**: One of four words for the bar’s step: under half, from half, from three quarters, done. Set them with `labels`.
8. **Progress bar**: Fills in four steps on its own, or follows `progress` (0 to 1). The fill is a whole pill slid in, so a sliver is a crescent.

## When to use

- A playful wait of a few seconds that fills a screen or a panel: a game level, a first sync, an export.
- Empty or splash moments where a little character makes the wait feel shorter, and the brand can afford a joke.
- A wait with four rough milestones: drive the progress tile with `progress` and its caption and corner cat follow.
- Any size: every tile is drawn on its own 400 × 298 grid, so it looks the same from a thumbnail to a hero.

## When not to use

- Small inline waits, in a button, a field or a table row. Use a plain spinner at text size.
- Exact progress people plan around, like an upload with minutes left. Show a bar with a number and the time.
- Serious or stressful moments (a payment, an error, a medical result): cats bouncing read as not taking it seriously.
- Waits under a second: no loop gets past its first beat, and the tiles flash. Show nothing, or a skeleton.
- Errors and partial results: the tiles only know “still loading”. Say what failed in words.

## Installation

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

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

export function Uploading({ sent, total }: { sent: number; total: number }) {
  return (
    <LoadingCats
      animations={['progress']}
      progress={sent / total}
      labels={['Uploading', 'Halfway', 'Nearly there', 'Uploaded!']}
    />
  );
}
```

## States

- **Still**: With `autoplay={false}`, when paused, or under reduced motion: the cat on its pad, the stretched pill, the full pile and the bar half way.
- **Progress, under half**: `progress={0.25}`: the first caption, and a cat peeks in from the bottom-right corner.
- **Progress, half way**: From 0.5 the second caption, and the cat swings in from the top-left instead.
- **Progress, almost**: From 0.75 the third caption; the cat moves to the bottom-left.
- **Progress, done**: At 1 the last caption, and a cat leans in from all four corners at once.
- **Dark**: Every colour is a light-dark() token: at night light cats on charcoal tiles. `colorScheme` forces one.
- **One tile**: `animations` picks which tiles and in what order; a single tile is centred, at most 480 px wide.

## How it works

- Every tile is drawn on a 400 × 298 grid. A tile is a size container and --u is a 400th of its width, so positions, strokes, type and even the keyframes’ distances scale with the tile; one stylesheet draws it at 140 px or at 440 px.
- Each loop is a set of keyframes written frame by frame at 30 fps, as dense linear steps with eased spans in between: the bounce runs 1.8 s, the word 5.5 s, the pile 3 s and the progress bar 8.8 s, each with a negative delay that starts it mid-loop.
- The cats move with the individual translate, rotate and scale properties, each from its own animation, so a fall (ease-in), a tilt and a squash (scaled about the bottom of the body) keep their own timing on one element.
- The trampoline is a see-through pad drawn over the cat: it widens and sags into a bowl (border-radius with an elliptical bottom) as the cat sinks in, pinches to 199 units, then springs back past its width and settles. The dust is four discs that fly out from under the cat and shrink away.
- LOAD is three drawn capitals and a ring of the same 15.5-unit weight in a centred flex row. Only the O’s slot changes width: wide, it spreads the word into a pill (with an expo ease and an overshoot); at zero, the word closes to LAD while the ring falls out with an ease-in and later drops back in with an ease-out. A 30° slanted band, clipped by the pill’s inside, sweeps it three times.
- The trapdoor’s two leaves turn about hinges just past the tile’s sides, to 60° and back with a small overshoot and a dip as each cat lands. The cats fall through with gravity, wait off the tile, then drop back in one by one, stretched as they fall and squashed as they land.
- Each corner of the progress tile is the bottom-right one mirrored about the tile’s middle (rotate(180deg), scaleX(-1), scaleY(-1)), so one swing, round a point past the tile’s right side, serves all four. The fill is a whole pill slid in from the left, so a sliver of it reads as a crescent.
- With progress set, the progress tile leaves its loop: the fill, the caption and the corner cat follow the value with transitions. Without it, under reduced motion or with the loops paused, every tile falls back to its base styles, which are the stills.
- The cat is one SVG on a 150 × 104 grid (ears and tail first, so the body covers their roots), stretched to each box with preserveAspectRatio none; the pile’s loafing cats are just flatter boxes. Its colour is currentColor, its eyes and inner ears a token, so the grey cats and the night scheme cost nothing.
- Every colour is a light-dark() token on the root: by day near-black cats on light-grey tiles, at night light cats with dark eyes on charcoal tiles.

## API reference

- **animations** (`('bounce' | 'word' | 'pile' | 'progress')[]`, default `all four`): Which tiles to show, in this order. One tile is drawn alone, up to 480 px wide.
- **labels** (`[string, string, string, string]`, default `['Loading', 'Halfway', 'Almost', 'Done!']`): The progress tile’s captions: under half, from half, from three quarters, and done.
- **progress** (`number`): Drives the progress tile, from 0 to 1, instead of its loop; its caption and corner cat follow the value.
- **label** (`string`, default `'Loading'`): What the group of tiles is called for screen readers.
- **onPausedChange** (`(paused: boolean) => void`): Called when the reader pauses (true) or plays (false) the loops, by a click or the button.
- **autoplay** (`boolean`, default `true`): Play the loops while the tiles are on screen. Without it every tile shows its still: the cat on its pad, the stretched pill, the full pile and the bar half way.
- **colorScheme** (`'light' | 'dark' | 'system'`): Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
- **className** (`string`): Extra class for the root.

## Accessibility

- The tiles are a group named by label. The bounce, word and pile tiles are images with a short description; the drawings inside them are aria-hidden.
- The progress tile is a progressbar named by the first caption. With progress set it reports its value (0–100) and a value text with the current caption; without it, it is an indeterminate progressbar.
- A Pause animations toggle button (aria-pressed) sits in the stage’s corner: it shows on hover and on keyboard focus, with a focus ring. Clicking or tapping any tile toggles the loops too, so the motion can always be stopped.
- The loops pause off screen and in a hidden tab. Under reduced motion they never play and the toggle is hidden: every tile shows its still, and a value-driven bar jumps without transitions.

## Source

### loading-cats/loading-cats.tsx

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

'use client';

import { useState, type CSSProperties } from 'react';
import { Cat, Letter, type Eyes } from './cat';
import { useAutoplay } from './use-autoplay';
import styles from './loading-cats.module.css';

/*
 * Loading cats: four game-like loading loops in grey tiles, every one of them plain CSS
 * animation.
 *
 * - Bounce: a cat drops onto a pad that gives like a trampoline (it widens, sags and turns
 *   see-through over the cat), springs off it in a puff of dust and sails out of the top.
 * - Word: the O of LOAD stretches into a pill and a shine sweeps it three times; it shrinks back,
 *   drops out of the word (L AD, LAD) and falls back in from above.
 * - Pile: a heap of cats sits on a two-leaf trapdoor; the leaves swing down, the cats fall through,
 *   the leaves swing shut, and the cats drop back in one by one, squashing as they land.
 * - Progress: a bar fills in four steps, each with its caption; at each step a cat swings in from
 *   another corner, and at the last one, from all four.
 *
 * Every tile is drawn on a 400 × 298 grid: --u is one unit, a 400th of the tile's width (a
 * container query), so a tile looks the same at any size. The loops are keyframes timed frame
 * by frame at 30 fps; the cats move by the individual translate, rotate and scale properties, so a
 * fall, a tilt and a squash can each keep their own timing.
 *
 * While part of it is on screen the loops play; off screen or in a hidden tab they pause where they
 * are. With autoplay off, when the reader pauses them, or under reduced motion, every tile shows a
 * still: the cat on its pad, the stretched pill, the full pile, and the bar half way.
 */

export type LoadingCatsAnimation = 'bounce' | 'word' | 'pile' | 'progress';

const ALL: LoadingCatsAnimation[] = ['bounce', 'word', 'pile', 'progress'];
const LABELS: [string, string, string, string] = ['Loading', 'Halfway', 'Almost', 'Done!'];

/** n units of the tile's grid, as CSS. */
const u = (n: number) => `calc(var(--u) * ${n})`;

/** A box on the tile's grid, and the point (in the box's own units) it turns and squashes about. */
function box(
  left: number,
  top: number,
  width: number,
  height: number,
  origin?: [number, number],
): CSSProperties {
  return {
    left: u(left),
    top: u(top),
    width: u(width),
    height: u(height),
    transformOrigin: origin ? `${u(origin[0])} ${u(origin[1])}` : undefined,
  };
}

/** The step of the bar a progress belongs to: its caption and its corner cat. */
const stageOf = (progress: number) =>
  progress >= 1 ? 3 : progress >= 0.75 ? 2 : progress >= 0.5 ? 1 : 0;

function BounceTile() {
  return (
    <div
      className={`${styles.tile} ${styles.bounce}`}
      data-part="tile"
      data-animation="bounce"
      role="img"
      aria-label="A cat bouncing on a trampoline"
    >
      <span className={`${styles.puff} ${styles.puffBigLeft}`} />
      <span className={`${styles.puff} ${styles.puffBigRight}`} />
      <span className={`${styles.puff} ${styles.puffSmall} ${styles.puffSmallLeft}`} />
      <span className={`${styles.puff} ${styles.puffSmall} ${styles.puffSmallRight}`} />
      {/* The body's bottom middle, (64, 102), rests on the pad's top at y 243. */}
      <Cat
        className={`${styles.cat} ${styles.bounceCat}`}
        style={box(124, 141, 150, 104, [64, 102])}
      />
      <span className={styles.pad} data-part="pad" />
    </div>
  );
}

function WordTile() {
  return (
    <div
      className={`${styles.tile} ${styles.word}`}
      data-part="tile"
      data-animation="word"
      role="img"
      aria-label="The word LOAD, its O stretching into a progress bar"
    >
      <div className={styles.wordRow}>
        <Letter letter="L" className={styles.letterL} />
        <span className={styles.slot}>
          <span className={styles.ring} data-part="pill">
            <span className={styles.band} />
          </span>
        </span>
        <Letter letter="A" className={styles.letterA} />
        <Letter letter="D" />
      </div>
    </div>
  );
}

/*
 * The pile, back to front: the sleeping cat on top, the left cat, the front cat, and the cat on
 * its side against the right wall. Each box is the 150 × 104 drawing stretched to that cat's
 * proportions; the origin is the bottom middle of its body, where it squashes on landing.
 */
const PILE: { eyes: Eyes; tone: 'ink' | 'soft'; name: string; style: CSSProperties }[] = [
  {
    eyes: 'sleep',
    tone: 'ink',
    name: 'pileD',
    style: box(73.3, 23.4, 177.5, 107.7, [75.7, 105.7]),
  },
  { eyes: 'star', tone: 'soft', name: 'pileA', style: box(3.4, 108.8, 171, 105, [73.1, 103]) },
  { eyes: 'open', tone: 'ink', name: 'pileB', style: box(143, 94, 181.5, 119.6, [77.4, 117.3]) },
  {
    eyes: 'star',
    tone: 'soft',
    name: 'pileC',
    style: { ...box(269.3, 74.7, 177.5, 111.3, [75.7, 64.3]), transform: 'rotate(-84deg)' },
  },
];

function PileTile() {
  return (
    <div
      className={`${styles.tile} ${styles.pile}`}
      data-part="tile"
      data-animation="pile"
      role="img"
      aria-label="A pile of cats dropping through a trapdoor and back"
    >
      <span className={`${styles.leaf} ${styles.leafLeft}`} data-part="leaf" />
      <span className={`${styles.leaf} ${styles.leafRight}`} data-part="leaf" />
      {PILE.map(({ eyes, tone, name, style }) => (
        <Cat
          key={name}
          eyes={eyes}
          className={`${styles.cat} ${styles[name]} ${tone === 'soft' ? styles.soft : ''}`}
          style={style}
        />
      ))}
    </div>
  );
}

const CORNERS = ['br', 'tl', 'bl', 'tr'] as const;

function ProgressTile({ labels, progress }: { labels: readonly string[]; progress?: number }) {
  const controlled = progress != null;
  // Uncontrolled, the loop plays; its still (no motion, or paused) is the bar half way.
  const value = controlled ? Math.min(1, Math.max(0, progress)) : 0.5;
  const stage = stageOf(value);
  return (
    <div
      className={`${styles.tile} ${styles.progress}`}
      data-part="tile"
      data-animation="progress"
      role="progressbar"
      aria-label={labels[0]}
      aria-valuemin={controlled ? 0 : undefined}
      aria-valuemax={controlled ? 100 : undefined}
      aria-valuenow={controlled ? Math.round(value * 100) : undefined}
      aria-valuetext={controlled ? `${labels[stage]} ${Math.round(value * 100)}%` : undefined}
      data-mode={controlled ? 'value' : 'loop'}
      data-stage={stage}
      style={{ '--p': value } as CSSProperties}
    >
      {CORNERS.map((corner) => (
        <span
          key={corner}
          // The bottom-right corner is the unmirrored one: it has no class of its own.
          className={[styles.corner, styles[corner]].filter(Boolean).join(' ')}
          aria-hidden="true"
        >
          <span className={`${styles.swing} ${styles[`swing_${corner}`]}`}>
            {/* A round head tilted 12° towards its corner, the eyes' midpoint at (327, 237). */}
            <Cat
              round
              className={styles.peek}
              style={box(263.5, 182.4, 157.5, 113.4, [63.5, 54.6])}
            />
          </span>
        </span>
      ))}
      <span className={styles.steps} aria-hidden="true">
        {labels.slice(0, 4).map((text, index) => (
          <span
            key={index}
            className={`${styles.label} ${styles[`label${index}`]}`}
            data-part="caption"
          >
            {text}
          </span>
        ))}
      </span>
      <span className={styles.track} aria-hidden="true" data-part="track">
        <span className={styles.fill} data-part="fill" />
      </span>
    </div>
  );
}

export function LoadingCats({
  animations = ALL,
  labels = LABELS,
  progress,
  label = 'Loading',
  autoplay = true,
  colorScheme,
  className = '',
  onPausedChange,
}: {
  /** Which tiles to show, in this order. */
  animations?: LoadingCatsAnimation[];
  /** The progress tile's four captions: under half, from half, from three quarters, and done. */
  labels?: readonly [string, string, string, string];
  /** Drives the progress tile (0 to 1) instead of its loop; its caption and corner cat follow. */
  progress?: number;
  /** What the group of tiles is called for screen readers. */
  label?: string;
  /** Play the loops while on screen; without it every tile shows its still. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  className?: string;
  /** Called when the reader pauses (true) or plays (false) the loops, by a click or the button. */
  onPausedChange?: (paused: boolean) => void;
}) {
  const { ref, playing } = useAutoplay<HTMLDivElement>();
  /** The reader's own play or pause, which outlasts the autoplay prop until it changes. */
  const [chosen, setChosen] = useState<{ on: boolean; autoplay: boolean } | null>(null);
  const on = chosen && chosen.autoplay === autoplay ? chosen.on : autoplay;
  const toggle = () => {
    setChosen({ on: !on, autoplay });
    onPausedChange?.(on);
  };
  const run = !on ? 'still' : playing ? 'play' : 'pause';
  const shown = ALL.filter((name) => animations.includes(name)).sort(
    (a, b) => animations.indexOf(a) - animations.indexOf(b),
  );

  return (
    <div
      ref={ref}
      className={`${styles.root} ${className}`}
      data-part="stage"
      data-color-scheme={colorScheme}
    >
      <div
        className={styles.grid}
        role="group"
        aria-label={label}
        data-run={run}
        data-count={shown.length}
        onClick={toggle}
      >
        {shown.map((name) =>
          name === 'bounce' ? (
            <BounceTile key={name} />
          ) : name === 'word' ? (
            <WordTile key={name} />
          ) : name === 'pile' ? (
            <PileTile key={name} />
          ) : (
            <ProgressTile key={name} labels={labels} progress={progress} />
          ),
        )}
      </div>
      <button
        type="button"
        className={styles.toggle}
        data-part="pause"
        aria-pressed={!on}
        onClick={toggle}
      >
        <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
          {on ? <path d="M4 3h3v10H4zM9 3h3v10H9z" /> : <path d="M5 2.5v11l9-5.5z" />}
        </svg>
        <span className={styles.hidden}>Pause animations</span>
      </button>
    </div>
  );
}
```

### loading-cats/loading-cats.module.css

```css
/* Loading cats (loading-cats.tsx): four grey tiles in a 2 × 2 grid on a near-white stage, or at
   night light cats on charcoal tiles.

   Every tile is drawn on a 400 × 298 grid. A tile is a size container, and --u is a 400th of its
   width, so everything inside it (positions, strokes, type, the keyframes' distances) scales with
   the tile. Positions and boxes are set inline in units by the component; this file holds the
   looks and the motion.

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

   Motion. The keyframes at the end are timed frame by frame (30 fps) and written as dense
   linear steps with eased spans in between. They only run under prefers-reduced-motion:
   no-preference, and only while the grid's data-run is play (or pause, which freezes them where
   they are, off screen or in a hidden tab). With data-run still, every element falls back to its
   base style here, which is the tile's still.

   Memory. Each tile clips and contains its own layout and paint, so the pad's and the pill's
   width changes never lay out the page; the cats, puffs and leaves move by transforms. */

.root {
  /* The stage, and the tiles on it. */
  --page: #fdfdfd;
  --tile: #f2f2f2;
  /* The dark cats, the letters, the bar's fill and the captions; the grey cats. */
  --ink: #494949;
  --ink-soft: #696969;
  /* Eyes and the inside of ears: the page seen through the cat. */
  --cat-eye: #fdfdfd;
  /* The trampoline pad, drawn see-through over the cat; the dust. */
  --pad: #bebebe;
  --puff: #d3d3d3;
  /* The shine in the pill; the trapdoor's leaves; the bar's track. */
  --band: #d6d6d6;
  --leaf: #8c8c8c;
  --track: #dedede;
  /* The stage's edge; the pause button and its focus ring. */
  --line: rgb(0 0 0 / 0.06);
  --button: rgb(255 255 255 / 0.9);
  --button-ink: #494949;
  --focus: #387bff;
  position: relative;
  container-type: inline-size;
  width: 100%;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  background: var(--page);
  box-shadow: inset 0 0 0 1px var(--line);
  -webkit-tap-highlight-color: transparent;
}
@supports (color: light-dark(#fff, #000)) {
  .root {
    --page: light-dark(#fdfdfd, #141414);
    --tile: light-dark(#f2f2f2, #232323);
    --ink: light-dark(#494949, #e2e2e2);
    --ink-soft: light-dark(#696969, #9d9d9d);
    --cat-eye: light-dark(#fdfdfd, #1e1e1e);
    --pad: light-dark(#bebebe, #5a5a5a);
    --puff: light-dark(#d3d3d3, #3c3c3c);
    --band: light-dark(#d6d6d6, #4a4a4a);
    --leaf: light-dark(#8c8c8c, #6b6b6b);
    --track: light-dark(#dedede, #3a3a3a);
    --line: light-dark(rgb(0 0 0 / 0.06), rgb(255 255 255 / 0.06));
    --button: light-dark(rgb(255 255 255 / 0.9), rgb(40 40 40 / 0.9));
    --button-ink: light-dark(#494949, #e2e2e2);
  }
}
/* 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;
}

/* Two columns, 38 units apart (9.5% of a tile); one column on a narrow stage, or for one tile. */
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4.3cqw;
  padding: 4.3cqw;
  cursor: pointer;
}
.grid[data-count='1'] {
  grid-template-columns: 1fr;
  max-width: 480px;
  margin-inline: auto;
}
@container (max-width: 520px) {
  .grid {
    grid-template-columns: 1fr;
    gap: 5cqw;
    padding: 5cqw;
  }
}

.tile {
  --u: calc(100cqw / 400);
  position: relative;
  container-type: inline-size;
  contain: layout paint;
  aspect-ratio: 400 / 298;
  overflow: hidden;
  overflow: clip;
  /* 43 units round, on both axes. */
  border-radius: 10.75% / 14.43%;
  background: var(--tile);
  color: var(--ink);
}
.tile > * {
  position: absolute;
}

/* ----- Bounce ----- */

.cat {
  position: absolute;
  overflow: visible;
}
.soft {
  color: var(--ink-soft);
}
.pad {
  left: calc(var(--u) * 198);
  top: calc(var(--u) * 243);
  width: calc(var(--u) * 279);
  height: calc(var(--u) * 13);
  translate: -50% 0;
  /* A flat top with rounded ends, sagging into a bowl as it deepens. */
  border-radius: calc(var(--u) * 7) calc(var(--u) * 7) 50% 50% / calc(var(--u) * 7)
    calc(var(--u) * 7) 100% 100%;
  background: var(--pad);
  opacity: 0.73;
}
.puff {
  left: 0;
  top: 0;
  width: calc(var(--u) * 44);
  height: calc(var(--u) * 44);
  margin: calc(var(--u) * -22) 0 0 calc(var(--u) * -22);
  border-radius: 50%;
  background: var(--puff);
  scale: 0;
}
.puffSmall {
  width: calc(var(--u) * 20);
  height: calc(var(--u) * 20);
  margin: calc(var(--u) * -10) 0 0 calc(var(--u) * -10);
}

/* ----- Word ----- */

/* L, the O's slot, A and D in a centred row; the slot's width spreads or closes the word. */
.wordRow {
  left: 0;
  right: 0;
  top: calc(var(--u) * 119);
  height: calc(var(--u) * 66);
  display: flex;
  justify-content: center;
  translate: calc(var(--u) * -2.5) 0;
}
.wordRow > svg {
  flex: none;
  height: 100%;
  overflow: visible;
}
.letterL {
  margin-right: calc(var(--u) * 4);
}
.letterA {
  margin-right: calc(var(--u) * 8);
}
.slot {
  position: relative;
  flex: none;
  width: calc(var(--u) * 212);
  height: 100%;
}
/* The O: a ring as tall as the capitals, the pill when stretched. */
.ring {
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(var(--u) * 212);
  height: 100%;
  transform: translateX(-50%);
  box-sizing: border-box;
  border: calc(var(--u) * 15.5) solid currentColor;
  border-radius: calc(var(--u) * 33);
  overflow: hidden;
}
/* The shine: a band slanted 30°, clipped by the pill's inside. */
.band {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: calc(var(--u) * 83);
  transform: skewX(-30deg);
  background: var(--band);
  translate: calc(var(--u) * -100) 0;
}

/* ----- Pile ----- */

/* The trapdoor's two leaves, hinged just past the tile's sides. */
.leaf {
  top: calc(var(--u) * 217);
  height: calc(var(--u) * 27);
  border-radius: calc(var(--u) * 13.5);
  background: var(--leaf);
}
.leafLeft {
  left: calc(var(--u) * -20);
  width: calc(var(--u) * 213);
  transform-origin: calc(var(--u) * 20) 50%;
}
.leafRight {
  left: calc(var(--u) * 203);
  width: calc(var(--u) * 217);
  transform-origin: calc(var(--u) * 197) 50%;
}

/* ----- Progress ----- */

/* Each corner is the bottom-right one, mirrored about the tile's middle. Its cat swings in and out
   up from below the tile, round a point past its right side, (470, 280); out of sight at -60°. */
.corner {
  inset: 0;
}
.tl {
  transform: rotate(180deg);
}
.bl {
  transform: scaleX(-1);
}
.tr {
  transform: scaleY(-1);
}
.swing {
  position: absolute;
  inset: 0;
  transform-origin: calc(var(--u) * 470) calc(var(--u) * 280);
  rotate: -60deg;
}
.peek {
  position: absolute;
  overflow: visible;
  transform: rotate(-12deg);
}
.progress[data-stage='0'] .swing_br,
.progress[data-stage='1'] .swing_tl,
.progress[data-stage='2'] .swing_bl,
.progress[data-stage='3'] .swing {
  rotate: 0deg;
}

.steps {
  left: 0;
  right: 0;
  top: calc(var(--u) * 116);
  height: calc(var(--u) * 26);
  translate: calc(var(--u) * -4) 0;
}
.label {
  position: absolute;
  inset: 0;
  font:
    700 calc(var(--u) * 21) / calc(var(--u) * 26) system-ui,
    sans-serif;
  letter-spacing: 0.01em;
  text-align: center;
  white-space: nowrap;
  opacity: 0;
}
.progress[data-stage='0'] .label0,
.progress[data-stage='1'] .label1,
.progress[data-stage='2'] .label2,
.progress[data-stage='3'] .label3 {
  opacity: 1;
}
.track {
  left: calc(var(--u) * 140);
  top: calc(var(--u) * 156);
  width: calc(var(--u) * 112);
  height: calc(var(--u) * 21);
  border-radius: calc(var(--u) * 10.5);
  overflow: hidden;
  background: var(--track);
}
/* The fill is a whole pill slid in from the left, so a sliver of it is a crescent. */
.fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  translate: calc((var(--p) - 1) * 100%) 0;
}

/* ----- The pause button: in the stage's corner, shown on hover and on keyboard focus. ----- */

.toggle {
  position: absolute;
  top: 8px;
  right: 8px;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--button);
  color: var(--button-ink);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.12);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.2s;
}
.toggle svg {
  width: 14px;
  height: 14px;
  fill: currentColor;
}
.root:hover .toggle,
.toggle:focus-visible {
  opacity: 1;
}
.toggle:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
@media (hover: none) {
  .toggle {
    opacity: 0.85;
  }
}
@media (prefers-reduced-motion: reduce) {
  .toggle {
    display: none;
  }
}
.hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

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

@media (prefers-reduced-motion: no-preference) {
  /* A value-driven progress tile moves by transitions, from one step to the next. */
  .progress[data-mode='value'] .fill {
    transition: translate 0.6s cubic-bezier(0.33, 0.1, 0.3, 1);
  }
  .progress[data-mode='value'] .label {
    transition: opacity 0.35s;
  }
  .progress[data-mode='value'] .swing {
    transition: rotate 0.5s cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  .progress[data-mode='value'][data-stage='0'] .swing_br,
  .progress[data-mode='value'][data-stage='1'] .swing_tl,
  .progress[data-mode='value'][data-stage='2'] .swing_bl,
  .progress[data-mode='value'][data-stage='3'] .swing {
    transition: rotate 0.6s cubic-bezier(0.3, 1.45, 0.55, 1);
  }

  /* The loops. Each tile keeps one clock: its period, and a delay that starts it mid-loop. */
  .grid:not([data-run='still']) .bounce {
    --period: 1.8s;
    --delay: -0.133s;
  }
  .grid:not([data-run='still']) .word {
    --period: 5.5s;
    --delay: -0.033s;
  }
  .grid:not([data-run='still']) .pile {
    --period: 3s;
    --delay: -2.767s;
  }
  .grid:not([data-run='still']) .progress {
    --period: 8.8s;
    --delay: -8.3s;
  }
  .grid:not([data-run='still']) .tile * {
    animation-duration: var(--period);
    animation-delay: var(--delay);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
  }
  .grid:not([data-run='still']) .bounceCat {
    animation-name: bounceCatKeys, bounceSquashKeys;
  }
  .grid:not([data-run='still']) .pad {
    animation-name: bouncePadKeys;
  }
  .grid:not([data-run='still']) .puffBigLeft {
    animation-name: puffBigLeftKeys;
  }
  .grid:not([data-run='still']) .puffBigRight {
    animation-name: puffBigRightKeys;
  }
  .grid:not([data-run='still']) .puffSmallLeft {
    animation-name: puffSmallLeftKeys;
  }
  .grid:not([data-run='still']) .puffSmallRight {
    animation-name: puffSmallRightKeys;
  }
  .grid:not([data-run='still']) .slot {
    animation-name: wordSlotKeys;
  }
  .grid:not([data-run='still']) .ring {
    animation-name: wordRingKeys, wordDropKeys;
  }
  .grid:not([data-run='still']) .band {
    animation-name: wordBandKeys;
  }
  .grid:not([data-run='still']) .leafLeft {
    animation-name: pileFloorLeftKeys;
  }
  .grid:not([data-run='still']) .leafRight {
    animation-name: pileFloorRightKeys;
  }
  .grid:not([data-run='still']) .pileA {
    animation-name: pileAKeys, pileASquashKeys;
  }
  .grid:not([data-run='still']) .pileB {
    animation-name: pileBKeys, pileBSquashKeys;
  }
  .grid:not([data-run='still']) .pileC {
    animation-name: pileCKeys;
  }
  .grid:not([data-run='still']) .pileD {
    animation-name: pileDKeys, pileDSquashKeys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .fill {
    animation-name: progressFillKeys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .label0 {
    animation-name: progressCaption0Keys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .label1 {
    animation-name: progressCaption1Keys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .label2 {
    animation-name: progressCaption2Keys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .label3 {
    animation-name: progressCaption3Keys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .swing_br {
    animation-name: cornerBRKeys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .swing_tl {
    animation-name: cornerTLKeys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .swing_bl {
    animation-name: cornerBLKeys;
  }
  .grid:not([data-run='still']) .progress[data-mode='loop'] .swing_tr {
    animation-name: cornerTRKeys;
  }
  .grid[data-run='pause'] .tile * {
    animation-play-state: paused;
  }
}

@keyframes bounceCatKeys {
  0% {
    translate: 0 calc(var(--u) * -151);
    animation-timing-function: cubic-bezier(0.32, 0, 0.67, 0);
  }
  29.63% {
    translate: 0 calc(var(--u) * 31);
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  38.889% {
    translate: 0 calc(var(--u) * 41);
  }
  42.593% {
    translate: 0 calc(var(--u) * 41);
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  48.148% {
    translate: 0 calc(var(--u) * 31);
  }
  51.852% {
    translate: 0 calc(var(--u) * 27);
  }
  55.556% {
    translate: 0 calc(var(--u) * 27);
  }
  57.407% {
    translate: 0 calc(var(--u) * -1);
  }
  59.259% {
    translate: 0 calc(var(--u) * -23);
  }
  61.111% {
    translate: 0 calc(var(--u) * -38);
  }
  62.963% {
    translate: 0 calc(var(--u) * -51);
  }
  64.815% {
    translate: 0 calc(var(--u) * -62);
  }
  66.667% {
    translate: 0 calc(var(--u) * -72);
  }
  68.519% {
    translate: 0 calc(var(--u) * -80);
  }
  70.37% {
    translate: 0 calc(var(--u) * -88);
  }
  72.222% {
    translate: 0 calc(var(--u) * -96);
  }
  74.074% {
    translate: 0 calc(var(--u) * -102);
  }
  75.926% {
    translate: 0 calc(var(--u) * -109);
  }
  77.778% {
    translate: 0 calc(var(--u) * -114);
  }
  79.63% {
    translate: 0 calc(var(--u) * -120);
  }
  81.481% {
    translate: 0 calc(var(--u) * -125);
  }
  83.333% {
    translate: 0 calc(var(--u) * -129);
  }
  85.185% {
    translate: 0 calc(var(--u) * -133);
  }
  87.037% {
    translate: 0 calc(var(--u) * -137);
  }
  88.889% {
    translate: 0 calc(var(--u) * -140);
  }
  90.741% {
    translate: 0 calc(var(--u) * -143);
  }
  92.593% {
    translate: 0 calc(var(--u) * -145);
  }
  94.444% {
    translate: 0 calc(var(--u) * -147);
  }
  96.296% {
    translate: 0 calc(var(--u) * -149);
  }
  98.148% {
    translate: 0 calc(var(--u) * -150);
  }
  100% {
    translate: 0 calc(var(--u) * -151);
  }
}

@keyframes bounceSquashKeys {
  0% {
    scale: 1 1;
  }
  22.222% {
    scale: 1 1;
  }
  27.778% {
    scale: 0.96 1.05;
  }
  29.63% {
    scale: 1.02 0.97;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  38.889% {
    scale: 1.15 0.9;
  }
  42.593% {
    scale: 1.14 0.9;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  48.148% {
    scale: 1.05 0.97;
  }
  51.852% {
    scale: 1 1;
  }
  55.556% {
    scale: 1.03 0.97;
  }
  57.407% {
    scale: 0.96 1.04;
  }
  62.963% {
    scale: 0.96 1.05;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  72.222% {
    scale: 1 1;
  }
  100% {
    scale: 1 1;
  }
}

@keyframes bouncePadKeys {
  0% {
    width: calc(var(--u) * 279);
    height: calc(var(--u) * 13);
    opacity: 0.73;
  }
  25.926% {
    width: calc(var(--u) * 279);
    height: calc(var(--u) * 13);
    opacity: 0.73;
  }
  27.778% {
    width: calc(var(--u) * 287);
    height: calc(var(--u) * 18);
    opacity: 0.66;
  }
  29.63% {
    width: calc(var(--u) * 295);
    height: calc(var(--u) * 32);
    opacity: 0.5;
  }
  31.481% {
    width: calc(var(--u) * 307);
    height: calc(var(--u) * 35);
  }
  33.333% {
    width: calc(var(--u) * 319);
    height: calc(var(--u) * 38);
  }
  35.185% {
    width: calc(var(--u) * 331);
    height: calc(var(--u) * 41);
  }
  37.037% {
    width: calc(var(--u) * 342);
    height: calc(var(--u) * 46);
  }
  38.889% {
    width: calc(var(--u) * 349);
    height: calc(var(--u) * 51);
    opacity: 0.47;
  }
  40.741% {
    width: calc(var(--u) * 355);
    height: calc(var(--u) * 51);
  }
  42.593% {
    width: calc(var(--u) * 357);
    height: calc(var(--u) * 51);
  }
  44.444% {
    width: calc(var(--u) * 355);
    height: calc(var(--u) * 51);
  }
  46.296% {
    width: calc(var(--u) * 349);
    height: calc(var(--u) * 51);
  }
  48.148% {
    width: calc(var(--u) * 337);
    height: calc(var(--u) * 49);
  }
  50% {
    width: calc(var(--u) * 319);
    height: calc(var(--u) * 44);
  }
  51.852% {
    width: calc(var(--u) * 307);
    height: calc(var(--u) * 42);
    opacity: 0.5;
  }
  53.704% {
    width: calc(var(--u) * 271);
    height: calc(var(--u) * 32);
    opacity: 0.56;
  }
  55.556% {
    width: calc(var(--u) * 246);
    height: calc(var(--u) * 26);
  }
  57.407% {
    width: calc(var(--u) * 227);
    height: calc(var(--u) * 20);
  }
  59.259% {
    width: calc(var(--u) * 211);
    height: calc(var(--u) * 17);
  }
  61.111% {
    width: calc(var(--u) * 203);
    height: calc(var(--u) * 14);
    opacity: 0.62;
  }
  62.963% {
    width: calc(var(--u) * 199);
    height: calc(var(--u) * 13);
  }
  64.815% {
    width: calc(var(--u) * 203);
    height: calc(var(--u) * 13);
  }
  66.667% {
    width: calc(var(--u) * 215);
    height: calc(var(--u) * 13);
  }
  68.519% {
    width: calc(var(--u) * 236);
    height: calc(var(--u) * 13);
  }
  70.37% {
    width: calc(var(--u) * 263);
    height: calc(var(--u) * 13);
  }
  72.222% {
    width: calc(var(--u) * 286);
    height: calc(var(--u) * 13);
    opacity: 0.73;
  }
  74.074% {
    width: calc(var(--u) * 303);
    height: calc(var(--u) * 13);
  }
  75.926% {
    width: calc(var(--u) * 310);
    height: calc(var(--u) * 13);
  }
  77.778% {
    width: calc(var(--u) * 309);
    height: calc(var(--u) * 13);
  }
  79.63% {
    width: calc(var(--u) * 307);
    height: calc(var(--u) * 13);
  }
  81.481% {
    width: calc(var(--u) * 301);
    height: calc(var(--u) * 13);
  }
  83.333% {
    width: calc(var(--u) * 293);
    height: calc(var(--u) * 13);
  }
  85.185% {
    width: calc(var(--u) * 287);
    height: calc(var(--u) * 13);
  }
  87.037% {
    width: calc(var(--u) * 281);
    height: calc(var(--u) * 13);
  }
  88.889% {
    width: calc(var(--u) * 279);
    height: calc(var(--u) * 13);
  }
  100% {
    width: calc(var(--u) * 279);
    height: calc(var(--u) * 13);
    opacity: 0.73;
  }
}

@keyframes puffBigLeftKeys {
  0% {
    translate: calc(var(--u) * 176) calc(var(--u) * 226);
    scale: 0;
  }
  58.333% {
    translate: calc(var(--u) * 176) calc(var(--u) * 226);
    scale: 0;
  }
  59.259% {
    translate: calc(var(--u) * 177) calc(var(--u) * 228);
    scale: 0.6;
  }
  61.111% {
    translate: calc(var(--u) * 170) calc(var(--u) * 218);
    scale: 1;
  }
  62.963% {
    translate: calc(var(--u) * 155) calc(var(--u) * 208);
    scale: 1;
  }
  64.815% {
    translate: calc(var(--u) * 105) calc(var(--u) * 166);
    scale: 1;
  }
  66.667% {
    translate: calc(var(--u) * 90) calc(var(--u) * 152);
    scale: 1;
  }
  68.519% {
    translate: calc(var(--u) * 82) calc(var(--u) * 147);
    scale: 1;
  }
  70.37% {
    translate: calc(var(--u) * 78) calc(var(--u) * 142);
    scale: 1;
  }
  72.222% {
    translate: calc(var(--u) * 75) calc(var(--u) * 140);
    scale: 1;
  }
  74.074% {
    translate: calc(var(--u) * 72) calc(var(--u) * 137);
    scale: 1;
  }
  77.778% {
    translate: calc(var(--u) * 68) calc(var(--u) * 134);
    scale: 1;
  }
  79.63% {
    translate: calc(var(--u) * 66) calc(var(--u) * 132);
    scale: 1;
  }
  81.481% {
    translate: calc(var(--u) * 64) calc(var(--u) * 130);
    scale: 0.77;
  }
  83.333% {
    translate: calc(var(--u) * 63) calc(var(--u) * 128);
    scale: 0.55;
  }
  85.185% {
    translate: calc(var(--u) * 63) calc(var(--u) * 128);
    scale: 0.23;
  }
  87.037% {
    translate: calc(var(--u) * 63) calc(var(--u) * 128);
    scale: 0.11;
  }
  88.889% {
    translate: calc(var(--u) * 63) calc(var(--u) * 128);
    scale: 0;
  }
  100% {
    translate: calc(var(--u) * 63) calc(var(--u) * 128);
    scale: 0;
  }
}

@keyframes puffBigRightKeys {
  0% {
    translate: calc(var(--u) * 220) calc(var(--u) * 226);
    scale: 0;
  }
  58.333% {
    translate: calc(var(--u) * 220) calc(var(--u) * 226);
    scale: 0;
  }
  59.259% {
    translate: calc(var(--u) * 219) calc(var(--u) * 228);
    scale: 0.6;
  }
  61.111% {
    translate: calc(var(--u) * 226) calc(var(--u) * 218);
    scale: 1;
  }
  62.963% {
    translate: calc(var(--u) * 241) calc(var(--u) * 208);
    scale: 1;
  }
  64.815% {
    translate: calc(var(--u) * 291) calc(var(--u) * 166);
    scale: 1;
  }
  66.667% {
    translate: calc(var(--u) * 306) calc(var(--u) * 152);
    scale: 1;
  }
  68.519% {
    translate: calc(var(--u) * 314) calc(var(--u) * 147);
    scale: 1;
  }
  70.37% {
    translate: calc(var(--u) * 318) calc(var(--u) * 142);
    scale: 1;
  }
  72.222% {
    translate: calc(var(--u) * 321) calc(var(--u) * 140);
    scale: 1;
  }
  74.074% {
    translate: calc(var(--u) * 324) calc(var(--u) * 137);
    scale: 1;
  }
  77.778% {
    translate: calc(var(--u) * 328) calc(var(--u) * 134);
    scale: 1;
  }
  79.63% {
    translate: calc(var(--u) * 330) calc(var(--u) * 132);
    scale: 1;
  }
  81.481% {
    translate: calc(var(--u) * 332) calc(var(--u) * 130);
    scale: 0.77;
  }
  83.333% {
    translate: calc(var(--u) * 333) calc(var(--u) * 128);
    scale: 0.55;
  }
  85.185% {
    translate: calc(var(--u) * 333) calc(var(--u) * 128);
    scale: 0.23;
  }
  87.037% {
    translate: calc(var(--u) * 333) calc(var(--u) * 128);
    scale: 0.11;
  }
  88.889% {
    translate: calc(var(--u) * 333) calc(var(--u) * 128);
    scale: 0;
  }
  100% {
    translate: calc(var(--u) * 333) calc(var(--u) * 128);
    scale: 0;
  }
}

@keyframes puffSmallLeftKeys {
  0% {
    translate: calc(var(--u) * 188) calc(var(--u) * 240);
    scale: 0;
  }
  58.333% {
    translate: calc(var(--u) * 188) calc(var(--u) * 240);
    scale: 0;
  }
  59.259% {
    translate: calc(var(--u) * 188) calc(var(--u) * 240);
    scale: 0.7;
  }
  64.815% {
    translate: calc(var(--u) * 178) calc(var(--u) * 233);
    scale: 1;
  }
  66.667% {
    translate: calc(var(--u) * 160) calc(var(--u) * 227);
    scale: 1;
  }
  68.519% {
    translate: calc(var(--u) * 141) calc(var(--u) * 218);
    scale: 1;
  }
  70.37% {
    translate: calc(var(--u) * 136) calc(var(--u) * 216);
    scale: 1;
  }
  72.222% {
    translate: calc(var(--u) * 132) calc(var(--u) * 214);
    scale: 1;
  }
  74.074% {
    translate: calc(var(--u) * 130) calc(var(--u) * 213);
    scale: 1;
  }
  75.926% {
    translate: calc(var(--u) * 128) calc(var(--u) * 212);
    scale: 1;
  }
  77.778% {
    translate: calc(var(--u) * 127) calc(var(--u) * 211);
    scale: 1;
  }
  79.63% {
    translate: calc(var(--u) * 125) calc(var(--u) * 211);
    scale: 1;
  }
  81.481% {
    translate: calc(var(--u) * 125) calc(var(--u) * 211);
    scale: 0.95;
  }
  83.333% {
    translate: calc(var(--u) * 124) calc(var(--u) * 210);
    scale: 0.85;
  }
  85.185% {
    translate: calc(var(--u) * 123) calc(var(--u) * 210);
    scale: 0.7;
  }
  87.037% {
    translate: calc(var(--u) * 123) calc(var(--u) * 209);
    scale: 0.45;
  }
  88.889% {
    translate: calc(var(--u) * 123) calc(var(--u) * 209);
    scale: 0.2;
  }
  90.741% {
    translate: calc(var(--u) * 123) calc(var(--u) * 209);
    scale: 0.1;
  }
  92.593% {
    translate: calc(var(--u) * 123) calc(var(--u) * 209);
    scale: 0;
  }
  100% {
    translate: calc(var(--u) * 123) calc(var(--u) * 209);
    scale: 0;
  }
}

@keyframes puffSmallRightKeys {
  0% {
    translate: calc(var(--u) * 208) calc(var(--u) * 240);
    scale: 0;
  }
  58.333% {
    translate: calc(var(--u) * 208) calc(var(--u) * 240);
    scale: 0;
  }
  59.259% {
    translate: calc(var(--u) * 208) calc(var(--u) * 240);
    scale: 0.7;
  }
  64.815% {
    translate: calc(var(--u) * 218) calc(var(--u) * 233);
    scale: 1;
  }
  66.667% {
    translate: calc(var(--u) * 236) calc(var(--u) * 227);
    scale: 1;
  }
  68.519% {
    translate: calc(var(--u) * 255) calc(var(--u) * 218);
    scale: 1;
  }
  70.37% {
    translate: calc(var(--u) * 260) calc(var(--u) * 216);
    scale: 1;
  }
  72.222% {
    translate: calc(var(--u) * 264) calc(var(--u) * 214);
    scale: 1;
  }
  74.074% {
    translate: calc(var(--u) * 266) calc(var(--u) * 213);
    scale: 1;
  }
  75.926% {
    translate: calc(var(--u) * 268) calc(var(--u) * 212);
    scale: 1;
  }
  77.778% {
    translate: calc(var(--u) * 269) calc(var(--u) * 211);
    scale: 1;
  }
  79.63% {
    translate: calc(var(--u) * 271) calc(var(--u) * 211);
    scale: 1;
  }
  81.481% {
    translate: calc(var(--u) * 271) calc(var(--u) * 211);
    scale: 0.95;
  }
  83.333% {
    translate: calc(var(--u) * 272) calc(var(--u) * 210);
    scale: 0.85;
  }
  85.185% {
    translate: calc(var(--u) * 273) calc(var(--u) * 210);
    scale: 0.7;
  }
  87.037% {
    translate: calc(var(--u) * 273) calc(var(--u) * 209);
    scale: 0.45;
  }
  88.889% {
    translate: calc(var(--u) * 273) calc(var(--u) * 209);
    scale: 0.2;
  }
  90.741% {
    translate: calc(var(--u) * 273) calc(var(--u) * 209);
    scale: 0.1;
  }
  92.593% {
    translate: calc(var(--u) * 273) calc(var(--u) * 209);
    scale: 0;
  }
  100% {
    translate: calc(var(--u) * 273) calc(var(--u) * 209);
    scale: 0;
  }
}

@keyframes wordSlotKeys {
  0% {
    width: calc(var(--u) * 68);
  }
  0.606% {
    width: calc(var(--u) * 140);
  }
  1.212% {
    width: calc(var(--u) * 172);
  }
  1.818% {
    width: calc(var(--u) * 192);
  }
  2.424% {
    width: calc(var(--u) * 206);
  }
  3.03% {
    width: calc(var(--u) * 216);
  }
  3.636% {
    width: calc(var(--u) * 224);
  }
  4.242% {
    width: calc(var(--u) * 230);
  }
  4.848% {
    width: calc(var(--u) * 233);
  }
  5.455% {
    width: calc(var(--u) * 234);
  }
  7.879% {
    width: calc(var(--u) * 234);
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  12.121% {
    width: calc(var(--u) * 212);
  }
  57.576% {
    width: calc(var(--u) * 212);
    animation-timing-function: cubic-bezier(0.87, 0, 0.13, 1);
  }
  67.273% {
    width: calc(var(--u) * 68);
  }
  72.727% {
    width: calc(var(--u) * 68);
  }
  73.333% {
    width: calc(var(--u) * 66);
  }
  73.939% {
    width: calc(var(--u) * 64);
  }
  74.545% {
    width: calc(var(--u) * 62);
  }
  75.152% {
    width: calc(var(--u) * 60);
  }
  75.758% {
    width: calc(var(--u) * 56);
  }
  76.364% {
    width: calc(var(--u) * 48);
  }
  76.97% {
    width: calc(var(--u) * 36);
  }
  77.576% {
    width: calc(var(--u) * 22);
  }
  78.182% {
    width: calc(var(--u) * 14);
  }
  78.788% {
    width: calc(var(--u) * 10);
  }
  79.394% {
    width: calc(var(--u) * 6);
  }
  80% {
    width: calc(var(--u) * 4);
  }
  80.606% {
    width: calc(var(--u) * 2);
  }
  81.818% {
    width: 0;
  }
  86.667% {
    width: 0;
  }
  87.273% {
    width: calc(var(--u) * 1);
  }
  87.879% {
    width: calc(var(--u) * 2);
  }
  88.485% {
    width: calc(var(--u) * 4);
  }
  89.091% {
    width: calc(var(--u) * 6);
  }
  89.697% {
    width: calc(var(--u) * 8);
  }
  90.303% {
    width: calc(var(--u) * 12);
  }
  90.909% {
    width: calc(var(--u) * 20);
  }
  91.515% {
    width: calc(var(--u) * 32);
  }
  92.121% {
    width: calc(var(--u) * 46);
  }
  92.727% {
    width: calc(var(--u) * 54);
  }
  93.333% {
    width: calc(var(--u) * 58);
  }
  93.939% {
    width: calc(var(--u) * 62);
  }
  94.545% {
    width: calc(var(--u) * 64);
  }
  95.152% {
    width: calc(var(--u) * 66);
  }
  96.364% {
    width: calc(var(--u) * 68);
  }
  100% {
    width: calc(var(--u) * 68);
  }
}

@keyframes wordRingKeys {
  0% {
    width: calc(var(--u) * 68);
  }
  0.606% {
    width: calc(var(--u) * 140);
  }
  1.212% {
    width: calc(var(--u) * 172);
  }
  1.818% {
    width: calc(var(--u) * 192);
  }
  2.424% {
    width: calc(var(--u) * 206);
  }
  3.03% {
    width: calc(var(--u) * 216);
  }
  3.636% {
    width: calc(var(--u) * 224);
  }
  4.242% {
    width: calc(var(--u) * 230);
  }
  4.848% {
    width: calc(var(--u) * 233);
  }
  5.455% {
    width: calc(var(--u) * 234);
  }
  7.879% {
    width: calc(var(--u) * 234);
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  12.121% {
    width: calc(var(--u) * 212);
  }
  57.576% {
    width: calc(var(--u) * 212);
    animation-timing-function: cubic-bezier(0.87, 0, 0.13, 1);
  }
  67.273% {
    width: calc(var(--u) * 68);
  }
  100% {
    width: calc(var(--u) * 68);
  }
}

@keyframes wordDropKeys {
  0% {
    translate: 0 0;
  }
  70.909% {
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.32, 0, 0.67, 0);
  }
  78.182% {
    translate: 0 calc(var(--u) * 180);
  }
  78.788% {
    translate: 0 calc(var(--u) * 180);
  }
  78.848% {
    translate: 0 calc(var(--u) * -200);
  }
  89.091% {
    translate: 0 calc(var(--u) * -200);
    animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
  }
  98.182% {
    translate: 0 0;
  }
  100% {
    translate: 0 0;
  }
}

@keyframes wordBandKeys {
  0% {
    translate: calc(var(--u) * -100) 0;
  }
  15.152% {
    translate: calc(var(--u) * -90) 0;
  }
  15.758% {
    translate: calc(var(--u) * -77.5) 0;
  }
  16.364% {
    translate: calc(var(--u) * -63.5) 0;
  }
  16.97% {
    translate: calc(var(--u) * -47.5) 0;
  }
  17.576% {
    translate: calc(var(--u) * -29.5) 0;
  }
  18.182% {
    translate: calc(var(--u) * -10.5) 0;
  }
  18.788% {
    translate: calc(var(--u) * 10.5) 0;
  }
  19.394% {
    translate: calc(var(--u) * 32.5) 0;
  }
  20% {
    translate: calc(var(--u) * 53.5) 0;
  }
  20.606% {
    translate: calc(var(--u) * 75.5) 0;
  }
  21.212% {
    translate: calc(var(--u) * 96.5) 0;
  }
  21.818% {
    translate: calc(var(--u) * 116.5) 0;
  }
  22.424% {
    translate: calc(var(--u) * 133.5) 0;
  }
  23.03% {
    translate: calc(var(--u) * 149.5) 0;
  }
  23.636% {
    translate: calc(var(--u) * 163.5) 0;
  }
  24.242% {
    translate: calc(var(--u) * 190) 0;
  }
  24.303% {
    translate: calc(var(--u) * -100) 0;
  }
  29.697% {
    translate: calc(var(--u) * -90) 0;
  }
  30.303% {
    translate: calc(var(--u) * -77.5) 0;
  }
  30.909% {
    translate: calc(var(--u) * -63.5) 0;
  }
  31.515% {
    translate: calc(var(--u) * -47.5) 0;
  }
  32.121% {
    translate: calc(var(--u) * -29.5) 0;
  }
  32.727% {
    translate: calc(var(--u) * -10.5) 0;
  }
  33.333% {
    translate: calc(var(--u) * 10.5) 0;
  }
  33.939% {
    translate: calc(var(--u) * 32.5) 0;
  }
  34.545% {
    translate: calc(var(--u) * 53.5) 0;
  }
  35.152% {
    translate: calc(var(--u) * 75.5) 0;
  }
  35.758% {
    translate: calc(var(--u) * 96.5) 0;
  }
  36.364% {
    translate: calc(var(--u) * 116.5) 0;
  }
  36.97% {
    translate: calc(var(--u) * 133.5) 0;
  }
  37.576% {
    translate: calc(var(--u) * 149.5) 0;
  }
  38.182% {
    translate: calc(var(--u) * 163.5) 0;
  }
  38.788% {
    translate: calc(var(--u) * 190) 0;
  }
  38.848% {
    translate: calc(var(--u) * -100) 0;
  }
  44.242% {
    translate: calc(var(--u) * -90) 0;
  }
  44.848% {
    translate: calc(var(--u) * -77.5) 0;
  }
  45.455% {
    translate: calc(var(--u) * -63.5) 0;
  }
  46.061% {
    translate: calc(var(--u) * -47.5) 0;
  }
  46.667% {
    translate: calc(var(--u) * -29.5) 0;
  }
  47.273% {
    translate: calc(var(--u) * -10.5) 0;
  }
  47.879% {
    translate: calc(var(--u) * 10.5) 0;
  }
  48.485% {
    translate: calc(var(--u) * 32.5) 0;
  }
  49.091% {
    translate: calc(var(--u) * 53.5) 0;
  }
  49.697% {
    translate: calc(var(--u) * 75.5) 0;
  }
  50.303% {
    translate: calc(var(--u) * 96.5) 0;
  }
  50.909% {
    translate: calc(var(--u) * 116.5) 0;
  }
  51.515% {
    translate: calc(var(--u) * 133.5) 0;
  }
  52.121% {
    translate: calc(var(--u) * 149.5) 0;
  }
  52.727% {
    translate: calc(var(--u) * 163.5) 0;
  }
  53.333% {
    translate: calc(var(--u) * 190) 0;
  }
  53.394% {
    translate: calc(var(--u) * -100) 0;
  }
  100% {
    translate: calc(var(--u) * -100) 0;
  }
}

@keyframes pileFloorLeftKeys {
  0% {
    rotate: 0deg;
  }
  1.111% {
    rotate: 2.3deg;
  }
  2.222% {
    rotate: 5.4deg;
  }
  3.333% {
    rotate: 7.8deg;
  }
  4.444% {
    rotate: 15deg;
  }
  5.556% {
    rotate: 20deg;
  }
  6.667% {
    rotate: 27deg;
  }
  7.778% {
    rotate: 34deg;
  }
  8.889% {
    rotate: 45deg;
  }
  10% {
    rotate: 55deg;
  }
  11.111% {
    rotate: 60deg;
  }
  12.222% {
    rotate: 62deg;
  }
  13.333% {
    rotate: 60deg;
  }
  27.778% {
    rotate: 60deg;
  }
  28.889% {
    rotate: 59deg;
  }
  30% {
    rotate: 56deg;
  }
  31.111% {
    rotate: 51deg;
  }
  32.222% {
    rotate: 44deg;
  }
  33.333% {
    rotate: 34deg;
  }
  34.444% {
    rotate: 24deg;
  }
  35.556% {
    rotate: 16deg;
  }
  36.667% {
    rotate: 8deg;
  }
  37.778% {
    rotate: 1.7deg;
  }
  38.889% {
    rotate: -2.6deg;
  }
  40% {
    rotate: -4.7deg;
  }
  41.111% {
    rotate: -4.9deg;
  }
  42.222% {
    rotate: -4.1deg;
  }
  43.333% {
    rotate: -2.5deg;
  }
  44.444% {
    rotate: -0.9deg;
  }
  45.556% {
    rotate: 0deg;
  }
  58.889% {
    rotate: 0deg;
  }
  63.333% {
    rotate: 3.5deg;
  }
  67.778% {
    rotate: 3.5deg;
  }
  72.222% {
    rotate: 0deg;
  }
  73.333% {
    rotate: 0deg;
  }
  76.111% {
    rotate: 1.5deg;
  }
  80% {
    rotate: 0deg;
  }
  100% {
    rotate: 0deg;
  }
}

@keyframes pileFloorRightKeys {
  0% {
    rotate: 0deg;
  }
  1.111% {
    rotate: -2.3deg;
  }
  2.222% {
    rotate: -5.4deg;
  }
  3.333% {
    rotate: -7.8deg;
  }
  4.444% {
    rotate: -15deg;
  }
  5.556% {
    rotate: -20deg;
  }
  6.667% {
    rotate: -27deg;
  }
  7.778% {
    rotate: -34deg;
  }
  8.889% {
    rotate: -45deg;
  }
  10% {
    rotate: -55deg;
  }
  11.111% {
    rotate: -60deg;
  }
  12.222% {
    rotate: -62deg;
  }
  13.333% {
    rotate: -60deg;
  }
  27.778% {
    rotate: -60deg;
  }
  30% {
    rotate: -59deg;
  }
  31.111% {
    rotate: -56deg;
  }
  32.222% {
    rotate: -51deg;
  }
  33.333% {
    rotate: -44deg;
  }
  34.444% {
    rotate: -34deg;
  }
  35.556% {
    rotate: -24deg;
  }
  36.667% {
    rotate: -16deg;
  }
  37.778% {
    rotate: -8deg;
  }
  38.889% {
    rotate: -1.7deg;
  }
  40% {
    rotate: 2.6deg;
  }
  41.111% {
    rotate: 4.7deg;
  }
  42.222% {
    rotate: 4.9deg;
  }
  43.333% {
    rotate: 4.1deg;
  }
  44.444% {
    rotate: 2.5deg;
  }
  45.556% {
    rotate: 0.9deg;
  }
  46.667% {
    rotate: 0deg;
  }
  58.889% {
    rotate: 0deg;
  }
  62.222% {
    rotate: -5.5deg;
  }
  67.778% {
    rotate: -5deg;
  }
  71.111% {
    rotate: -2deg;
  }
  75.556% {
    rotate: 0deg;
  }
  76.667% {
    rotate: -1.5deg;
  }
  80% {
    rotate: 0deg;
  }
  100% {
    rotate: 0deg;
  }
}

@keyframes pileBKeys {
  0% {
    translate: 0 0;
    rotate: 0deg;
  }
  1.111% {
    translate: 0 0;
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0);
  }
  22.222% {
    translate: 0 calc(var(--u) * 300);
    rotate: 4deg;
  }
  22.333% {
    translate: 0 calc(var(--u) * -225);
    rotate: 0deg;
  }
  50.556% {
    translate: 0 calc(var(--u) * -225);
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  58.889% {
    translate: 0 0;
    rotate: 0deg;
  }
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes pileBSquashKeys {
  0% {
    scale: 1 1;
  }
  50.556% {
    scale: 0.8 1.3;
  }
  58.889% {
    scale: 0.86 1.2;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  62.222% {
    scale: 1.2 0.82;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  65.556% {
    scale: 0.97 1.04;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  68.889% {
    scale: 1 1;
  }
  100% {
    scale: 1 1;
  }
}

@keyframes pileAKeys {
  0% {
    translate: 0 0;
    rotate: 0deg;
  }
  4.444% {
    translate: 0 0;
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0);
  }
  25.556% {
    translate: 0 calc(var(--u) * 300);
    rotate: 12deg;
  }
  25.667% {
    translate: 0 calc(var(--u) * -230);
    rotate: 0deg;
  }
  54.444% {
    translate: 0 calc(var(--u) * -230);
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  62.778% {
    translate: 0 0;
    rotate: 0deg;
  }
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes pileASquashKeys {
  0% {
    scale: 1 1;
  }
  54.444% {
    scale: 0.8 1.3;
  }
  62.778% {
    scale: 0.86 1.2;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  66.111% {
    scale: 1.2 0.82;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  69.444% {
    scale: 0.97 1.04;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  72.778% {
    scale: 1 1;
  }
  100% {
    scale: 1 1;
  }
}

@keyframes pileDKeys {
  0% {
    translate: 0 0;
    rotate: 0deg;
  }
  5.556% {
    translate: 0 0;
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0);
  }
  26.667% {
    translate: 0 calc(var(--u) * 320);
    rotate: -8deg;
  }
  26.778% {
    translate: 0 calc(var(--u) * -250);
    rotate: 0deg;
  }
  65.556% {
    translate: 0 calc(var(--u) * -250);
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  73.889% {
    translate: 0 0;
    rotate: 0deg;
  }
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes pileDSquashKeys {
  0% {
    scale: 1 1;
  }
  65.556% {
    scale: 0.8 1.3;
  }
  73.889% {
    scale: 0.86 1.2;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  77.222% {
    scale: 1.2 0.82;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  80.556% {
    scale: 0.97 1.04;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  83.889% {
    scale: 1 1;
  }
  100% {
    scale: 1 1;
  }
}

@keyframes pileCKeys {
  0% {
    translate: 0 0;
    rotate: 0deg;
  }
  4.444% {
    translate: 0 0;
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.11, 0, 0.5, 0);
  }
  25.556% {
    translate: 0 calc(var(--u) * 330);
    rotate: -10deg;
  }
  25.667% {
    translate: 0 calc(var(--u) * -280);
    rotate: 45deg;
  }
  57.222% {
    translate: 0 calc(var(--u) * -280);
    rotate: 45deg;
    animation-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
  }
  71.111% {
    translate: 0 calc(var(--u) * 16);
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  77.222% {
    translate: 0 calc(var(--u) * -7);
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  86.111% {
    translate: 0 0;
    rotate: 0deg;
  }
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes progressFillKeys {
  0% {
    translate: -100% 0;
  }
  0.758% {
    translate: -100% 0;
    animation-timing-function: cubic-bezier(0.33, 0.1, 0.3, 1);
  }
  7.955% {
    translate: -75% 0;
  }
  23.485% {
    translate: -75% 0;
    animation-timing-function: cubic-bezier(0.33, 0.1, 0.3, 1);
  }
  30.682% {
    translate: -50% 0;
  }
  46.212% {
    translate: -50% 0;
    animation-timing-function: cubic-bezier(0.33, 0.1, 0.3, 1);
  }
  53.409% {
    translate: -25% 0;
  }
  68.939% {
    translate: -25% 0;
    animation-timing-function: cubic-bezier(0.33, 0.1, 0.3, 1);
  }
  76.136% {
    translate: 0% 0;
  }
  89.773% {
    translate: 0% 0;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  96.591% {
    translate: -100% 0;
  }
  100% {
    translate: -100% 0;
  }
}

@keyframes progressCaption0Keys {
  0% {
    opacity: 0;
  }
  4.545% {
    opacity: 1;
  }
  15.909% {
    opacity: 1;
  }
  20.455% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes progressCaption1Keys {
  0% {
    opacity: 0;
  }
  22.727% {
    opacity: 0;
  }
  27.273% {
    opacity: 1;
  }
  38.636% {
    opacity: 1;
  }
  43.182% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes progressCaption2Keys {
  0% {
    opacity: 0;
  }
  45.455% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
  61.364% {
    opacity: 1;
  }
  65.909% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes progressCaption3Keys {
  0% {
    opacity: 0;
  }
  68.182% {
    opacity: 0;
  }
  72.727% {
    opacity: 1;
  }
  84.091% {
    opacity: 1;
  }
  88.636% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes cornerBRKeys {
  0% {
    rotate: -60deg;
  }
  0.758% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  4.545% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  7.576% {
    rotate: 0deg;
  }
  15.909% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  18.182% {
    rotate: 6deg;
  }
  18.561% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  20.833% {
    rotate: -60deg;
  }
  69.508% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  73.295% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  76.326% {
    rotate: 0deg;
  }
  84.659% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  86.932% {
    rotate: 6deg;
  }
  87.311% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  89.583% {
    rotate: -60deg;
  }
  100% {
    rotate: -60deg;
  }
}

@keyframes cornerTLKeys {
  0% {
    rotate: -60deg;
  }
  23.485% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  27.273% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  30.303% {
    rotate: 0deg;
  }
  38.636% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  40.909% {
    rotate: 6deg;
  }
  41.288% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  43.561% {
    rotate: -60deg;
  }
  70.076% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  73.864% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  76.894% {
    rotate: 0deg;
  }
  85.227% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  87.5% {
    rotate: 6deg;
  }
  87.879% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  90.152% {
    rotate: -60deg;
  }
  100% {
    rotate: -60deg;
  }
}

@keyframes cornerBLKeys {
  0% {
    rotate: -60deg;
  }
  46.212% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  50% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  53.03% {
    rotate: 0deg;
  }
  61.364% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  63.636% {
    rotate: 6deg;
  }
  64.015% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  66.288% {
    rotate: -60deg;
  }
  70.265% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  74.053% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  77.083% {
    rotate: 0deg;
  }
  85.417% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  87.689% {
    rotate: 6deg;
  }
  88.068% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  90.341% {
    rotate: -60deg;
  }
  100% {
    rotate: -60deg;
  }
}

@keyframes cornerTRKeys {
  0% {
    rotate: -60deg;
  }
  68.939% {
    rotate: -60deg;
    animation-timing-function: cubic-bezier(0.3, 0.45, 0.4, 1);
  }
  72.727% {
    rotate: 5deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  75.758% {
    rotate: 0deg;
  }
  84.091% {
    rotate: 0deg;
    animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
  }
  86.364% {
    rotate: 6deg;
  }
  86.742% {
    rotate: 6deg;
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.4);
  }
  89.015% {
    rotate: -60deg;
  }
  100% {
    rotate: -60deg;
  }
}
```

### loading-cats/cat.tsx

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

import type { CSSProperties } from 'react';

/*
 * The drawings: the cat, and the three letters the word tile spells around its O.
 *
 * The cat is a mochi of a body on a 150 × 104 grid (the body itself 120 wide, from x 4 to 124 and
 * y 18 to 102), two pointed ears, a hooked tail and a face. Ears and tail are drawn first, so the
 * body covers their roots. Its colour is the current colour; the eyes and the inside of the ears
 * take --cat-eye. It stretches to whatever box it is given (preserveAspectRatio none), so a pile
 * can hold flatter, loafing cats.
 */

export type Eyes = 'open' | 'sleep' | 'star';

/** A four-point sparkle with concave sides, centred on (x, y). */
const star = (x: number, y: number, r: number) =>
  `M${x} ${y - r}Q${x} ${y} ${x + r} ${y}Q${x} ${y} ${x} ${y + r}Q${x} ${y} ${x - r} ${y}Q${x} ${y} ${x} ${y - r}Z`;

export function Cat({
  eyes = 'open',
  round = false,
  className,
  style,
}: {
  eyes?: Eyes;
  /** A rounder, taller head (the peeking cats): the body rises to y 11 and the ears with it, on a
   *  150 × 108 grid from y -4. */
  round?: boolean;
  className?: string;
  style?: CSSProperties;
}) {
  return (
    <svg
      className={className}
      style={style}
      viewBox={round ? '0 -4 150 108' : '0 0 150 104'}
      preserveAspectRatio="none"
      aria-hidden="true"
      focusable="false"
    >
      <path
        d="M23 38L29 4L52 22ZM76 22L99 4L105 38Z"
        transform={round ? 'translate(0 -7)' : undefined}
        fill="var(--cat-eye)"
        stroke="currentColor"
        strokeWidth="6"
        strokeLinejoin="round"
      />
      <path
        d="M106 96C121 101 134 100 143 87"
        fill="none"
        stroke="currentColor"
        strokeWidth="7"
        strokeLinecap="round"
      />
      <path
        d={
          round
            ? 'M64 11C104 11 128 41 128 68C128 94 100 104 64 104C28 104 0 94 0 68C0 41 24 11 64 11Z'
            : 'M64 18C101 18 124 44 124 71C124 93 99 102 64 102C29 102 4 93 4 71C4 44 27 18 64 18Z'
        }
        fill="currentColor"
      />
      <g fill="var(--cat-eye)">
        {eyes === 'open' && (
          <>
            <rect x="39.5" y="38" width="11" height="20" rx="5.5" />
            <rect x="70.5" y="38" width="11" height="20" rx="5.5" />
          </>
        )}
        {eyes === 'sleep' && (
          <>
            <rect x="33" y="44.5" width="21" height="7" rx="3.5" />
            <rect x="64" y="44.5" width="21" height="7" rx="3.5" />
          </>
        )}
        {eyes === 'star' && <path d={`${star(36.5, 63, 11)}${star(71, 63, 11)}`} />}
      </g>
    </svg>
  );
}

/*
 * The letters L, A and D: geometric capitals 66 units tall with 14-unit stems, the same weight as
 * the O's ring, so the ring can leave the word and come back without a seam. Drawn rather than
 * typeset, so the word is the same on every system.
 */
const LETTERS = {
  L: { width: 40, d: 'M0 0H14V52H40V66H0Z' },
  A: { width: 57, d: 'M20 0H37L57 66H43L38.8 52H18.2L14 66H0ZM28.5 16L22 40H35Z' },
  D: {
    width: 51,
    d: 'M0 0H22C39 0 51 14 51 33C51 52 39 66 22 66H0ZM14 14H22C31 14 37 22 37 33C37 44 31 52 22 52H14Z',
  },
};

export function Letter({
  letter,
  className,
}: {
  letter: keyof typeof LETTERS;
  className?: string;
}) {
  const { width, d } = LETTERS[letter];
  return (
    <svg
      className={className}
      style={{ width: `calc(var(--u) * ${width})` }}
      viewBox={`0 0 ${width} 66`}
      aria-hidden="true"
      focusable="false"
    >
      <path d={d} fillRule="evenodd" fill="currentColor" />
    </svg>
  );
}
```

### loading-cats/use-autoplay.ts

```tsx
'use client';

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

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

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

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

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