CSS and Layout skill 9 of 12

CSS Animation SKILL.md: Fix Transitions, Reduced Motion and Jank

The CSS Animations and Transitions skill makes Claude, Cursor or another AI agent find and fix transition: all, layout properties animated, missing or blunt reduced-motion rules, unitless durations, display fades without allow-discrete and endless loops. A scanner reports each case by file and line, the agent edits only flagged lines, and you approve each batch.

css-animations-transitions/SKILL.md, version 1.0.0, 4,135 words, plus scripts/motion_check.py and two reference files.

An agent works on motion in this order: confirm scope and the motion rules the project already has, run the scanner, fix declarations that are invalid or do nothing, move animated properties onto the cheap ones, write the reduced-motion rule for every animation that remains, handle entry and exit of hidden elements, and re-run the scanner plus one emulated check. The scanner reads text. It cannot see animations started by JavaScript or timing it cannot parse, so it reports patterns for a person or agent to confirm.

Reusable code is in references/patterns.md. Support notes, tool notes and the rival comparison are in references/appendix.md.

When to use this skill, and when not to

Use it when an animation stutters, a transition does nothing, an element cannot fade out because it is display: none, a design has no reduced-motion handling, or a stylesheet is full of transition: all.

RequestUse instead
Animating a custom property, @property, var() inside keyframescss-custom-properties.md
Opening and closing <dialog> and popovers, focus, light dismissdialog-and-popover.md
Focus rings and what they must showkeyboard-focus.md
Announcing changes to screen readersaria-live-announcements.md
GSAP, Framer Motion, the Web Animations API, LottieOut of scope. Name the tool and stop
Scroll-driven animation, view transitions, SVG animationOut of scope. No source read for this skill covers them
Choosing the brand's motion personalityOut of scope. Durations and easing come from the user

Guardrails

  1. Propose, then change. Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
  2. Durations and easing come from the user. The agent never picks a value for the project. It offers the options in step 5, the user chooses, and the choice goes into a token or a shared rule.
  3. Reduced motion changes what people see. Show the reduced version of each animation to the user before applying it. Never remove an animation that carries information (a progress indicator, a validation shake that is the only error cue) without agreeing a replacement.
  4. Not a conformance claim. WCAG success criteria are read here as guidance. A statement that a product conforms needs a human accessibility review and is out of scope. This is not legal advice.
  5. Data handled. The scanner prints file, line, code and a fixed message, never source text. Class names and comments can reveal unreleased features: report selectors only when the user needs them, and never paste stylesheets into prompts.
  6. Nothing is invented. Every rule carries a source and a date. Anything that depends on a browser version carries verify before relying on this. The sources for compositing advice are old (step 4): measure instead of trusting them.
  7. Refuses to: add a global animation: none !important reset as the whole fix, add will-change to silence a jank report, claim a speed-up without a before and after measurement, or declare a fix verified without re-running the scanner.

Inputs the agent must have before starting

Ask for every missing item. Do not assume.

InputAsk exactly
Scope"Which files or folders should I review? Give paths."
Symptom"What goes wrong: stutter, a transition that does nothing, an element that cannot fade out, no reduced-motion support, or a general review?"
Motion rules"Does the project have motion tokens or a motion guideline? Give the file or say none."
Browsers"Which browsers and minimum versions must work? Say if Firefox or Safari must support @starting-style and allow-discrete."
JavaScript"Does any script listen for animationend or transitionend, toggle classes to start motion, or use the Web Animations API? Give paths or say none."
Essential motion"Which animations carry information a user needs (progress, loading, error cues)?"
Approval"May I edit on a branch in batches of one file and show each diff first?"

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as css-animations-transitions/SKILL.md.

---
name: css-animations-transitions
description: "Audits and fixes CSS transitions and animations: transition: all, layout properties animated, missing or blunt prefers-reduced-motion, unitless durations, display transitions without allow-discrete, broad will-change, endless loops. Runs a scanner, then edits flagged lines only. Use for 'janky animation', 'prefers-reduced-motion', 'transition not working', '@starting-style', fade out display none. Do NOT use for JavaScript animation libraries, WAAPI or GSAP, custom property typing (use css-custom-properties), scroll-driven animation or SVG."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/
  category: frontend-development
  subcategory: css-layout
  version: "1.0.0"
  created: "2026-11-07"
  updated: "2026-11-07"
---

# CSS Animations and Transitions: Motion That Is Cheap, Short and Optional

An agent works on motion in this order: confirm scope and the motion rules the project already has, run the scanner, fix declarations that are invalid or do nothing, move animated properties onto the cheap ones, write the reduced-motion rule for every animation that remains, handle entry and exit of hidden elements, and re-run the scanner plus one emulated check. The scanner reads text. It cannot see animations started by JavaScript or timing it cannot parse, so it reports patterns for a person or agent to confirm.

Reusable code is in `references/patterns.md`. Support notes, tool notes and the rival comparison are in `references/appendix.md`.

## When to use this skill, and when not to

Use it when an animation stutters, a transition does nothing, an element cannot fade out because it is `display: none`, a design has no reduced-motion handling, or a stylesheet is full of `transition: all`.

| Request | Use instead |
|---|---|
| Animating a custom property, `@property`, `var()` inside keyframes | `css-custom-properties.md` |
| Opening and closing `<dialog>` and popovers, focus, light dismiss | `dialog-and-popover.md` |
| Focus rings and what they must show | `keyboard-focus.md` |
| Announcing changes to screen readers | `aria-live-announcements.md` |
| GSAP, Framer Motion, the Web Animations API, Lottie | Out of scope. Name the tool and stop |
| Scroll-driven animation, view transitions, SVG animation | Out of scope. No source read for this skill covers them |
| Choosing the brand's motion personality | Out of scope. Durations and easing come from the user |

## Guardrails

1. **Propose, then change.** Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Durations and easing come from the user.** The agent never picks a value for the project. It offers the options in step 5, the user chooses, and the choice goes into a token or a shared rule.
3. **Reduced motion changes what people see.** Show the reduced version of each animation to the user before applying it. Never remove an animation that carries information (a progress indicator, a validation shake that is the only error cue) without agreeing a replacement.
4. **Not a conformance claim.** WCAG success criteria are read here as guidance. A statement that a product conforms needs a human accessibility review and is out of scope. This is not legal advice.
5. **Data handled.** The scanner prints file, line, code and a fixed message, never source text. Class names and comments can reveal unreleased features: report selectors only when the user needs them, and never paste stylesheets into prompts.
6. **Nothing is invented.** Every rule carries a source and a date. Anything that depends on a browser version carries **verify before relying on this**. The sources for compositing advice are old (step 4): measure instead of trusting them.
7. **Refuses to:** add a global `animation: none !important` reset as the whole fix, add `will-change` to silence a jank report, claim a speed-up without a before and after measurement, or declare a fix verified without re-running the scanner.

## Inputs the agent must have before starting

Ask for every missing item. Do not assume.

| Input | Ask exactly |
|---|---|
| Scope | "Which files or folders should I review? Give paths." |
| Symptom | "What goes wrong: stutter, a transition that does nothing, an element that cannot fade out, no reduced-motion support, or a general review?" |
| Motion rules | "Does the project have motion tokens or a motion guideline? Give the file or say none." |
| Browsers | "Which browsers and minimum versions must work? Say if Firefox or Safari must support `@starting-style` and `allow-discrete`." |
| JavaScript | "Does any script listen for `animationend` or `transitionend`, toggle classes to start motion, or use the Web Animations API? Give paths or say none." |
| Essential motion | "Which animations carry information a user needs (progress, loading, error cues)?" |
| Approval | "May I edit on a branch in batches of one file and show each diff first?" |

## Workflow

### Step 1: Run the scanner

```
python3 scripts/motion_check.py src | head -40
```

Read only its output. Do not read the script. The first line holds the counts. `--strict` exits 1 on warnings. It skips `node_modules`, `dist`, `build`, `.git`, `.next` and `vendor`, and reads `.css`, `.scss`, `.sass`, `.less`, `.html`, `.htm`, `.vue`, `.svelte` and `.astro`.

| Code | Level | Meaning | Fix |
|---|---|---|---|
| `no-reduced-motion` | error | Animation or transition code and no `prefers-reduced-motion` query anywhere scanned | Step 3 |
| `duration-no-unit` | error | A non-zero duration with no `s` or `ms`: invalid | Step 5 |
| `discrete-no-allow-discrete` | error | `display` or `content-visibility` transitioned without `allow-discrete` | Step 6 |
| `transition-all` | warn | `transition: all` or `transition-property: all` | Step 5 |
| `layout-property-animated` | warn | A layout property in a transition or in `@keyframes` | Step 4 |
| `behavior-before-shorthand` | warn | `transition-behavior` written before the `transition` shorthand | Step 6 |
| `will-change-broad` | warn | `will-change` on `*`, `html`, `body`, `div`, `span` or `:root` | Step 4 |
| `reduced-motion-blunt-reset` | warn | `!important` duration or `animation` reset on a broad selector inside a reduced-motion query | Step 3 |
| `infinite-no-pause` | warn | `infinite` animation whose selector has no reduced-motion rule and no `animation-play-state` | Step 3 |
| `animation-name-undefined` | warn | `animation-name` with no `@keyframes` in the scanned files | Step 5 |
| `will-change-static` | info | `will-change` in a normal rule | Step 4 |
| `keyframes-unused` | info | `@keyframes` never referenced | Step 5 |

The scanner splits on text. A hit in a string or template is a false positive: read the line. It cannot see an `@keyframes` defined in a file outside the scan, which produces `animation-name-undefined`. It checks reduced motion at file-set level, so one query in one file silences `no-reduced-motion` for all files: step 3 checks each animation by hand.

### Step 2: Fix invalid and inert declarations

| Finding | Why it does nothing | Fix |
|---|---|---|
| `duration-no-unit` | `opacity .3` has no unit, so the declaration is invalid | `0.3s` or `300ms`. Only `0` may omit the unit |
| `animation-name-undefined` | The name has no `@keyframes` in scope, so nothing runs | Define it, correct the spelling, or ask where it lives |
| `keyframes-unused` | Dead code | Report. Delete only on approval, since JavaScript can set the name |
| A transition that never starts | The property has no start value in the same rule set, or the element is inserted already in its final state | Step 6 for new elements. Otherwise declare the start value |

### Step 3: Reduced motion

Sources: WCAG 2.2, Understanding SC 2.3.3, Animation from Interactions, fetched 7 November 2026; web.dev, prefers-reduced-motion, last updated 11 March 2019, fetched 7 November 2026.

| Fact | Consequence |
|---|---|
| SC 2.3.3 is Level AAA: motion animation triggered by interaction can be disabled, unless it is essential (WCAG) | Aim for it, but do not describe a site as failing AA because it lacks it |
| Sufficient techniques are C39 (the `prefers-reduced-motion` media query) and SCR40 (the same query in JavaScript) (WCAG) | Both are covered in `references/patterns.md`, sections 1 and 2 |
| Colour, opacity and blur changes that do not alter perceived size, shape or position are outside the glossary definition, though an erratum removes the blur exclusion (WCAG) | A fade is usually safe to keep. A blur needs a check |
| SC 2.2.2 covers animation the page starts by itself, and one animation can fail both criteria (WCAG) | Read SC 2.2.2 for its time threshold before quoting one: it was not fetched for this skill |
| `reduce` means minimise non-essential movement, ideally to none (web.dev) | Remove movement, keep feedback. Do not remove everything |
| The `*`, `::before`, `::after` reset with `!important` is a workaround for user stylesheets, and it uses 1 ms because some sites rely on `animationend` (web.dev) | Do not ship it as the site's own fix |
| The media query cannot stop motion started by the Web Animations API (web.dev) | Check JavaScript separately (step 7) |

Classify every animation the scanner and a read of the code find:

| Class | Examples | Under `reduce` |
|---|---|---|
| Essential | Progress bar, indeterminate loading, the only error cue | Keep, with the movement removed or shrunk: agree the replacement with the user |
| Movement | Translate, scale, rotate, parallax, bounce, slide-in | Remove: set the end state at once |
| Non-movement | Opacity or colour fade | Keep, and shorten if the user wants |
| Autoplay loop | Infinite spin, marquee, pulsing badge | Stop, or pause with a control |

Rules:

1. Write the reduced rule next to the animation it changes, inside a `@media (prefers-reduced-motion: reduce)` block, selector by selector. This is the pattern at `references/patterns.md` section 1. For new code, the opt-in form (motion inside `no-preference`) is the safer default: it is a house rule, and web.dev shows both forms.
2. Setting `animation: none` means `animationend` never fires. Run `grep -rnE "animationend|transitionend|onAnimationEnd|onTransitionEnd" src`. Every handler found needs a fallback that runs without the event, or the reduced rule must keep a near-zero duration for that element instead.
3. `reduced-motion-blunt-reset` is fixed by replacing the global reset with per-selector rules, not by deleting it and leaving nothing.
4. `infinite-no-pause`: stop the loop under `reduce`. If the loop is essential, ask the user for the replacement (guardrail 3).

### Step 4: Move animated properties onto cheap ones

Sources: web.dev, Stick to compositor-only properties and manage layer count, last updated 20 March 2015, fetched 7 November 2026; Josh Comeau, An Interactive Guide to CSS Transitions, fetched 7 November 2026. **Verify before relying on this**: the web.dev article is more than ten years old, so confirm in the browser's performance panel, in the browsers the user named.

| Fact | Consequence |
|---|---|
| web.dev names only `transform` and `opacity` as compositor-only | Prefer them. Replace a width, height, top or left animation with `transform` where the visual result is the same |
| For other properties the article suggests FLIP, which remaps the change onto `transform` and `opacity` | Offer FLIP only when a layout change must animate. Sketch in `references/patterns.md`, section 5 |
| At 60 fps a frame is about 16.6 ms (Comeau) | A layout property animated on every frame can break that budget on a slow device |
| `transform` moves between pixels, while `margin-top` rounds to whole pixels (Comeau) | A `margin` animation can look steppy even when it is fast |
| The article suggests about 4 to 5 ms of compositing time as a target during scrolling or transitions | Treat it as a prompt to measure, not as a pass mark |

`will-change` rules:

| Situation | Do |
|---|---|
| On `*`, `html`, `body` or a tag selector | Remove. web.dev shows a `*` rule with `will-change` as its bad example |
| On a class, in a static rule | Report as info. Keep only if the user can show the first frame stutters without it |
| Needed for one animation | Add it just before the motion starts and remove it after, as in `references/patterns.md`, section 6 |
| "How many layers is too many?" | No source read gives a number. Open the Layers panel in browser DevTools and ask the user what the target device is |

The article names Chrome DevTools: in the Paint profiler, select a frame and open the Layers tab to see each layer and why it exists. Menu names change, so **verify before relying on this** in the current DevTools.

### Step 5: Transitions: properties, durations and easing

| Rule | Detail |
|---|---|
| List the properties | Replace `all` with the exact list (Comeau: "It pays to be really precise with the properties we animate") |
| Units | `s` or `ms` on every non-zero duration |
| Define the keyframes you reference | One name, one `@keyframes` |
| Hover triggers | Put the trigger on the parent and animate a child, so the moved element does not slide out from under the pointer and retrigger hover (Comeau) |
| Dropdown gaps | A `transition-delay` on the close lets the pointer cross a gap (Comeau gives 300 ms in a demo) |

Durations and easing come from the user (guardrail 2). If the project has no motion tokens, offer these as options and say where each comes from. Neither source is a standard.

| Option | Source |
|---|---|
| 125 ms for a snappy hover-in, 450 ms for a relaxed hover-out, 250 to 750 ms in general demos | Josh Comeau, demos, fetched 7 November 2026 |
| About 150 to 300 ms for UI elements, 300 to 500 ms for page transitions | moderndegree, motion-animation skill (a rival's claim), fetched 7 November 2026 |
| `ease-out` for elements entering, `ease-in` only when leaving fully offscreen or becoming invisible, `ease-in-out` for loops | Josh Comeau, fetched 7 November 2026 |

Put the chosen values in tokens (`css-custom-properties.md`, step 5) and read them with `var()` in `transition` values.

### Step 6: Entry, exit and `display`

Source: Chrome for Developers, Four new CSS features for smooth entry and exit animations, last updated 16 August 2023 with a note dated 13 September 2024, fetched 7 November 2026. **Verify before relying on this** for every browser outside Chrome: the page gives Chrome versions only and no Baseline status.

| Fact | Consequence |
|---|---|
| Discrete properties such as `display` transition only with `transition-behavior: allow-discrete`, set for each transitioning property | Add it to every rule that transitions `display` or `content-visibility` |
| The `transition` shorthand resets `transition-behavior` to `normal` | Write `transition-behavior` after the shorthand: `behavior-before-shorthand` |
| In a transition, `display` switches at the start when animating in and at the end when animating out | A fade-out keeps the element visible until the fade ends |
| `@starting-style` gives the before-open state the browser reads when an element first renders (Chrome 117) | Use it for fade-in on insert and on `[open]` or `:popover-open` |
| A note says `@starting-style` rules must come after the normal declarations from Chrome 130 | Put the block after the base rule |
| Without `overlay` in the transition list, a popover or dialog snaps out of the top layer and the fade-out is not seen | Add `overlay` with `allow-discrete` for top-layer elements |
| With `animation-fill-mode: forwards` the element stays `display: none`. Removing the node needs a timeout so the animation can finish | Ask before removing nodes from script |

Pattern: `references/patterns.md`, section 4. For focus return and light dismiss on dialogs and popovers, hand over to `dialog-and-popover.md`.

### Step 7: JavaScript and the rest

| Check | Do |
|---|---|
| Script adds classes to start motion | Make sure the class change cannot start a movement animation under `reduce`: use the matchMedia pattern (`references/patterns.md`, section 2) |
| Script uses the Web Animations API | The CSS query does not stop it (web.dev). Gate it with `matchMedia` |
| Script waits for `animationend` | Step 3, rule 2 |
| Script removes nodes after an exit | Wait for the event or a timeout equal to the duration, from the same token |

### Step 8: Verify and report

1. Re-run the scanner. Errors must be 0. Every warning is fixed or listed with a reason.
2. Emulate reduced motion in the browser or test runner (`references/appendix.md`, section 3) and load one page per fixed finding. Report any movement still visible.
3. Compare before and after in the browser's performance panel for each animation moved onto `transform` or `opacity`. Report the numbers, or say it was not measured.
4. State what is untested: JavaScript-started motion, browsers outside the matrix, low-end devices, screen reader behaviour.
5. Use the report template. Do not echo unchanged CSS back.

## Worked example

An invented `motion.css` of 12 lines: `will-change` on `*`, `transition: all`, a height transition and an unitless duration, a `display` transition with no `allow-discrete`, another with the behaviour longhand before the shorthand, two infinite animations (one pointing at a missing `@keyframes`), a keyframes block that animates `width`, an unused keyframes block, and a reduced-motion rule that zeroes every duration with `!important`.

```
$ python3 scripts/motion_check.py motion.css
2 errors, 9 warnings, 1 info, 1 files
motion.css:1 warn  will-change-broad  will-change on a broad selector promotes many layers ...
motion.css:2 warn  transition-all  transition on all: list the exact properties
motion.css:3 error duration-no-unit  transition duration has no unit (s or ms) ...
motion.css:3 warn  layout-property-animated  transitions height: layout work each frame
motion.css:4 error discrete-no-allow-discrete  display transition without transition-behavior: allow-discrete ...
motion.css:5 warn  behavior-before-shorthand  transition shorthand after transition-behavior resets it ...
motion.css:6 warn  infinite-no-pause  infinite animation, and its selector has no reduced-motion rule ...
motion.css:7 warn  animation-name-undefined  animation-name orbit has no @keyframes in the scanned files
motion.css:8 warn  layout-property-animated  @keyframes pulse animates width: layout work each frame
motion.css:11 warn  reduced-motion-blunt-reset  global !important reset in reduced motion ...
```

(Messages shortened, 2 lines omitted.) The agent asks for the durations and is told the project has no tokens and the user picks 200 ms for state changes and 300 ms for panels. It lists the transitioned properties, rewrites the height and width animations as `transform`, adds units, writes the longhand after the shorthand with `allow-discrete`, defines the `orbit` keyframes, deletes the unused block and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. It greps for `animationend` and finds no handler.

```
$ python3 scripts/motion_check.py motion.css
0 errors, 0 warnings, 0 info, 1 files
```

The scanner passing shows that the patterns it knows are gone. It does not show that the frame rate improved or that the reduced version reads well. Both go in the report as untested until measured and shown to the user.

## Templates

### Report

```
Motion report: <repo or folder>, <date>
Files: <count>. Browsers: <matrix>. Motion tokens: <file or none>.
motion_check before: N errors, N warnings, N info. After: N errors, N warnings, N info.
Changes: <file:line  code  old -> new>   (one line each)
Durations and easing chosen by the user: <token  value>
Reduced-motion decisions (user approved): <selector  class  reduced behaviour>
Left as is: <file:line  reason>
Untested: JavaScript-started motion, browsers outside the matrix, low-end devices, measured frame rate (edit to match)
```

### Starting prompt

```
Use the css-animations-transitions skill. Scope: <paths>. Browsers: <matrix>.
Symptom: <stutter | transition does nothing | cannot fade out | no reduced motion | review>.
Motion tokens: <file or none>. Scripts that use animationend, transitionend or WAAPI: <paths or none>.
Essential motion: <list>. Edit on a branch in batches of one file and show each diff first.
```

### Per-component reduced motion

```css
.toast { animation: slide-in var(--motion-panel) ease-out; }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }   /* end state at once; the fade stays on .toast__body */
}
```

## Failure modes and anti-patterns

| Mistake | How to spot it | Do instead |
|---|---|---|
| `transition: all` | `transition-all`; unrelated properties animate when a class changes | List the properties (step 5) |
| Animating layout properties | `layout-property-animated`; stutter on slow devices | `transform` or `opacity`, or FLIP (step 4) |
| A global reduced-motion kill switch | `reduced-motion-blunt-reset`; feedback vanishes and `animationend` handlers stall | Per-selector rules and a handler check (step 3) |
| `display` transition that never animates | `discrete-no-allow-discrete`, `behavior-before-shorthand` | `allow-discrete` after the shorthand, plus `@starting-style` (step 6) |
| `will-change` everywhere | `will-change-broad` | Scope it to one element and one moment (step 4) |

## Quality bar

Run every item before handing over.

1. `motion_check.py` errors are 0, and every warning and info line is fixed or listed with a reason.
2. Every duration and easing value in the changes came from the user, and the report lists each one.
3. Every remaining animation is classified in step 3 and has a reduced-motion rule written next to it, or the report says why not.
4. Every `animationend` and `transitionend` handler has a fallback when its animation is removed, or the report lists it.
5. Every animation moved onto `transform` or `opacity` has before and after numbers, or the report says it was not measured.
6. No `will-change` remains on `*`, `html`, `body` or a tag selector.
7. The reduced version of each essential animation was shown to the user.
8. Browsers outside the user's matrix are listed as untested.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 89 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 6,288 | Estimate | When the skill fires |
| C. `references/patterns.md` | 1,057 | Estimate | Step 3 for the reduced-motion code, step 4 for FLIP and `will-change`, step 6 for entry and exit, step 5 for hover patterns, step 7 for the handler check |
| C. `references/appendix.md` | 1,184 | Estimate | Only for support notes, tool notes, source notes or the rival comparison |
| Script output (typical run) | 448 | Estimate | Scanner output measured on the worked example (13 lines, 1,567 characters) |
| D. Typical run (A + B + script output) | 6,825 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 9,996 | Estimate | Large input, every branch. Above 9,000: use `head -40` and read one reference at a time |
| E. Cheaper path | 6,825 | Estimate | Run the scanner, fix only flagged lines, skip both references and the emulated check |

The three rivals I read have bodies of about 1,150 and 440 tokens, or a body that was not visible (estimates from visible text). None ships a scanner and none covers `display` transitions or checks `animationend` handlers before removing motion. This skill costs more to load, and in return the scanner finds the patterns so the agent does not read every stylesheet to look for them.

## Handoffs

`css-custom-properties.md` covers registering and typing the properties you animate and the tokens that hold durations. `dialog-and-popover.md` covers the elements that most need entry and exit motion. `keyboard-focus.md` covers focus visibility during transitions. All four read different sources, so none repeats another.

## Appendix

Reusable code is in `references/patterns.md`. Support notes, tool notes and the rival comparison are in `references/appendix.md`. Read them only when a step points there.

## Sources

Last verified 7 November 2026 for every source.

- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- W3C, Understanding SC 2.3.3, Animation from Interactions (WCAG 2.2): https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
- web.dev, prefers-reduced-motion: Sometimes less movement is more (last updated 11 March 2019): https://web.dev/articles/prefers-reduced-motion
- web.dev, Stick to compositor-only properties and manage layer count (last updated 20 March 2015): https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count
- Chrome for Developers, Four new CSS features for smooth entry and exit animations (last updated 16 August 2023): https://developer.chrome.com/blog/entry-exit-animations
- Josh Comeau, An Interactive Guide to CSS Transitions: https://www.joshwcomeau.com/animation/css-transitions/
- pluginagentmarketplace, css-animations. Rival implementation: https://playbooks.com/skills/pluginagentmarketplace/custom-plugin-css/css-animations
- moderndegree, motion-animation. Rival implementation: https://skills.cat/skills/moderndegree/agent-skills/motion-animation
- msrbuilds, animate-css-skill. Rival implementation, page metadata and README only: https://github.com/msrbuilds/animate-css-skill
- Anthropic, frontend-design. Related implementation: https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.

Workflow

Step 1: Run the scanner

python3 scripts/motion_check.py src | head -40

Read only its output. Do not read the script. The first line holds the counts. --strict exits 1 on warnings. It skips node_modules, dist, build, .git, .next and vendor, and reads .css, .scss, .sass, .less, .html, .htm, .vue, .svelte and .astro.

CodeLevelMeaningFix
no-reduced-motionerrorAnimation or transition code and no prefers-reduced-motion query anywhere scannedStep 3
duration-no-uniterrorA non-zero duration with no s or ms: invalidStep 5
discrete-no-allow-discreteerrordisplay or content-visibility transitioned without allow-discreteStep 6
transition-allwarntransition: all or transition-property: allStep 5
layout-property-animatedwarnA layout property in a transition or in @keyframesStep 4
behavior-before-shorthandwarntransition-behavior written before the transition shorthandStep 6
will-change-broadwarnwill-change on *, html, body, div, span or :rootStep 4
reduced-motion-blunt-resetwarn!important duration or animation reset on a broad selector inside a reduced-motion queryStep 3
infinite-no-pausewarninfinite animation whose selector has no reduced-motion rule and no animation-play-stateStep 3
animation-name-undefinedwarnanimation-name with no @keyframes in the scanned filesStep 5
will-change-staticinfowill-change in a normal ruleStep 4
keyframes-unusedinfo@keyframes never referencedStep 5

The scanner splits on text. A hit in a string or template is a false positive: read the line. It cannot see an @keyframes defined in a file outside the scan, which produces animation-name-undefined. It checks reduced motion at file-set level, so one query in one file silences no-reduced-motion for all files: step 3 checks each animation by hand.

Step 2: Fix invalid and inert declarations

FindingWhy it does nothingFix
duration-no-unitopacity .3 has no unit, so the declaration is invalid0.3s or 300ms. Only 0 may omit the unit
animation-name-undefinedThe name has no @keyframes in scope, so nothing runsDefine it, correct the spelling, or ask where it lives
keyframes-unusedDead codeReport. Delete only on approval, since JavaScript can set the name
A transition that never startsThe property has no start value in the same rule set, or the element is inserted already in its final stateStep 6 for new elements. Otherwise declare the start value

Step 3: Reduced motion

Sources: WCAG 2.2, Understanding SC 2.3.3, Animation from Interactions, fetched 7 November 2026; web.dev, prefers-reduced-motion, last updated 11 March 2019, fetched 7 November 2026.

FactConsequence
SC 2.3.3 is Level AAA: motion animation triggered by interaction can be disabled, unless it is essential (WCAG)Aim for it, but do not describe a site as failing AA because it lacks it
Sufficient techniques are C39 (the prefers-reduced-motion media query) and SCR40 (the same query in JavaScript) (WCAG)Both are covered in references/patterns.md, sections 1 and 2
Colour, opacity and blur changes that do not alter perceived size, shape or position are outside the glossary definition, though an erratum removes the blur exclusion (WCAG)A fade is usually safe to keep. A blur needs a check
SC 2.2.2 covers animation the page starts by itself, and one animation can fail both criteria (WCAG)Read SC 2.2.2 for its time threshold before quoting one: it was not fetched for this skill
reduce means minimise non-essential movement, ideally to none (web.dev)Remove movement, keep feedback. Do not remove everything
The *, ::before, ::after reset with !important is a workaround for user stylesheets, and it uses 1 ms because some sites rely on animationend (web.dev)Do not ship it as the site's own fix
The media query cannot stop motion started by the Web Animations API (web.dev)Check JavaScript separately (step 7)

Classify every animation the scanner and a read of the code find:

ClassExamplesUnder reduce
EssentialProgress bar, indeterminate loading, the only error cueKeep, with the movement removed or shrunk: agree the replacement with the user
MovementTranslate, scale, rotate, parallax, bounce, slide-inRemove: set the end state at once
Non-movementOpacity or colour fadeKeep, and shorten if the user wants
Autoplay loopInfinite spin, marquee, pulsing badgeStop, or pause with a control

Rules:

  1. Write the reduced rule next to the animation it changes, inside a @media (prefers-reduced-motion: reduce) block, selector by selector. This is the pattern at references/patterns.md section 1. For new code, the opt-in form (motion inside no-preference) is the safer default: it is a house rule, and web.dev shows both forms.
  2. Setting animation: none means animationend never fires. Run grep -rnE "animationend|transitionend|onAnimationEnd|onTransitionEnd" src. Every handler found needs a fallback that runs without the event, or the reduced rule must keep a near-zero duration for that element instead.
  3. reduced-motion-blunt-reset is fixed by replacing the global reset with per-selector rules, not by deleting it and leaving nothing.
  4. infinite-no-pause: stop the loop under reduce. If the loop is essential, ask the user for the replacement (guardrail 3).

Step 4: Move animated properties onto cheap ones

Sources: web.dev, Stick to compositor-only properties and manage layer count, last updated 20 March 2015, fetched 7 November 2026; Josh Comeau, An Interactive Guide to CSS Transitions, fetched 7 November 2026. Verify before relying on this: the web.dev article is more than ten years old, so confirm in the browser's performance panel, in the browsers the user named.

FactConsequence
web.dev names only transform and opacity as compositor-onlyPrefer them. Replace a width, height, top or left animation with transform where the visual result is the same
For other properties the article suggests FLIP, which remaps the change onto transform and opacityOffer FLIP only when a layout change must animate. Sketch in references/patterns.md, section 5
At 60 fps a frame is about 16.6 ms (Comeau)A layout property animated on every frame can break that budget on a slow device
transform moves between pixels, while margin-top rounds to whole pixels (Comeau)A margin animation can look steppy even when it is fast
The article suggests about 4 to 5 ms of compositing time as a target during scrolling or transitionsTreat it as a prompt to measure, not as a pass mark

will-change rules:

SituationDo
On *, html, body or a tag selectorRemove. web.dev shows a * rule with will-change as its bad example
On a class, in a static ruleReport as info. Keep only if the user can show the first frame stutters without it
Needed for one animationAdd it just before the motion starts and remove it after, as in references/patterns.md, section 6
"How many layers is too many?"No source read gives a number. Open the Layers panel in browser DevTools and ask the user what the target device is

The article names Chrome DevTools: in the Paint profiler, select a frame and open the Layers tab to see each layer and why it exists. Menu names change, so verify before relying on this in the current DevTools.

Step 5: Transitions: properties, durations and easing

RuleDetail
List the propertiesReplace all with the exact list (Comeau: "It pays to be really precise with the properties we animate")
Unitss or ms on every non-zero duration
Define the keyframes you referenceOne name, one @keyframes
Hover triggersPut the trigger on the parent and animate a child, so the moved element does not slide out from under the pointer and retrigger hover (Comeau)
Dropdown gapsA transition-delay on the close lets the pointer cross a gap (Comeau gives 300 ms in a demo)

Durations and easing come from the user (guardrail 2). If the project has no motion tokens, offer these as options and say where each comes from. Neither source is a standard.

OptionSource
125 ms for a snappy hover-in, 450 ms for a relaxed hover-out, 250 to 750 ms in general demosJosh Comeau, demos, fetched 7 November 2026
About 150 to 300 ms for UI elements, 300 to 500 ms for page transitionsmoderndegree, motion-animation skill (a rival's claim), fetched 7 November 2026
ease-out for elements entering, ease-in only when leaving fully offscreen or becoming invisible, ease-in-out for loopsJosh Comeau, fetched 7 November 2026

Put the chosen values in tokens (css-custom-properties.md, step 5) and read them with var() in transition values.

Step 6: Entry, exit and display

Source: Chrome for Developers, Four new CSS features for smooth entry and exit animations, last updated 16 August 2023 with a note dated 13 September 2024, fetched 7 November 2026. Verify before relying on this for every browser outside Chrome: the page gives Chrome versions only and no Baseline status.

FactConsequence
Discrete properties such as display transition only with transition-behavior: allow-discrete, set for each transitioning propertyAdd it to every rule that transitions display or content-visibility
The transition shorthand resets transition-behavior to normalWrite transition-behavior after the shorthand: behavior-before-shorthand
In a transition, display switches at the start when animating in and at the end when animating outA fade-out keeps the element visible until the fade ends
@starting-style gives the before-open state the browser reads when an element first renders (Chrome 117)Use it for fade-in on insert and on [open] or :popover-open
A note says @starting-style rules must come after the normal declarations from Chrome 130Put the block after the base rule
Without overlay in the transition list, a popover or dialog snaps out of the top layer and the fade-out is not seenAdd overlay with allow-discrete for top-layer elements
With animation-fill-mode: forwards the element stays display: none. Removing the node needs a timeout so the animation can finishAsk before removing nodes from script

Pattern: references/patterns.md, section 4. For focus return and light dismiss on dialogs and popovers, hand over to dialog-and-popover.md.

Step 7: JavaScript and the rest

CheckDo
Script adds classes to start motionMake sure the class change cannot start a movement animation under reduce: use the matchMedia pattern (references/patterns.md, section 2)
Script uses the Web Animations APIThe CSS query does not stop it (web.dev). Gate it with matchMedia
Script waits for animationendStep 3, rule 2
Script removes nodes after an exitWait for the event or a timeout equal to the duration, from the same token

Step 8: Verify and report

  1. Re-run the scanner. Errors must be 0. Every warning is fixed or listed with a reason.
  2. Emulate reduced motion in the browser or test runner (references/appendix.md, section 3) and load one page per fixed finding. Report any movement still visible.
  3. Compare before and after in the browser's performance panel for each animation moved onto transform or opacity. Report the numbers, or say it was not measured.
  4. State what is untested: JavaScript-started motion, browsers outside the matrix, low-end devices, screen reader behaviour.
  5. Use the report template. Do not echo unchanged CSS back.

Worked example

An invented motion.css of 12 lines: will-change on *, transition: all, a height transition and an unitless duration, a display transition with no allow-discrete, another with the behaviour longhand before the shorthand, two infinite animations (one pointing at a missing @keyframes), a keyframes block that animates width, an unused keyframes block, and a reduced-motion rule that zeroes every duration with !important.

$ python3 scripts/motion_check.py motion.css
2 errors, 9 warnings, 1 info, 1 files
motion.css:1 warn  will-change-broad  will-change on a broad selector promotes many layers ...
motion.css:2 warn  transition-all  transition on all: list the exact properties
motion.css:3 error duration-no-unit  transition duration has no unit (s or ms) ...
motion.css:3 warn  layout-property-animated  transitions height: layout work each frame
motion.css:4 error discrete-no-allow-discrete  display transition without transition-behavior: allow-discrete ...
motion.css:5 warn  behavior-before-shorthand  transition shorthand after transition-behavior resets it ...
motion.css:6 warn  infinite-no-pause  infinite animation, and its selector has no reduced-motion rule ...
motion.css:7 warn  animation-name-undefined  animation-name orbit has no @keyframes in the scanned files
motion.css:8 warn  layout-property-animated  @keyframes pulse animates width: layout work each frame
motion.css:11 warn  reduced-motion-blunt-reset  global !important reset in reduced motion ...

(Messages shortened, 2 lines omitted.) The agent asks for the durations and is told the project has no tokens and the user picks 200 ms for state changes and 300 ms for panels. It lists the transitioned properties, rewrites the height and width animations as transform, adds units, writes the longhand after the shorthand with allow-discrete, defines the orbit keyframes, deletes the unused block and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. It greps for animationend and finds no handler.

$ python3 scripts/motion_check.py motion.css
0 errors, 0 warnings, 0 info, 1 files

The scanner passing shows that the patterns it knows are gone. It does not show that the frame rate improved or that the reduced version reads well. Both go in the report as untested until measured and shown to the user.

Templates

Report

Motion report: <repo or folder>, <date>
Files: <count>. Browsers: <matrix>. Motion tokens: <file or none>.
motion_check before: N errors, N warnings, N info. After: N errors, N warnings, N info.
Changes: <file:line  code  old -> new>   (one line each)
Durations and easing chosen by the user: <token  value>
Reduced-motion decisions (user approved): <selector  class  reduced behaviour>
Left as is: <file:line  reason>
Untested: JavaScript-started motion, browsers outside the matrix, low-end devices, measured frame rate (edit to match)

Starting prompt

Use the css-animations-transitions skill. Scope: <paths>. Browsers: <matrix>.
Symptom: <stutter | transition does nothing | cannot fade out | no reduced motion | review>.
Motion tokens: <file or none>. Scripts that use animationend, transitionend or WAAPI: <paths or none>.
Essential motion: <list>. Edit on a branch in batches of one file and show each diff first.

Per-component reduced motion

.toast { animation: slide-in var(--motion-panel) ease-out; }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }   /* end state at once; the fade stays on .toast__body */
}

Failure modes and anti-patterns

MistakeHow to spot itDo instead
transition: alltransition-all; unrelated properties animate when a class changesList the properties (step 5)
Animating layout propertieslayout-property-animated; stutter on slow devicestransform or opacity, or FLIP (step 4)
A global reduced-motion kill switchreduced-motion-blunt-reset; feedback vanishes and animationend handlers stallPer-selector rules and a handler check (step 3)
display transition that never animatesdiscrete-no-allow-discrete, behavior-before-shorthandallow-discrete after the shorthand, plus @starting-style (step 6)
will-change everywherewill-change-broadScope it to one element and one moment (step 4)

Quality bar

Run every item before handing over.

  1. motion_check.py errors are 0, and every warning and info line is fixed or listed with a reason.
  2. Every duration and easing value in the changes came from the user, and the report lists each one.
  3. Every remaining animation is classified in step 3 and has a reduced-motion rule written next to it, or the report says why not.
  4. Every animationend and transitionend handler has a fallback when its animation is removed, or the report lists it.
  5. Every animation moved onto transform or opacity has before and after numbers, or the report says it was not measured.
  6. No will-change remains on *, html, body or a tag selector.
  7. The reduced version of each essential animation was shown to the user.
  8. Browsers outside the user's matrix are listed as untested.

Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.

ItemTokensStatusRead condition
A. Description (always loaded)89EstimateEvery session with the skill installed
B. SKILL.md body6,288EstimateWhen the skill fires
C. references/patterns.md1,057EstimateStep 3 for the reduced-motion code, step 4 for FLIP and will-change, step 6 for entry and exit, step 5 for hover patterns, step 7 for the handler check
C. references/appendix.md1,184EstimateOnly for support notes, tool notes, source notes or the rival comparison
Script output (typical run)448EstimateScanner output measured on the worked example (13 lines, 1,567 characters)
D. Typical run (A + B + script output)6,825EstimateCommon path, no references
D. Worst case (A + B + both references + 40 lines of output)9,996EstimateLarge input, every branch. Above 9,000: use head -40 and read one reference at a time
E. Cheaper path6,825EstimateRun the scanner, fix only flagged lines, skip both references and the emulated check

The three rivals I read have bodies of about 1,150 and 440 tokens, or a body that was not visible (estimates from visible text). None ships a scanner and none covers display transitions or checks animationend handlers before removing motion. This skill costs more to load, and in return the scanner finds the patterns so the agent does not read every stylesheet to look for them.

Handoffs

css-custom-properties.md covers registering and typing the properties you animate and the tokens that hold durations. dialog-and-popover.md covers the elements that most need entry and exit motion. keyboard-focus.md covers focus visibility during transitions. All four read different sources, so none repeats another.

Appendix

Reusable code is in references/patterns.md. Support notes, tool notes and the rival comparison are in references/appendix.md. Read them only when a step points there.

Sources

Last verified 7 November 2026 for every source.

What this skill does

Motion code fails in ways that are easy to miss. A duration written as .3 has no unit, so the declaration is invalid. A global animation-duration: 0.01ms !important reset removes feedback people need and stalls any script waiting for animationend. A display transition does nothing without allow-discrete. A will-change rule on * is the bad example in the web.dev compositing article.

The skill runs scripts/motion_check.py, which reports file, line and code for 12 patterns, and the agent edits only the flagged lines in batches you approve. It classifies each animation as essential, movement, fade or loop, writes a reduced-motion rule next to each one, moves layout animations onto transform where the result is the same, and sets up entry and exit with @starting-style. It asks you for every duration and easing value instead of choosing one.

Who it is for

Use it if you maintain a stylesheet full of transition: all, review AI-generated animation, or ship a product with no reduced-motion handling. It fits when an animation stutters, a transition does nothing, or an element cannot fade out because it is display: none. It does not cover GSAP, Framer Motion, the Web Animations API, scroll-driven animation or SVG, and it is not a WCAG conformance statement.

What is inside

The file is 4,135 words (version 1.0.0) with one script and two reference files. It holds:

  • Seven guardrails, including never choosing a duration, showing every reduced version to the user, and no global kill switch as the whole fix.
  • Seven inputs the agent must have first, including which scripts listen for animationend.
  • Eight workflow steps: scan, inert declarations, reduced motion, cheap properties, transitions, entry and exit, JavaScript, verify.
  • One tool, scripts/motion_check.py, a standard-library scanner with 12 codes that prints file, line and a fixed message but never source text.
  • A worked example with real output, a report template, a starting prompt, five failure modes and an eight-item quality bar.
ItemTokensStatusRead when
Description89EstimateEvery session with the skill installed
SKILL.md body6,288EstimateWhen the skill fires
references/patterns.md1,057EstimateSteps 3 to 7, for reduced-motion code, hover patterns, entry and exit, FLIP, will-change or the handler check
references/appendix.md1,184EstimateOnly for support notes, tool notes, source notes or the rival comparison
Typical run6,825EstimateDescription, body and scanner output
Worst case9,996EstimateBoth references and 40 lines of output
Cheaper path6,825EstimateRun the scanner, fix flagged lines, skip the references

The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The three rivals I read have bodies of about 1,150 and 440 tokens or a body I could not see, and none ships a scanner.

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as css-animations-transitions/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. The scanner needs Python 3 and nothing else. Then send the starting prompt from the Templates section.

Before you install. Read the script first. It reads files and writes nothing. The skill tells the agent to edit only on a branch in batches you approve, to take every duration and easing value from you, and to show you each reduced-motion version before applying it. Install skills only from sources you trust.

A worked example

An invented 12-line motion.css has will-change on *, transition: all, a height transition with an unitless duration, two display transitions set up wrongly, two infinite animations (one pointing at missing keyframes), a keyframes block that animates width, an unused keyframes block and a reduced-motion rule that zeroes every duration with !important. The scanner prints 2 errors, 9 warnings and 1 info line.

The agent asks for the durations and is told 200 ms for state changes and 300 ms for panels. It lists the properties, rewrites the height and width animations as transform, adds units, puts allow-discrete after the shorthand, defines the missing keyframes and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. The re-run prints 0 errors, 0 warnings and 0 info. Measured frame rate and the reduced version’s feel are listed as untested.

Common mistakes

The skill is built around five ways this goes wrong:

  • Using transition: all. Unrelated properties animate when a class changes. The skill lists the exact properties.
  • Animating layout properties. Width, height and margin force layout work each frame. The skill moves them onto transform or opacity, or uses FLIP.
  • A global reduced-motion kill switch. Feedback vanishes and animationend handlers stall. The skill writes per-selector rules and greps for handlers.
  • A display transition with no allow-discrete. It never animates. The skill adds the longhand after the shorthand, plus starting-style.
  • will-change on everything. It promotes many layers. The skill scopes it to one element and one moment.

This skill sits in Skills-md, Frontend Development and CSS and Layout. CSS Custom Properties covers registering the properties you animate and the tokens that hold durations. Dialog and Popover covers the elements that most need entry and exit motion. Keyboard Focus covers focus visibility during transitions. CLS Optimisation covers layout shift in Core Web Vitals.

Frequently asked questions

Wrap movement in @media (prefers-reduced-motion: reduce) and set the end state, or put motion inside no-preference so unsupported browsers get none. Reduce means minimise non-essential movement, per web.dev, so keep fades and feedback. WCAG SC 2.3.3 is Level AAA. The skill writes the rule next to each animation and checks animationend handlers.

It animates every property that changes, including ones you never meant to, and it hides what the transition costs. Josh Comeau advises listing the exact properties. The scanner flags transition: all and transition-property: all, and the skill asks you for the list, the duration and the easing instead of choosing them.

Per web.dev, transform and opacity are the only compositor-only properties, though that article was last updated in March 2015, so confirm in your browser’s performance panel. Layout properties such as width, height, top and margin force layout work each frame. The skill flags them and offers transform or FLIP.

Add transition-behavior: allow-discrete after the transition shorthand, list display in the transition, and give the entering state with @starting-style, per Chrome for Developers. Add overlay for dialogs and popovers so they stay in the top layer. The page gives Chrome versions only, so verify support in other browsers.

Common causes are a duration with no unit, such as opacity .3, which is invalid, a property with no start value, an element inserted already in its final state, or display transitioned without allow-discrete. The scanner reports the unit and display cases by file and line, and the skill covers the rest.

No. web.dev calls that reset a workaround for user stylesheets and says it uses 1 ms because some sites rely on animationend. It also removes feedback users need. The skill replaces it with per-selector rules and greps for animationend and transitionend handlers before any animation is removed.

The description is about 89 tokens and loads in every session. The SKILL.md body is about 6,288 tokens when the skill fires. A typical run is about 6,825 tokens with scanner output, and the worst case with both references is about 9,996. All figures are estimates, not from a model tokeniser.

Changelog

  • 1.0.0, 7 November 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.