# Design Engineering

> A free course for people and agents who build UI with AI. Start from zero, learn the words and the craft, take an exam on your own or with your agent, and get certified.

A free course at https://school.agents.org.in, built on the open design-engineering skill (AgentsORG/design-engineering 3.3.1). Every lesson is available as markdown by adding `.md` to its URL.

## For agents

- MCP server (streamable HTTP, no auth): https://school.agents.org.in/mcp — tools: list_modules, get_lesson, search_skill, get_skill_install, get_skill_file, list_exams, start_exam, submit_exam, verify_certificate
- Install the skill: `npx skills add AgentsORG/design-engineering` (or the Claude Code plugin `design-engineering@agentsorg`)
- Exams: call `start_exam` with an exam id (`quiz:<module>`, `exam:<module>`, `final:<course>`) and a candidate, answer, then `submit_exam`. Passing returns a signed certificate URL.
- REST mirror: https://school.agents.org.in/openapi.json
- Every skill node: https://school.agents.org.in/skill/<cluster>/<node>.md

# Welcome

> The difference between a screen that works and one that feels right, and how you will learn to see it and get an agent to build it, starting from zero.

Module: Start from zero · Level: beginner · 5 min · https://school.agents.org.in/foundations/start/welcome

More and more interfaces are built by asking an AI to build them. You describe a screen, an agent (A language model plus a harness: the model decides what to do, the harness gives it tools to read files, write code, run it and check the result) writes the code, and a minute later it runs. That part is solved. What is not solved is the difference between a screen that works and a screen that **feels right**: the button that answers your press, the menu that opens from where you clicked, the error that tells you what to do next.

That difference is called design engineering (The craft between design and code: building the details (motion, states, type, color, copy) that make a working interface feel right), and it is what this lesson starts you on. You will learn it twice over: once as a person who can see and name the details, and once as a person who can get an agent to build them.

*[Interactive demo: Install the skill once; from then on, your agent reviews UI the way a design engineer would.]*

## Who this is for

You do not need to know how to design or how to code. If you have never opened a terminal, the next lessons walk you through it, one click at a time. If you are already a designer or an engineer, skim the first module and start where it gets new.

> **In plain words.** An **agent** is an AI that does work on your computer instead of only chatting: it reads files, writes code and runs it. A **skill** is a folder of notes you hand the agent so it knows how *you* want the work done. This course is built on one skill, called design-engineering.

## How the course is laid out

The course has four parts.

1. **Start here.** Set up an agent, give it the skill, learn the words designers use and get your first screen reviewed.
2. **The craft.** Motion, color, type, layout, components and the tells of AI-made UI. One discipline per module.
3. **Making things.** Product demos, icons, sound, launch films and diagrams.
4. **Working with agents.** How to brief, review and check what your agent builds, and the toolkit of commands that does it.

Every lesson is short. Most have a demo you can watch, a prompt you can copy into your agent, and a question to check yourself. Words that might be new have a dotted underline: hover or tap one, like easing (How speed changes during an animation: start fast and slow down, start slow and speed up, or both. It decides whether motion feels snappy or sluggish), to see what it means in one sentence.

## How to get the most out of it

- **Do the prompts.** Reading about a detail teaches you to recognise it. Asking your agent to build it, and seeing it get it wrong, teaches you to direct it.
- **Learn the words.** The fastest way to better results from an agent is a more precise word. The [vocabulary](/vocabulary) collects every term in the course.
- **Take the quick quiz** at the end of a module, then the exam when you are ready. Pass it and you get a certificate you can share.

> **Take the exam with your agent.** Every exam can be taken on your own, or **with your agent**: connect your agent to this course (one command, covered in [Connect your agent](/learn/start/connect-your-agent)) and it sits the exam with you. Since this course is about building with AI, a certificate earned together says something real: you can direct an agent to do this well.

## For agents reading this

This page is also written for you. Every lesson is available as markdown (add `.md` to the address), the whole course is at [/llms-full.txt](/llms-full.txt), and the [MCP server](/agents) lets you install the skill, study a topic and sit an exam that grades how you apply it.

**Check yourself:** What does this course mean by design engineering?

- [ ] Making an interface work at all
- [x] The details that make a working interface feel right
- [ ] Choosing a color palette
- [ ] Writing the backend for a website

*Why:* An agent can already make a screen work. Design engineering is the layer on top: motion, states, type, color and copy that make it feel considered.


---

# What is design engineering?

> Tell vibe coding from design engineering, and know which on-ramp is yours whether you come from design, from code or from neither.

Module: Start from zero · Level: beginner · 6 min · https://school.agents.org.in/foundations/start/what-is-design-engineering

Two people ask an agent for the same pricing card. Ten minutes later, both have one that works. One of them looks like every other AI-made card on the internet. The other looks like someone cared.

The difference was not the agent. It was what each person did before and after the prompt.

*[Interactive demo: Same request, two processes. Step through both and compare what comes out.]*

## Vibe coding: prompt, accept, hope

Vibe coding (Asking an agent for something, accepting whatever runs and hoping it is good. Fine for throwaways; it ships the agent's average guess) is asking an agent for something, accepting whatever runs, and hoping it is good. It is a fine way to make a throwaway tool on a Saturday. It is a poor way to make something people will use every day.

The trouble is that an agent, left alone, builds the *average* of everything it has seen. The average pricing card has a purple gradient, three buttons of equal weight and light gray text nobody can read. None of that is a bug, so nothing tells you it is wrong.

## Design engineering: decide, direct, check

Design engineering (The craft between design and code: building the details (motion, states, type, color, copy) that make a working interface feel right) is the craft of building the details that make a working interface feel right: the motion, the states, the type, the color and the words. A design engineer works in the real material (code, in a real browser) rather than in pictures of it, and is judged by how the thing feels in the hand.

With an agent, that becomes a loop of three moves:

  **Step: Decide.** Make the calls an agent cannot make for you. One primary button. Body text dark enough to read. The price is the biggest thing on the card.

  **Step: Direct.** Hand those decisions to the agent in exact words, with the design-engineering skill (A folder of written instructions an agent loads when a task matches, so it does the work the way you want instead of its default way) loaded so it knows the craft rules too.

  **Step: Check.** Look at the result. Run a review. Fix what it found, then look again.

**The agent does the typing; you own the judgement.** That split is the whole course in one line.

  **Vibe coding (avoid):**

"Make me a nice pricing card." Accept it. Ship it. Find out from users that the gray text is unreadable on their phone.

  **Design engineering (do this):**

"Pricing card: price at 40px, one dark primary button, a quiet text link for the second action, body text at 4.5:1 contrast." Then `/design-engineering review` and a look at the screen.

## What a design engineer actually does

The job sits in the gap between a design and a shipped product. A designer draws what the screen should look like; an engineer makes it work; and somewhere in between, a hundred small things get decided by nobody. How long the menu takes to open. What happens when the list is empty. Whether the button shows it was pressed.

A design engineer decides those things on purpose. Some days that means prototyping an interaction to see if it feels right before anyone writes a spec. Some days it means building the components a whole team uses. Increasingly, it means steering an agent through all of that.

> **In plain words.** Designers ask "what should this look like?" Engineers ask "how does this work?" Design engineers ask "how does this **feel** to use, and what exactly would make it feel better?"

## Pick your on-ramp

You do not need to be both a designer and an engineer to start. You need to be curious about the gap. Where you begin depends on where you come from.

  **From design:**

You already have the eye: you see when spacing is off or a color is muddy. What is new is the material. Learn to read the code an agent writes well enough to spot a problem, and learn to judge a design **in the browser**, moving, at real sizes, not in a static mockup.

    Start with [Set up your agent](/learn/start/set-up-your-agent), then the Motion module: motion is the part a static design tool never shows you.

  **From code:**

You already have the material: you can build anything. What is new is the eye. Learn the names for what you are looking at ([words matter](/learn/start/words-matter)), and train yourself to notice the details you used to skip: the hover state, the empty state, the 4px that makes two things look grouped.

    Start with [How good interfaces think](/learn/philosophy/taste-is-trained), then the craft modules in order.

  **From neither:**

Good news: you skip the habits the others have to unlearn. Go in order. Set up an agent, learn the words, and get your first screen reviewed. Every lesson assumes nothing.

    Start with [What is an agent?](/learn/start/what-is-an-agent)

*(Your agent can already write code. What it cannot do is want something. That part is you.)*

## The prompt that starts the habit

Before you ask for any new screen, ask the agent to slow down and make you decide. It is the single cheapest habit in this course.

**Make me decide first (paste into your agent):**

```text
Before you build this, ask me up to five questions about what matters most on this screen: the one action people should take, what they need to read first, and what states it can be in (empty, loading, error). Then build it with the design-engineering skill and show me a review table of anything you were unsure about.
```

**Check yourself:** A friend asks an agent for a sign-up form, it runs, and they ship it without looking closely. What were they doing?

- [ ] Design engineering, because it works
- [x] Vibe coding: prompt, accept, hope
- [ ] Directing the agent
- [ ] Checking the result

*Why:* Working is the starting line, not the finish. Without deciding what matters and checking the result, they shipped the agent's average guess.

## In the skill

- https://school.agents.org.in/skill/meta/directing-agents.md
- https://school.agents.org.in/skill/philosophy/feeling-right.md


---

# What is an agent?

> Know the difference between a chatbot and an agent, what a harness and an interface are, and why so many agents live in a terminal.

Module: Start from zero · Level: beginner · 7 min · https://school.agents.org.in/foundations/start/what-is-an-agent

Ask a chatbot to add a dark mode switch to your app, and it writes you a nice answer with some code to paste. Ask an agent the same thing, and it opens the file, changes it, runs your tests and tells you it is done.

Same words in. One gives you homework; the other does the work.

*[Interactive demo: The same request to a chatbot and to an agent. One explains, the other edits the file and runs it.]*

## A model only writes text

Under every chatbot and every agent is a large language model (The AI itself, like Claude or GPT. On its own it only reads text and writes text; it cannot open files or run anything) (an LLM): the AI itself, like Claude or GPT. On its own, a model can do exactly one thing. It reads text and writes more text.

It cannot open a file. It cannot run a command. It cannot see your screen. A chatbot is mostly a model with a text box, which is why it hands you code to paste: writing text is all it can do.

## A harness gives it hands

So how does an agent edit files? Theo Browne explains it well in [What is a harness?](https://x.com/theo/article/2113424203345265053): the model is wrapped in a layer of ordinary code called a harness (The hidden layer of ordinary code around a model: it lists the tools, notices when the model asks for one, runs it and sends the result back. Model + harness = agent).

The harness does four jobs:

  **Step: It lists the tools.** It tells the model which tools (One thing a harness can do for the model, like read_file, edit_file, run a command or search. Most run on your computer; an MCP server's tools run somewhere else) exist, such as `read_file`, `edit_file`, `bash` (run a command) and `search`, and the text format for asking to use one.

  **Step: It watches the model's text.** When the model writes something like `read_file("src/Settings.tsx")`, the harness notices that this is a request, not a sentence.

  **Step: It runs the tool.** The harness, not the model, actually opens the file or runs the command on your computer.

  **Step: It sends the result back.** The file's contents go back to the model, which reads them and writes its next step. Round and round until the job is done.

**An agent is a model plus a harness.** The model decides; the harness does.

*[Interactive demo: Step through one request. It goes down to the model, out to a tool, and back up to you.]*

## The interface is what you see

There is one more layer, the one you actually look at: the interface (The screen you see and type into: the claude screen in a terminal, Cursor's window, a desktop app. Not the same as the harness behind it). It is the screen where you type and read answers. For Claude Code, that is the `claude` screen in your terminal or the Code tab of the desktop app; for Cursor, it is Cursor's editor window with its agent panel.

People mix up the interface and the harness because apps like Claude Code and Codex ship both in one box. But they are different things. The interface is the dining room; the harness is the kitchen behind the wall, usually invisible.

> **The kitchen picture.** The model is the **chef**: it knows how to cook but has no hands of its own here. The harness is the **kitchen**: the counters, the stove, the staff who fetch things. The tools are the **pots and knives**. You are the diner, and the interface is the dining room where you order and get your plate.

## Tools here, and tools over there

Most tools run right on your computer: read this file, run that command. An MCP (Model Context Protocol: a standard way for an agent to use tools that live somewhere else, by sending a request to a server, like this course's exam server) server is a tool that lives somewhere else. Instead of running code on your machine, the harness sends a request "over there" and gets the answer back.

Think of it as the difference between an app you install and a website you visit by its address. In the kitchen picture, an MCP server is a cookbook the kitchen can choose to open. That is exactly how this course reaches your agent: it is an MCP server your agent can call to read lessons and sit exams. You will connect it in [Connect your agent](/learn/start/connect-your-agent).

## Which harness?

Several apps bundle a harness with an interface. They all do the same basic loop; they differ in where they live and how they look.

| App | Where it lives | Good first pick if… |
|---|---|---|
| Claude Code | Your terminal, plus a desktop app | You want the agent this course is tested with most |
| Cursor | A code editor (an app with windows and files) | You would rather click than type commands |
| Codex | Your terminal | You already use OpenAI's tools |
| VS Code | A code editor, with an agent panel | You already have VS Code open all day |

All of them can load the design-engineering skill and connect to this course. Pick one; you can switch later.

## What is a terminal?

Many agents live in a terminal (A window where you type commands to your computer instead of clicking. Many agents live in one): a plain window where you type commands to your computer instead of clicking. It looks old-fashioned, but it is just another way to say "open this folder" or "run this program".

  **Clicking (avoid):**

Open Finder or File Explorer, find Documents, make a new folder, name it `my-app`, double-click into it.

  **Typing in a terminal (do this):**

```
    mkdir my-app && cd my-app
    ```
    Same result, one line. And an agent can type it for you.

That last part is the point. Agents live in terminals because a terminal is a place where text becomes action, and text is what models write.

*(You do not need to learn terminal commands. You need to know how to open one and paste a line into it.)*

**Ask your agent to show its work (paste into your agent):**

```text
Before you change anything, tell me which files you will read, which you will edit and which commands you will run. Then do it, and list each tool call as you make it.
```

**Check yourself:** What turns a language model into an agent that can edit your files?

- [ ] A bigger model
- [x] A harness that gives it tools, runs them and sends the results back
- [ ] A nicer chat window
- [ ] Typing your request in a terminal

*Why:* A model only writes text. The harness lists tools, notices when the model asks for one, runs it on your computer and returns the result. Agent = model + harness.

## In the skill

- https://school.agents.org.in/skill/meta/directing-agents.md


---

# Set up your agent

> Go from nothing installed to an agent building your first screen, on a Mac or on Windows, in about fifteen minutes.

Module: Start from zero · Level: beginner · 8 min · https://school.agents.org.in/foundations/start/set-up-your-agent

By the end of this page you will have typed one sentence and watched an agent make a web page from it. Here is where you are heading:

*[Interactive demo: An empty folder, one sentence, a first screen.]*

Four steps get you there: open a terminal, install Node.js, install an agent, and ask it for something. Each one is a few clicks or one pasted line.

## 1. Open a terminal

A terminal (A window where you type commands to your computer instead of clicking. Many agents live in one) is the window where you type commands. Every computer already has one.

  **Mac:**

Press <kbd>Cmd</kbd> + <kbd>Space</kbd>, type **Terminal** and press <kbd>Enter</kbd>. A window opens with a line ending in `%` or `$`. That line is the prompt: it is waiting for you to type.

  **Windows:**

Press the <kbd>Windows</kbd> key, type **Terminal** and press <kbd>Enter</kbd>. (On older Windows, type **PowerShell** instead.) A window opens with a line ending in `>`. That is the prompt, waiting for you.

To run a command, paste it after the prompt and press <kbd>Enter</kbd>. To paste in a terminal, use <kbd>Cmd</kbd> + <kbd>V</kbd> on a Mac, or right-click (or <kbd>Ctrl</kbd> + <kbd>V</kbd>) on Windows.

## 2. Install Node.js

Node.js (A free program that lets your computer run JavaScript tools. Installing it also gives you npm, which installs agents like Claude Code and Codex) lets your computer run JavaScript tools, and it comes with npm (The tool that comes with Node.js for installing JavaScript packages. npx runs a package once without installing it for good), the tool that installs them. Claude Code and Codex are installed with npm, and most web projects need Node anyway.

  **Step: Download it.** Go to [nodejs.org](https://nodejs.org) and download the version marked **LTS** (long-term support, the steady one).

  **Step: Install it.** Open the file you downloaded and click through the installer with the default choices.

  **Step: Check it worked.** Close your terminal, open a new one, and run the line below. You should see a version number like `v22.x.x`.

```bash
node -v
```

> **If it says “command not found”.** The terminal you had open started before Node existed. Close it completely and open a fresh one. That fixes it nine times out of ten.

## 3. Install an agent

Pick one. They all work with this course; you can try another later.

  **Claude Code:**

Install it, then start it:

    ```bash
npm install -g @anthropic-ai/claude-code
```
    ```bash
claude
```

    The first time, it opens your browser to sign in. **You need a Claude account with a paid plan (Pro or Max) or Anthropic API credit.** On Windows, Claude Code also needs [Git for Windows](https://git-scm.com/downloads/win) installed; it will tell you if it is missing.

  **Cursor:**

Cursor is an app, not a command.

    1. Download it from [cursor.com](https://cursor.com) and install it like any app.
    2. Sign in. **Cursor needs a free account**; heavy use needs a paid plan.
    3. Choose **File → Open Folder** and pick a folder (make one first, below).
    4. Open the agent panel with <kbd>Cmd</kbd> + <kbd>L</kbd> on a Mac or <kbd>Ctrl</kbd> + <kbd>L</kbd> on Windows, and make sure it is set to **Agent**.

  **Codex:**

Install it, then start it:

    ```bash
npm install -g @openai/codex
```
    ```bash
codex
```

    The first time, it asks you to sign in. **You need a ChatGPT account on a paid plan, or an OpenAI API key.** Windows support is newer; if it gives you trouble, OpenAI's docs suggest running it inside WSL (Linux on Windows).

  **VS Code:**

1. Download [VS Code](https://code.visualstudio.com) and install it.
    2. Choose **File → Open Folder** and pick a folder (make one first, below).
    3. Open the Chat view from the title bar and switch it to **Agent** mode.
    4. Sign in with GitHub when asked. **Agent mode uses GitHub Copilot, which has a free plan.**

*(Every one of these needs an account. That is the price of the model, not of this course, which is free.)*

## 4. Make a project folder

An agent works inside one folder at a time, and it can change anything in it. So give it a fresh, empty folder of its own. In your terminal:

```bash
mkdir my-first-app
```
```bash
cd my-first-app
```

`mkdir` makes a directory (a folder); `cd` changes into it. Now start your agent here (`claude` or `codex`), or open this folder in Cursor or VS Code.

## 5. Ask for your first screen

Type a plain sentence. Be specific about what is on the screen and keep it small.

**Your first screen (paste into your agent):**

```text
Make a sign-up screen in one file, index.html: a heading, one line of text under it, an email field and one button. Plain HTML and CSS, no frameworks. Then tell me how to open it.
```

The agent will ask permission before it writes the file or runs anything. Read what it wants to do and say yes. When it is done, open `index.html` in your browser (double-click it in your file manager, or ask the agent to open it).

  **Too vague (avoid):**

"Make me a website." The agent has to guess everything, so it guesses the average: a hero, three cards, a footer.

  **Small and specific (do this):**

"A sign-up screen: heading, one line, email field, one button, in index.html." You can check every part of the answer at a glance.

It will look plain, maybe a little generic. That is fine: in [Your first review](/learn/start/your-first-review) you will make it better, with the skill doing half the work.

**Check yourself:** Why should you start your agent inside a new, empty folder?

- [ ] It runs faster there
- [x] An agent can change anything in the folder it is in, so give it one of its own
- [ ] Agents cannot open folders that have files
- [ ] npm needs an empty folder

*Why:* The agent can read, edit and delete files in its folder. A fresh project folder keeps its work contained and easy to see.

## In the skill

- https://school.agents.org.in/skill/meta/directing-agents.md


---

# What is a skill?

> Understand what a skill is, how an agent decides to load one, and how design-engineering routes every request through one entry point to the right note.

Module: Start from zero · Level: beginner · 6 min · https://school.agents.org.in/foundations/start/what-is-a-skill

Tell your agent "this dropdown feels slow" and, with the design-engineering skill installed, it does not guess. It opens a note about easing curves, reads that a menu should take about 200ms with a strong ease-out, and fixes it to that. Try a few requests and watch where each one lands:

*[Interactive demo: Every request goes in through one door and lands on the note that owns it.]*

## A folder of notes

A skill (A folder of written instructions an agent loads when a task matches, so it does the work the way you want instead of its default way) is a folder of plain text files written for an agent. At the top sits one file, `SKILL.md`, with a short description of when the skill applies. Beside it are the notes themselves.

That is all it is. No code to run, no magic. Just instructions, written down once, so the agent does the work the way an expert would instead of the way the average of the internet would.

```
design-engineering/
  SKILL.md              ← "load me when reviewing UI, picking easing…"
  references/
    motion/easing-curves.md
    motion/duration-table.md
    surface/contrast-and-color-scheme.md
    … about 200 more
```

## How the agent knows when to load it

Your agent reads only the short description of every installed skill at first. When your request matches one ("review this card", "why does this feel off"), it opens that skill's `SKILL.md`. From there it follows links to the one or two notes the job needs, and ignores the rest.

> **In plain words.** A skill is like a shelf of recipe cards. The agent reads the labels on the spines all the time, but only pulls out a card when you ask for that dish, and only reads the cards that recipe needs.

This matters because an agent can only hold so much in mind at once. Loading two notes about motion beats loading two hundred notes about everything.

## One skill, one door

design-engineering is a single skill with a single entry point. Whatever you want, you start the same way:

```bash
/design-engineering [verb] [target]
```

The slash command (A command you type into an agent starting with /, like /design-engineering. It runs a skill or a built-in action instead of being read as a normal message) `/design-engineering` opens the skill. What you type after it decides where it goes:

  **Step: A verb.** If the first word is a verb (The first word after /design-engineering, like review, polish or motion. It picks which procedure the skill follows) like `review`, `polish`, `motion` or `tells`, the agent opens that command and follows it step by step. `/design-engineering review src/PricingCard.tsx` reviews that file.

  **Step: A plain request.** If you just describe the problem, a router note reads it, decides which part of the craft owns it, and hands off. `/design-engineering why does this card feel flat`.

  **Step: Nothing at all.** On its own, `/design-engineering` shows a menu of verbs, leading with the two or three that fit what you are doing. It never runs one without asking.

## Thirteen clusters, two hundred notes

Behind the door, the notes are grouped into 13 clusters (A group of related notes in the design-engineering skill, one per part of the craft: motion, typography, layout and so on. There are 13), one per part of the craft: philosophy, motion, surface (color and depth), typography, layout, components, anti-patterns (the tells of AI-made UI), faux UI, SVG, sound, launch video, diagrams, and meta (how to review and direct).

Each note is a node (One note inside a skill: one idea with exact values and links to related notes, like "never scale from zero"): one idea, a few hundred words, with exact values and links to related nodes. "Never scale from zero" is a node. "The duration table" is a node. **The modules of this course follow the same clusters**, so what you learn here is exactly what your agent reads.

  **Agent without the skill (avoid):**

"The dropdown feels slow." It tries `transition: all 0.5s ease-in-out`, because that is common. Now it feels slower.

  **Agent with the skill (do this):**

Routes to `motion/easing-curves`. Sets `transform` and `opacity` only, 200ms, `cubic-bezier(0.23, 1, 0.32, 1)`, origin at the button. Tells you why.

*(Most of the time you will not type a verb at all. Describe the problem in plain words; the router is there for that.)*

## Your own taste, on top

Two files in the skill are meant for you to edit: `pov.md`, where you write opinions that override the skill's defaults, and `gotchas.md`, where you jot down mistakes the agent made so it never makes them again. You will not need them on day one. Just know the skill is a starting point you can make your own.

**See the router think (paste into your agent):**

```text
/design-engineering which: I want the settings page to feel less cramped. Tell me which cluster and which node own this, and why, before you change anything.
```

**Check yourself:** You type /design-engineering with nothing after it. What happens?

- [ ] It reviews every file in the project
- [x] It shows a menu of verbs, leading with the ones that fit, and waits
- [ ] It installs the skill again
- [ ] Nothing; you must always give a verb

*Why:* With no arguments and no request on the table, the skill shows its verb menu and waits for you to pick. It never auto-runs a verb.

## In the skill

- https://school.agents.org.in/skill/meta/skill-router.md
- https://school.agents.org.in/skill/meta/routing-table.md


---

# Install design-engineering

> Install the skill one way (skills.sh or the Claude Code plugin), check that it loaded, and use its menu.

Module: Start from zero · Level: beginner · 5 min · https://school.agents.org.in/foundations/start/install-design-engineering

Once the skill is in, one line brings up its menu. If you see this, everything worked:

*[Interactive demo: Type the entry point with nothing after it: the menu comes back, leading with what fits right now.]*

There are two ways to get there. **Pick one.** Both install the same skill; they differ in how you call it afterwards.

## Way 1: skills.sh (any agent)

skills.sh (An installer that copies a skill into the place your agents look for it (Claude Code, Cursor, Codex and others), with one npx command) is an installer that puts a skill where your agent looks for it: Claude Code, Cursor, Codex and many more. Run this in your terminal, inside your project folder:

```bash
npx skills add AgentsORG/design-engineering
```

`npx` runs a tool from npm without installing it for good, so you need [Node.js](/learn/start/set-up-your-agent) for this. The installer asks which agents to link the skill into; pick the one you use.

Afterwards, you call it as:

```bash
/design-engineering
```

## Way 2: the Claude Code plugin

If you use Claude Code, you can install it as a plugin (A package that adds skills, commands or helpers to a harness in one install. design-engineering ships as a Claude Code plugin and as a plain skill) instead. A plugin can bring extra helpers along with the skill, such as specialist reviewers for motion or accessibility. These two lines go **inside Claude Code** (after you have run `claude`), not in a plain terminal:

```bash
/plugin marketplace add AgentsORG/design-engineering
```
```bash
/plugin install design-engineering@agentsorg
```

The first line adds the marketplace (A published list of plugins you add to Claude Code once, so you can install plugins from it by name) (a list of plugins someone publishes); the second installs this plugin from it. Plugins name their commands after themselves, so afterwards you call it as:

```bash
/design-engineering:design-engineering
```

  **Both ways at once (avoid):**

Two copies of the skill. Your agent sees two that match every design request, loads both, and may follow two slightly different versions. Confusing and wasteful.

  **One way (do this):**

One copy, one entry point. If you switch, remove the old one first: `npx skills remove design-engineering`, or `/plugin uninstall design-engineering@agentsorg` in Claude Code.

> **Which should I pick?.** Using Claude Code? The plugin is the fuller install. Using Cursor, Codex or several agents? skills.sh. Not sure? skills.sh works everywhere.

## Check that it loaded

Start a fresh agent session (skills are read when a session starts, so restart after installing). Then type the entry point with nothing after it:

  **Claude Code:**

Type `/design-engineering` (skills.sh) or `/design-engineering:design-engineering` (plugin). As you type the slash, it should appear in the list of commands. Press <kbd>Enter</kbd> and the menu comes back.

  **Cursor:**

In the agent panel, type `/design-engineering` and send it. If the menu of verbs comes back, it loaded. If not, check that you picked Cursor when skills.sh asked which agents to link.

  **Codex:**

Type `/design-engineering` and send it, or just ask "Use the design-engineering skill: show me its menu." The verbs should come back.

  **VS Code:**

In Agent mode, type `/design-engineering` or ask "Use the design-engineering skill: show me its menu." The verbs should come back.

If the agent answers with a general reply about design instead of the menu, the skill did not load. Restart the session; if that does not help, run the install again and read what it prints.

## The no-argument menu

The menu is your map. It lists every verb with a one-line job, and it puts the two or three that fit first: an unfinished change in your project suggests `review`, a `.design` file suggests `tokens`, a video file suggests `film review`.

| Verb | What it does |
|---|---|
| `review` | Reviews UI code as a Before / After / Why table |
| `polish` | For screens that work but feel unfinished |
| `motion` | Should it move? Then the timing, easing and springs |
| `tells` | Lists what makes a screen look AI-made, to delete |
| `a11y` | An accessibility audit with evidence |
| `ship` | A whole-app pass before launch |

**It never runs a verb on its own.** It shows the menu and waits for you to pick, or to describe the job in plain words.

*(Remember which way you installed. Every prompt in this course says `/design-engineering`; with the plugin, read it as `/design-engineering:design-engineering`.)*

**Confirm the install (paste into your agent):**

```text
Is the design-engineering skill loaded? If it is, show me its menu with no verb and tell me which two verbs fit this project right now. If it is not, tell me exactly what you see instead.
```

**Check yourself:** You installed the skill with skills.sh, then also installed the Claude Code plugin. What should you do?

- [ ] Nothing, two copies are more thorough
- [x] Remove one, so the agent loads a single copy
- [ ] Delete Node.js
- [ ] Rename one of them

*Why:* Install one way only. Two copies both match every design request, so the agent may load both and follow two versions.

## In the skill

- https://school.agents.org.in/skill/commands/MOC-commands.md
- https://school.agents.org.in/skill/meta/skill-router.md


---

# Connect your agent

> Connect this course's MCP server to your agent so it can study lessons and sit exams, alone or with you as a pair.

Module: Start from zero · Level: beginner · 6 min · https://school.agents.org.in/foundations/start/connect-your-agent

This course is also a server your agent can talk to. Connect it once, and your agent can read any lesson, install the skill and sit an exam, with you watching every answer before it is handed in:

*[Interactive demo: An exam taken as a pair: the agent answers, shows you its work, and submits only when you say yes.]*

## What you are connecting

In [What is an agent?](/learn/start/what-is-an-agent) you met tools (One thing a harness can do for the model, like read_file, edit_file, run a command or search. Most run on your computer; an MCP server's tools run somewhere else): things the harness can do on your computer. An MCP (Model Context Protocol: a standard way for an agent to use tools that live somewhere else, by sending a request to a server, like this course's exam server) server is a set of tools that live somewhere else. This course is one, at this address:

```bash
https://school.agents.org.in/mcp
```

Once connected, your agent gets eight new tools:

| Tool | What it does |
|---|---|
| `list_modules` | Lists every module and lesson |
| `get_lesson` | Reads one lesson |
| `get_skill_install` | Says how to install the skill in this agent |
| `get_skill_file` | Reads one note from the skill |
| `list_exams` | Lists every quiz and exam |
| `start_exam` | Starts a sitting and returns the questions |
| `submit_exam` | Hands in the answers and gets the grade |
| `verify_certificate` | Checks a certificate is real |

## Connect it

One command or one small file, depending on your agent.

  **Claude Code:**

Run this in your terminal (not inside Claude Code):

    ```bash
claude mcp add --transport http design-course https://school.agents.org.in/mcp
```

    Then start `claude` again and type `/mcp`: `design-course` should be listed as connected.

  **Cursor:**

In your project folder, make a folder called `.cursor` and a file inside it called `mcp.json` with exactly this:

    ```json
    {"mcpServers":{"design-course":{"url":"https://school.agents.org.in/mcp"}}}
    ```

    Then open **Cursor Settings → MCP**: `design-course` should be listed with a green dot.

  **Codex:**

Run this in your terminal:

    ```bash
codex mcp add design-course --url https://school.agents.org.in/mcp
```

    Then start `codex` again; `/mcp` lists the servers it can see.

  **VS Code:**

In your project folder, make a folder called `.vscode` and a file inside it called `mcp.json` with exactly this:

    ```json
    {"servers":{"design-course":{"type":"http","url":"https://school.agents.org.in/mcp"}}}
    ```

    VS Code shows a small **Start** button above the server in that file. Press it, then use Agent mode.

> **Check it worked.** Ask your agent: "Use the design-course MCP server and list its modules." If it replies with the course's modules, you are connected.

## Two ways to sit an exam

Every exam has multiple-choice questions and **practical tasks**: small pieces of code to fix, which are checked automatically. You can sit it two ways.

  **The agent alone (avoid):**

Your agent sits the exam by itself and earns a certificate in its own name, with its harness and model printed on it. Useful for testing how well an agent applies the skill.

  **You with your agent (do this):**

A **pair** certificate, in your name. The agent answers, but shows you its practical answers before it submits, and you decide. This one says you can direct an agent to do the work well.

**The pair certificate is the one this course is really about.** Building UI with AI is a team of two; the exam should be too.

## The prompt to paste

Pick an exam (each module has one, like `exam-motion`, and there is a final exam across everything). Then paste this into your agent, with your own name:

**Take an exam as a pair (paste into your agent):**

```text
Use the design-course MCP server. Take the exam \`exam-motion\` as a pair for <your name>: install the skill first, answer each question yourself, show me your practical answers before you submit, then give me the certificate link.
```

Here is what happens next:

  **Step: It installs the skill.** The agent calls `get_skill_install` and sets up design-engineering, because the practical tasks test whether it applies the skill.

  **Step: It starts the exam.** `start_exam` returns the questions. The answers never leave the server.

  **Step: It shows you its work.** Before it submits, it shows its practical answers. Read them. If one looks wrong, say so; that is your half of the pair.

  **Step: It submits and gets the grade.** `submit_exam` grades everything at once and, if you passed, returns a certificate link you can share.

*(Say no if an answer looks wrong. A pair that never pushes back is just the agent alone with your name on it.)*

## Just studying

You do not have to sit an exam to use the server. It is a good study partner too:

**Study with the course (paste into your agent):**

```text
Use the design-course MCP server. Read the lesson on easing curves, then quiz me with three questions from it, one at a time, and explain anything I get wrong.
```

**Check yourself:** What makes a pair certificate different from an agent-alone one?

- [ ] It has more questions
- [x] The agent shows you its practical answers before submitting, and the certificate is in your name
- [ ] You answer without the agent
- [ ] It does not need the skill installed

*Why:* In a pair, the agent does the answering but you review its work before it is handed in, so the certificate shows you can direct an agent.


---

# Words matter

> Learn why the exact word you use changes what an agent builds, and how to turn "make it pop" into a request it can get right.

Module: Start from zero · Level: beginner · 6 min · https://school.agents.org.in/foundations/start/words-matter

Here is a small dialog and four ways to ask an agent to improve it. Tap each one and watch what the agent does with your words:

*[Interactive demo: The vaguer the words, the more the agent guesses. Its guess is the average of everything it has seen.]*

"Make it pop" got a gradient, a glow and a button that grew. The agent did exactly what you asked. The problem is you did not ask for anything in particular, so it filled the gap with the most common idea of "pop" it knows.

## Vague words get average results

An agent cannot see what you see. When you say "make it pop", it has to guess what "it" is and what "pop" means, and its best guess is the middle of everything it was trained on. That middle has a name in this course: AI-default UI.

The fix is not a longer prompt. It is a **more exact** one.

  **A wish (avoid):**

"Make the button pop."

  **A spec (do this):**

"Raise the contrast of the primary button's text to 4.5:1. Keep the secondary button quiet."

The second one names *which* element, *what* property and *how much*. There is nothing left to guess, so there is nothing to get wrong.

## What design vocabulary is

Design vocabulary (The exact names for how interfaces look and move, like tracking, easing or hierarchy. The right word gets you the right result from an agent) is the set of exact names for how interfaces look and move. Designers use these words to talk to each other quickly; you can use them to talk to an agent precisely.

A few you will meet soon:

| You might say | The exact word | What it means |
|---|---|---|
| "the text is hard to read" | contrast ratio (How different the text color is from its background, as a number like 4.5:1. Body text needs at least 4.5:1 to be readable for most people; large text 3:1) | How much darker the text is than its background. 4.5:1 is the minimum for body text. |
| "the letters are squished" | tracking | The space between all the letters in a line |
| "the menu feels slow" | easing (How speed changes during an animation: start fast and slow down, start slow and speed up, or both. It decides whether motion feels snappy or sluggish) | How speed changes during an animation: fast start, gentle stop |
| "the numbers jump around" | tabular figures | Digits that are all the same width, so columns line up |
| "the main button" | primary action (The one thing you most want people to do on a screen, usually the one filled, strongest button. A screen should have one) | The one thing you most want people to do on this screen |

You do not need to memorise them. You need to know they exist, so that when something feels off you can look up its name. The [vocabulary page](/vocabulary) collects every term in this course, each with a phrase you can hand an agent.

> **In plain words.** "It feels off" is a feeling. "The line height is too tight" is a fix. The gap between them is not talent, it is knowing the word.

## Point, don't describe

Sometimes the best word is no word at all. Telling an agent "the blue button in the sidebar" sends it searching through your files. Showing it the exact thing sends it straight there.

  **Step: Name the file.** "In src/components/PlanCard.tsx…" beats "on the pricing page".

  **Step: Show a picture.** A screenshot with a circle around the problem beats a paragraph describing it.

  **Step: Link a reference.** "Make the hover like Linear's sidebar" beats five adjectives.

**Point at the exact thing, then name exactly what is wrong with it.** The [philosophy module](/learn/philosophy/say-it-precisely) goes deeper, including tools that let you click an element and hand the agent its exact address.

## The precise version is often plainer

Precision is not jargon. "The button has no hover state" is precise and plain. "The affordance lacks an interactive feedback layer" is just harder to read. Use the shortest words that leave nothing to guess.

*(When you are stuck for the word, ask the agent: "what is it called when…". The skill has a verb for exactly that.)*

**Find the word (paste into your agent):**

```text
/design-engineering name: the numbers in my pricing table shift sideways when they change. What is this called, and what is the exact CSS fix?
```

And when you know what you want, say it like a spec:

**A precise request (paste into your agent):**

```text
On the upgrade dialog, make the primary button's text contrast at least 4.5:1 against its background, keep "Not now" as a quiet text button, and change nothing else. Tell me the contrast ratio before and after.
```

**Check yourself:** Which request will an agent most reliably get right?

- [ ] Make the card look more premium
- [ ] Make it pop
- [x] Raise the card title to 20px semibold and the body text contrast to 4.5:1
- [ ] Make it look like a real designer made it

*Why:* It names the element, the property and the amount. The others are wishes, so the agent fills them with its average guess.

## In the skill

- https://school.agents.org.in/skill/meta/design-vocabulary.md
- https://school.agents.org.in/skill/philosophy/articulate-precisely.md


---

# Your first review

> Ask an agent for a pricing card, review it with the skill, read the Before / After / Why table and apply the fixes.

Module: Start from zero · Level: beginner · 8 min · https://school.agents.org.in/foundations/start/your-first-review

This is the loop you will use for the rest of the course: build, review, fix, look. Here is the pricing card an agent made on its first try, and the review the skill wrote for it. Apply the fixes one at a time and watch the card change:

*[Interactive demo: Each row of the review is one change. Apply them one by one, then press the buttons to feel row 4.]*

Every row was small. Together they turn a card that looks generated into one that looks considered. Now do it yourself, in your own project.

## 1. Ask for a pricing card

In your project folder, with your agent running, ask for something small enough to check at a glance:

**Build the card (paste into your agent):**

```text
Make a pricing card component for a "Pro" plan at $24/month: the plan name, the price, three features and two buttons, "Start free trial" and "Compare plans". Put it on a page I can open in the browser. Don't use the design-engineering skill yet.
```

The last line is on purpose. You want to see the agent's default first, so you can see what the review changes.

Open the page and take a screenshot. You will want it later.

## 2. Run the review

Now ask the skill to review what it made. Give it the file it just wrote:

```bash
/design-engineering review src/PricingCard.tsx
```

(Use whatever file name the agent created. With the plugin install, type `/design-engineering:design-engineering review …`.)

The `review` verb (The first word after /design-engineering, like review, polish or motion. It picks which procedure the skill follows) reads the code against a checklist of a few dozen common mistakes: animating everything, gray text that fails contrast, missing focus styles, buttons that do not respond to a press. It does not change anything yet. It reports.

## 3. Read the table

The review always comes back in the same shape: a Before / After / Why (The table a design-engineering review comes back in: the exact line, its replacement, and one sentence of reason. One problem per row) table. One row per problem.

| Before | After | Why |
|---|---|---|
| `color: #9ca3af` | `color: oklch(0.45 0 0)` | Body text was 2.5:1 contrast; now about 7:1 |
| `transition: all 300ms` | `transition: transform 160ms` | Animate only what moves |
| `outline: none` | a 2px `:focus-visible` ring | Keyboard users can see where they are |

Each column has a job:

  **Step: Before.** The exact line from your code, quoted. So you can find it.

  **Step: After.** The replacement. Same job, better result.

  **Step: Why.** One sentence of reason. If the agent cannot say why in a sentence, the change is probably not worth making.

**Read the Why column first.** It is where you learn. After a few reviews you will start spotting these problems before the agent does.

> **You can disagree.** A review is advice, not orders. If a row is wrong for your design ("no, I want the price small, it is a quiet page"), say so. If you disagree with the skill often in the same way, write it in `pov.md` and it will stop suggesting it.

## 4. Apply the fixes

Once you have read the table, ask the agent to apply it:

**Apply the review (paste into your agent):**

```text
Apply every row of that review table except any you think is wrong for this card; tell me which you skipped and why. Then show me the page again.
```

Reviews only report; you decide what changes. That is on purpose: the skill keeps "judge" verbs like `review` separate from "do" verbs like `polish`, so nothing is changed behind your back.

## 5. Compare before and after

Open the page again and put it next to your screenshot from step 1. Look for:

  **Before (typical) (avoid):**

Light gray text that is hard to read. A small price. Three buttons fighting for attention. Nothing happens when you press a button. No visible focus when you press <kbd>Tab</kbd>.

  **After (do this):**

Text you can read. The price is the first thing you see. One obvious button. Buttons that dip slightly when pressed. A clear ring when you <kbd>Tab</kbd> through.

Try the keyboard too: press <kbd>Tab</kbd> to move between the buttons. If you cannot see where you are, the focus row was skipped.

*(Look at the screen, not just the table. A review that reads well and looks wrong is still wrong.)*

## The loop

That is it: build, review, apply, look. It works on any screen, and the next modules teach you what each row in a review means, so you can direct the fixes yourself instead of only accepting them.

**Check yourself:** In a Before / After / Why review, why does every row need a Why?

- [ ] To make the table longer
- [x] So you can judge whether the change is right, and learn the rule for next time
- [ ] Because the agent cannot apply a fix without it
- [ ] To link to the documentation

*Why:* The Why is what lets you accept, reject or learn from a change. A change nobody can explain in a sentence is usually not worth making.

## In the skill

- https://school.agents.org.in/skill/commands/cmd-review.md
- https://school.agents.org.in/skill/meta/review-format.md
- https://school.agents.org.in/skill/meta/review-checklist.md


---

# Taste is trained

> See why the details nobody notices decide whether people trust a product, and how to build the taste to spot them.

Module: How good interfaces think · Level: beginner · 7 min · https://school.agents.org.in/design/philosophy/taste-is-trained

Here is a small receipt card with six details switched on. Switch them off one at a time: each one alone is almost invisible. Then press All off, and All on again:

*[Interactive demo: Six details nobody would name. Together they are the difference between fine and nice.]*

Nobody using this card would say "lovely tabular figures". They would say "this feels nice", or nothing at all, and keep using it. That is the whole idea of this module.

## Unseen details compound

Paul Graham made the point first and Emil Kowalski sharpened it for interfaces: **the details people consciously notice are mostly noise; the ones they don't notice are what earn their trust.**

Each invisible detail does one of two things. It removes a tiny bit of friction (the amounts line up, so you compare them without effort), or it signals that someone cared (the button answers your press). People cannot point at either. They just feel the product is considered.

  **Details nobody tuned (avoid):**

Text in pure `#000`. One heavy gray shadow. Amounts that wobble because every digit is a different width. A button that does nothing when pressed.

  **Details someone tuned (do this):**

Text in a deep gray. Three soft shadow layers at 4 to 8% opacity. `font-variant-numeric: tabular-nums`. `transform: scale(0.97)` on press, 160ms.

Twenty of these and a product stops feeling generated. That is why the skill exists: to write the unseen details down so an agent applies them every time.

> **Know when to stop.** Detail work can turn into procrastination. Three days on the loading spinner of a page people see once a quarter is not craft; it is avoidance. Spend detail time where people spend their time.

## Beauty is leverage

Most software does the same job as its competitors. There are dozens of to-do apps, chat apps and CRMs with the same features. When the feature list is a tie, people choose on feel, and feel is made of exactly these details.

Emil's framing is that polish (The small, mostly invisible details (press feedback, aligned numbers, soft shadows, real copy) that make a working product feel considered) is not a cost; it is leverage. Features plateau, competitors copy them in a sprint, but the last few percent of polish is slow and hard to copy. So "polish is not a priority right now" usually has it backwards: once the features are there, polish is what sets you apart.

Two limits keep this honest:

  **Step: Correct first.** A broken product with lovely animations is still broken, and looks worse for it. Polish multiplies the trust that working software earns; it never replaces it.

  **Step: Invisible, not theatrical.** The best polish is the kind nobody sees. Confetti on every click is not polish; a button that responds in 160ms is.

## Taste is trained, not born

So who decides which details matter? Someone with taste (A trained instinct for what works, built by looking closely at a lot of good work. Not a gift you are born with), and taste is not a gift. It is a trained instinct, built the way any instinct is: by looking at a lot of good work, closely, for a long time.

The people whose work feels obviously right (Emil Kowalski, Benji Taylor, the Vercel design team) share one habit. They collect references: screenshots, screen recordings, products opened just to feel how a menu moves. That collection is their training set.

  **Step: Keep a reference library.** When an interaction stops you, save it: a screenshot, a recording, a link. Rewatch them when you are stuck.

  **Step: Use constraints.** Two fonts, three colors, one accent. Fewer choices force real decisions; unlimited choice produces average work.

  **Step: Imitate before you invent.** Your first hundred buttons should look like buttons. A button that copies a well-made one is more tasteful than one that "innovates" with a gradient and an odd shape.

  **Step: Expect the first version to be ugly.** Taste compounds with care. Version three is where things start to feel inevitable.

*(Imitating considered work is taste. Reinventing a solved problem is not.)*

## What this means with an agent

An agent has seen everything and collected nothing. It reproduces the average of all the buttons it has read, which is why its first try looks like everyone else's. Your reference library is what pulls it away from the average.

**Hand the agent your references (paste into your agent):**

```text
Here are three references for this settings page: [links or screenshots]. Before building, tell me in one line each what these three have in common (spacing, type, how they handle buttons). Then build the page to match those qualities, not the average settings page.
```

And when a screen is "done", spend the last pass on the unseen:

**The unseen pass (paste into your agent):**

```text
This screen works. Now find the details nobody would notice but everyone would feel: numbers that should use tabular figures, buttons without press feedback, pure black text, a single heavy shadow, uppercase labels without tracking. List them as a Before / After / Why table, then apply them.
```

**Check yourself:** Two apps have the same features. Why do people often prefer one without being able to say why?

- [ ] It has more features hidden in menus
- [x] Its unseen details remove friction and signal care, and those compound
- [ ] It uses a brighter color palette
- [ ] It animates more things

*Why:* When features tie, feel decides. Feel is made of details people do not consciously notice, and their effect adds up.

## In the skill

- https://school.agents.org.in/skill/philosophy/taste-is-trained.md
- https://school.agents.org.in/skill/philosophy/unseen-details-compound.md
- https://school.agents.org.in/skill/philosophy/beauty-is-leverage.md


---

# Delight has a budget

> Decide how much delight a moment can carry from how often people see it, tell marketing UI from product UI, and test whether a screen feels right.

Module: How good interfaces think · Level: beginner · 7 min · https://school.agents.org.in/design/philosophy/delight-has-a-budget

Move the slider from "every keystroke" to "rare milestone" and try each example. Notice how much each moment is allowed to do:

*[Interactive demo: The more often people see something, the less it may perform. Rare moments can celebrate.]*

Typing gets nothing. A button gets a tiny dip. Finishing onboarding gets confetti. Swap any two and it feels wrong: confetti on every save would be unbearable by Wednesday.

## The delight-impact curve

This is Benji Taylor's rule, and it is the most useful single question in this course: **how often will someone see this?**

Delight (A small flourish that makes a product feel alive, like a tick on save or confetti at the end of onboarding. It wears out the more often you see it), the small flourishes that make a product feel alive, wears out with repetition. Something you see once a year stays fresh; something you see a hundred times a day becomes friction by lunchtime.

| How often | Delight allowed |
|---|---|
| Every keystroke, every scroll | None. Any animation is friction. |
| Every action (a button press, hovering a row) | Subtle: about 100 to 160ms, a slight press. |
| Daily (save, send) | Small: a tick, a brief highlight. |
| Weekly (new project, share) | Medium: a short stagger or scale-in. |
| Rare milestones (signup done, first export) | Big. Confetti is allowed here. |

So when someone says "let's make this more fun", ask how often people see it. For anything frequent, the right answer is usually *less* visible, not more.

> **The one exception.** First-run moments (onboarding) are rare for each person but matter a lot, so they can carry more delight than their frequency suggests. Once onboarding is over, the budget drops back to normal.

## Marketing UI is not product UI

The same rule explains a mistake you will see everywhere: building the inside of an app like a landing page, or the reverse.

A marketing page (Pages people see once or twice, like a landing page. Their job is to explain and impress, so they can take bigger visual risks) is visited once or twice. Its job is to impress and explain. A product screen (Screens people use hundreds of times inside an app. Their job is to get out of the way: calm, consistent and fast) is used hundreds of times. Its job is to get out of the way.

  **Marketing energy in the product (avoid):**

The dashboard's cards stagger in every time you open it. Headlines in a display font. An off-center, playful layout. Delightful on day one; by session fifty it is slow and the asymmetry looks broken.

  **Each in its place (do this):**

The landing page gets the scroll-driven hero, the custom type and the asymmetric grid. The dashboard is calm, dense, consistent and instant, and earns its personality in small moments like a keyboard shortcut.

| | Marketing | Product |
|---|---|---|
| Seen | Once or twice | Hundreds of times |
| Goal | Convert, explain, impress | Get out of the way |
| Motion budget | Generous | Tight |
| Layout | Can break the system | Must follow the system |
| The real work | The story | The states |

*(Onboarding sits in between: theatrical like marketing, disciplined in layout like product.)*

Before reviewing any screen, ask which one it is. The same animation can be right on the landing page and wrong in the app.

## Feeling right

A screen can be correct, accessible, fast and consistent, and still feel cheap. What is left after every box is ticked is whether it **feels right**, and it is the part an agent is weakest at. Benji Taylor put it plainly after building morphing icons with Claude: the agent got them technically correct but could not tell when something looked wrong. That judgement is yours.

Feeling right is not the same as fancy. A still screen with clear hierarchy and good type feels more right than a busy one with five animations. Restraint reads as confidence; over-design reads as anxiety.

You cannot test feel with an automated check, but you can test it:

  **Step: The next-day review.** Build it, sleep, look again. If you still like it tomorrow, ship it. (Emil Kowalski's habit.)

  **Step: The slow-motion replay.** Record the interaction and play it at a quarter speed. Almost every motion bug shows up frame by frame.

  **Step: The cold-eyes test.** Show it to someone who has never seen it. Their first reaction is the truth.

  **Step: The phone-call demo.** Demo it to a friend over a call. If they say "wait, what?", the timing is off.

Do this last, after correctness and accessibility. When something "works but feels off", the fix is rarely one property; it is usually rhythm and timing across several elements.

**Check the budget (paste into your agent):**

```text
For every animation on this screen, tell me how often a user will see it (every keystroke, every action, daily, weekly, rare) and whether this is marketing or product UI. Remove or shrink anything that performs more than its frequency allows, and list what you changed.
```

**Check yourself:** Your team wants a bouncy, 600ms celebration every time someone saves a note. What does the delight-impact curve say?

- [ ] Great idea, saving should feel rewarding
- [x] Saving is daily: a small tick or highlight at most; save the big celebration for rare milestones
- [ ] Make it 1,200ms so people notice
- [ ] Only animate it on marketing pages

*Why:* Saving happens many times a day. Big delight on a frequent action becomes friction within a week; it belongs on rare moments.

## In the skill

- https://school.agents.org.in/skill/philosophy/delight-impact-curve.md
- https://school.agents.org.in/skill/philosophy/marketing-vs-product-ui.md
- https://school.agents.org.in/skill/philosophy/feeling-right.md


---

# States are the work

> Design every state a screen can be in, not just the happy one, and treat tables and charts with the same care as a landing page.

Module: How good interfaces think · Level: beginner · 7 min · https://school.agents.org.in/design/philosophy/states-are-the-work

This inbox has six tabs. The first one is the screen everybody designs. Click through the other five:

*[Interactive demo: One component, six states. The happy path is one of them.]*

Loading, empty, error, offline, half-loaded. Every one of those is a screen a real person will see, often more often than the perfect one. And every one needs its own design and its own words.

## The happy path is the easy part

The happy path (The screen when everything goes right: data loaded, user signed in, content present. The easy state, and the one everyone designs first) is the screen with everything going right: data loaded, user signed in, content present. Designers draw it, engineers build it, agents generate it. Then real people show up on a slow train, with an empty account, with an expired session, and the product looks broken.

**Most "this feels broken" reports trace back to a state nobody designed,** not to a bug in the happy path. So fixing states is some of the highest-value design work there is.

## The states to check, every screen

You will not need all of these for every screen, but you should be able to say why each one does not apply:

| State | What is happening |
|---|---|
| Loading | The data is on its way |
| Empty | Nothing here yet: new account, zero results |
| Partial | Some content loaded, some still coming |
| Error | Something failed, like the server or the network |
| Offline | No connection at all, different from one failed request |
| No permission | Signed in, but not allowed to see this |
| Stale | Showing saved data from a while ago |
| Over limit | A plan limit reached; read-only |
| First run | Onboarding, sample data |

Each state (One of the situations a screen can be in, like loading, empty, error, offline or no permission. Each needs its own design and words) needs real copy, not "Error: something went wrong", and a designed way to get from one to the next without a flash.

  **One message for everything (avoid):**

"No data." Shown when the inbox is empty, and when the server is down. The person cannot tell whether they are caught up or whether their mail is gone.

  **Empty and error, apart (do this):**

Empty: "You're all caught up. New mail will show up here." Error: "We couldn't load your mail. Your messages are safe. Try again." Different look, different words, a way forward.

> **Break it on purpose.** Turn off your wifi, sign out in another tab, use an account with nothing in it. Most apps show an unhandled state within a minute. Better you find it than your users.

## Data is content

There is a second place the craft usually stops: tables, charts and lists. Teams spend a week on a hero image and half an hour on the table people look at every single day. That is backwards. In most working software, data *is* the content.

*[Interactive demo: The same five rows: a library's default, and treated as content.]*

  **Step: Right-align numbers, left-align text.** Numbers compare when their last digits line up. Text reads from the left.

  **Step: Use tabular figures.** `font-variant-numeric: tabular-nums` gives every digit the same width, so a column of amounts lines up and values stop jittering when they change.

  **Step: Quiet the lines.** Heavy borders on every cell and zebra stripes are the spreadsheet look. Use space and a hairline divider, like `1px solid rgba(0,0,0,0.06)`.

  **Step: Label with meaning.** "Value" says nothing. "Revenue, USD" says what and in what unit.

Charts follow the same rules. Pick two to four colors from your palette instead of the library's rainbow. Remove default chartjunk (Decoration in a chart that carries no information: extra gridlines, needless legends, 3D effects, a rainbow of default colors. Edward Tufte's word) (extra gridlines, legends nobody needs). Make tooltips useful: the exact value, the date and a comparison, not "y: 47". Animate the chart drawing in once, on first load; updates should be instant.

*(Charts may have a little personality. Tables and lists should be predictable.)*

For tools people use all day, density matters too. Power users want compact rows (about 28px), casual users want comfortable ones (about 44px); offer both. Let people edit a cell in place and move with the arrow keys, the way every spreadsheet already works.

**Design the states (paste into your agent):**

```text
For this screen, list every state it can be in: loading, empty, partial, error, offline, no permission, stale, over limit, first run. For each, say whether it applies. For the ones that do, build it with real copy (no "something went wrong") and make sure empty and error look and read differently.
```

**Treat the table as content (paste into your agent):**

```text
Review this table as content: right-align numeric columns with tabular-nums, left-align text, replace cell borders and zebra stripes with a 6% hairline between rows, make the header say what each column is and in what unit, and keep the header visible on scroll.
```

**Check yourself:** An inbox shows the same 'No data' message when it is empty and when the server fails. What is wrong?

- [ ] Nothing, it is consistent
- [x] Empty and error are different states; collapsing them leaves people unsure whether anything is missing
- [ ] It should show a spinner instead
- [ ] The message should be in red

*Why:* An empty inbox and a broken one mean very different things. Each needs its own look, words and next step.

## In the skill

- https://school.agents.org.in/skill/philosophy/states-are-the-work.md
- https://school.agents.org.in/skill/philosophy/data-is-content.md


---

# Say it precisely

> Give feedback an agent can act on first time, by pointing at the exact element and naming exactly what is wrong with it.

Module: How good interfaces think · Level: beginner · 6 min · https://school.agents.org.in/design/philosophy/say-it-precisely

Same problem, two ways to report it. In **Describe**, the agent gets a sentence. In **Point**, tap the part of the card you mean:

*[Interactive demo: A sentence sends the agent searching. A pointer sends it to the line.]*

"The button on the pricing thing looks kind of off" opened four files and ended in a guess. Tapping the button handed over its exact address, its file and line, and its current style. The agent had nothing left to search for.

## Pointing beats describing

Benji Taylor's rule: **when feedback is going to an agent, precise selection beats prose.** Agents are good at making changes and comparatively bad at finding where to make them. Every step spent searching is a step not spent on the fix, and a chance to fix the wrong thing.

| Prose | Pointing |
|---|---|
| "the blue button in the sidebar" | `.sidebar > button.primary` |
| "the spacing on the card feels off" | `.product-card`, `ProductCard.tsx:42`, `padding: 16px 12px` |
| "make this section more interesting" | `section#hero`, plus a link to a hero you like |

That address in the right column is a selector (The exact address of an element on a web page, like .plan-card > button.primary. Handing one to an agent tells it precisely which thing you mean): the exact path to one element on the page, the same thing CSS uses to style it.

## How to point

You do not have to write selectors by hand.

  **Step: Click to annotate.** Benji Taylor's Agentation (Benji Taylor's development toolbar: click any element in your running app, write a short note, and it hands an agent the element's selector, file and your words) adds a small toolbar to your app while you develop. Click an element, type your note, and it hands the agent the selector, file and styles.

  **Step: Copy from the browser.** Right-click the element, choose Inspect, then right-click it in the panel and pick Copy → Copy selector. Free and built in.

  **Step: Screenshot with a mark.** For anything that is not a web page, a screenshot with a circle drawn on it is pointing too.

  **Step: Quote the lines.** In code review, paste the exact lines, not "the function near the top".

> **Fragile addresses break.** `.card > div:nth-child(3) > span` is a selector, but it breaks the moment someone adds a div. Prefer names that describe the thing: `.product-card__name` or `[data-testid="price"]`.

## Point, then say why

A bare pointer is worse than prose. "`.sidebar > button.primary`, fix it" tells the agent *where* but not *what*, so it guesses. Pointing finds the element; words explain the problem.

  **Pointer, no reason (avoid):**

`.plan-card > button.primary` — fix this.

  **Pointer plus reason (do this):**

`.plan-card > button.primary` (`PlanCard.tsx:42`): padding is `6px 10px`, cramped for the main action. Make it `10px 16px` with a 44px minimum height so it is easy to tap.

## Articulate precisely

The words half of feedback has its own rule. Two people look at the same screen. One says "it feels off". The other says "the line height is too tight and the small uppercase labels need more letter spacing". Only the second can fix it, hand it to someone or teach it. The difference is not taste. It is vocabulary (The exact names for how interfaces look and move, like tracking, easing or hierarchy. The right word gets you the right result from an agent).

"Say precisely what you mean" is the idea behind Index, the design vocabulary project by Emil Kowalski and Glenn Carstens-Peters. Naming something exactly is what lets you discuss it, critique it and instruct it. "Make it pop" is a wish; "raise the contrast to 4.5:1" is a spec.

Precision runs both ways:

- **Your input.** The exact word ("widow", "optical center", "layout shift") tells the agent exactly what to change.
- **Its output.** A good review names the thing: "the play icon looks off-center because it is centered by its box, not by its visual weight". "The icon looks weird" is not reviewable.

*(Every "don't" needs a "because". A rule without a reason does not survive the next prompt.)*

Watch the pairs that vague language blurs together: kerning (one pair of letters) versus tracking (a whole line), a tag versus a badge, a widow versus an orphan. Mixing them up quietly tells the agent you cannot see the difference.

And precise does not mean jargon. "The button has no hover state" is precise and plain. Use the shortest words that leave nothing to guess.

**The two-channel note (paste into your agent):**

```text
In src/components/PlanCard.tsx, the element .plan-card__price (line 21): the digits shift sideways when the price toggles between monthly and annual, because they are proportional figures. Add font-variant-numeric: tabular-nums. Change nothing else, and tell me the line you edited.
```

**Ask the agent to be precise back (paste into your agent):**

```text
When you review this screen, name each problem exactly (the element's selector and file, the property, the value) and give one sentence of why. No "looks off" or "feels weird".
```

**Check yourself:** Which piece of feedback will an agent act on correctly first time?

- [ ] The pricing card looks a bit off
- [ ] .plan-card > button.primary — fix it
- [x] .plan-card > button.primary (PlanCard.tsx:42): padding 6px 10px is cramped for the main action; use 10px 16px
- [ ] Make the button pop more

*Why:* It points at the exact element and says what is wrong and what to change. A bare selector says where but not what; prose says neither.

## In the skill

- https://school.agents.org.in/skill/philosophy/pointing-beats-describing.md
- https://school.agents.org.in/skill/philosophy/articulate-precisely.md


---

# The bundle is design

> Treat every dependency as a cost the user pays, and know when to use the platform, a few lines of your own, or a library.

Module: How good interfaces think · Level: intermediate · 6 min · https://school.agents.org.in/design/philosophy/the-bundle-is-design

Here are five libraries a typical web app ships, and what each costs a phone before the page can respond. Tap a row to swap the library for what the browser already has built in:

*[Interactive demo: Every library is downloaded by every visitor, on every network. The platform is already there.]*

Swap them all and roughly 145 KB disappears: around three seconds on a slow mobile connection, gone, with nothing lost.

## Every dependency is a tax

A dependency (Code someone else wrote that your project pulls in, usually with npm install. Convenient once, then a cost every visitor pays) is code someone else wrote that your project pulls in, usually with `npm install`. Each one is convenient on the day you add it, and then it is a cost forever: more for every visitor to download, more code that could have a security hole, more updates to keep up with.

All of that ends up in the bundle (The JavaScript files a browser has to download before your page works. Every dependency makes it bigger), the JavaScript file the browser downloads before your page works. **A 5 MB bundle is a design decision, and so is a 50 KB one.** The person on a cheap phone on a train feels the difference before they see a single pixel you designed.

So the bar for adding a dependency should be higher than "it's convenient".

## Use what the browser already has

The web platform has grown a lot. Many libraries that "everyone uses" now have a built-in replacement, a native API (A feature already built into the browser, like fetch, Intl or crypto.randomUUID, so it costs nothing to download), that costs nothing to download.

| Instead of | Use |
|---|---|
| `moment` for dates | `Intl.DateTimeFormat`, `Intl.RelativeTimeFormat` |
| `axios` for requests | `fetch()` |
| most of `lodash` | `Array` methods, `Set`, `Object.fromEntries` |
| `uuid` | `crypto.randomUUID()` |
| `query-string` | `URLSearchParams` |
| jQuery | `querySelector`, `closest`, `matches` |

## Write the five lines

If you only use two functions from a big library, write the two functions.

  **A library for one line (avoid):**

```js
    import uniq from "lodash/uniq";
    const tags = uniq(allTags);
    ```

  **The one line (do this):**

```js
    const tags = [...new Set(allTags)];
    ```

## Draw simple shapes in CSS

The same goes for images. A dot, a ring or a chevron does not need a file to download.

```css
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.chevron {
  width: 8px; height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}
```

No request, no image to decode, no alt text to maintain. The shape lives in the stylesheet.

## When a library is the right call

This is not "never install anything". Some things are genuinely worth it:

  **Step: Hard to get right.** Cryptography, a framework like React, tricky date parsing. Writing your own is how bugs get in.

  **Step: Security-maintained.** Payment and sign-in clients, where a dedicated team watches for problems.

  **Step: Small and focused.** A tiny package with one clear job, like `clsx` for joining class names.

  **Step: Speed matters more right now.** A prototype or an internal tool can grab whatever is fastest. Calibrate to how long the code will live and who will load it.

*("It's convenient" is not on that list.)*

Agents reach for packages by reflex, because most code they have read imports them. So ask the question for them:

**Before you install (paste into your agent):**

```text
Before adding any new dependency, tell me its gzipped size and whether the browser or a few lines of our own code can do the same job. Only install it if it is hard to get right, security-maintained or tiny and focused, and say which.
```

**Audit the bundle (paste into your agent):**

```text
List our dependencies with their approximate gzipped sizes. For each one, say whether a native browser API or a short utility could replace it (Intl for moment, fetch for axios, crypto.randomUUID for uuid, Array and Set for lodash). Don't change anything yet; give me a table.
```

**Check yourself:** Your agent wants to install moment to format one date on a page. What is the better first move?

- [ ] Install moment; it is the standard
- [x] Use Intl.DateTimeFormat, which is built into the browser
- [ ] Install moment and lodash together
- [ ] Format the date on the server with jQuery

*Why:* Intl.DateTimeFormat formats dates natively at zero download cost. moment adds tens of kilobytes every visitor pays for.

## In the skill

- https://school.agents.org.in/skill/philosophy/dependency-discipline.md


---

# Should it move at all?

> Decide whether something deserves an animation before you build one, and take motion out safely when it doesn't.

Module: Motion · Level: beginner · 7 min · https://school.agents.org.in/design/motion/should-it-move

Press the button below a few times, fast. Both command palettes open the same menu. One of them is pretty the first time and in your way every time after.

*[Interactive demo: The same palette, opened from the keyboard. Count how often you'd do this in a day.]*

That is the whole first lesson of motion: **the default answer to "should this animate?" is no.** Motion has to earn its place. Most of the craft in this module is about the cases where it does, but the most useful habit is asking the question at all.

## Ask how often

Emil Kowalski, who built the toast library Sonner and the drawer library Vaul, starts every animation decision with one question: how often will someone see this?

*[Interactive demo: Pick a moment. The more often it happens, the less motion it can afford.]*

| How often | What it gets |
|---|---|
| 100+ times a day (shortcuts, tab switches, a command palette) | No animation |
| Tens of times a day (hover, moving through a list) | Almost nothing |
| Now and then (a modal, a drawer, a toast) | A standard animation |
| Rare or once (onboarding, a first success) | This is where delight lives |

Two hard rules sit on top of the table. **Anything triggered from the keyboard doesn't animate.** Someone who presses a shortcut wants the result, not a show. And **data people are reading** (a chart, a table, a number) doesn't move for decoration; it moves only when the value really changes.

## Name the job

If it passes the frequency test, the motion still needs a job. Name one, or don't build it:

- **Feedback**: the button shows it heard your press.
- **Where it came from**: a menu grows out of the button you clicked.
- **What changed**: a toggle slides so you see it switch.
- **Smoothing a jump**: content that would otherwise teleport glides instead.
- **Explaining**: a demo shows how a feature works (marketing and onboarding only).
- **Delight**: only in the rare tier.

Three jobs that don't count: decoration, hiding slow code ("fix the code"), and "the animation library is installed, so let's use it".

> **In plain words.** An animation is a change you can watch happen. If nobody would miss it, it is a cost: a few hundred milliseconds of waiting, every time.

## Then pick the cheapest tool

Once something earns motion, reach for the simplest tool that does the job. A CSS transition (A CSS rule that animates a property from its current value to a new one when it changes. Because it starts from wherever it is, it can reverse smoothly if interrupted) for a state you toggle. CSS `@starting-style` for something appearing for the first time. A CSS animation for motion planned in advance. A JavaScript library like Motion only for springs, gestures and exits that need it. The later lessons cover each decision in order: easing (How speed changes during an animation: start fast and slow down, start slow and speed up, or both. It decides whether motion feels snappy or sluggish), then duration, then entering and leaving, then springs.

  **Step: How often?.** Use the table. Keyboard-triggered means no.

  **Step: What job?.** Feedback, origin, state, smoothing a jump, explaining, or rare delight.

  **Step: Which tool?.** The cheapest one that works.

  **Step: Which curve, how long, which property?.** The next three lessons.

Four quick checks before you keep any animation: would the UI feel broken without it? Does it work with reduced motion on? Does it still look right played at quarter speed? Will it still feel right the fiftieth time?

## Taking motion out without breaking things

The opposite job comes up just as often: "the app feels slow, tone down the motion." Deleting an animation looks like deleting one line. It rarely is.

*(The animation is usually holding something up you can't see in the diff.)*

- **Hidden start states.** If an element starts at `opacity: 0` and an animation fades it in, deleting the animation leaves it invisible forever. Delete both together.
- **Code that waits.** A `setTimeout` sized to the animation, or a handler waiting for it to end, keeps pausing the UI for motion that no longer exists.
- **Exits that delay unmounting.** Libraries hold a closing component on screen until its exit finishes; with no exit, whatever ran "after" must run immediately.
- **Layout animations hiding a jump.** If removing one makes content lurch, keep it and fix the lurch separately.
- **Focus and tests** that were timed to the animation.

Before deleting, try reducing:

| Today | Reduced to |
|---|---|
| Hover with scale, lift or a growing shadow | Color or opacity only, 150ms or less |
| A slide or scale entrance on something frequent | Opacity only, 150ms |
| Any UI animation over 300ms | 200–250ms on the same curve |
| A stagger over 80ms per item or 300ms total | 30ms per item, or fade the group at once |
| An endless pulse or shimmer | Play once, keep the end state |

Then clean up what the removal orphaned: unused keyframes, empty wrappers, imports. And keep a short list of what you deliberately *kept*, with the job that saved each one. That list is what makes it a restraint pass rather than deleting every transition in the repo.

**A restraint pass (paste into your agent):**

```text
Go through the animations in this app. For each one, tell me how often a user sees it and what job it does (feedback, origin, state change, smoothing a jump). Remove or reduce any that are keyboard-triggered, decorative, or seen 100+ times a day. When you remove one, also remove its start state (like opacity: 0), any timeouts or animation-end handlers that waited on it, and any orphaned keyframes. List what you kept and why.
```

## Going deeper

This module teaches the decisions and the exact values. If you want to learn motion in depth, Emil Kowalski's course at animations.dev is the canonical resource; his libraries Sonner and Vaul, Jakub Antalik's transitions.dev catalog and the animated icon set at lucide-animated.com are the best places to read real, production motion code.

**Check yourself:** A user opens your command palette with ⌘K about 150 times a day. What should its open animation be?

- [ ] A 300ms scale and fade, so it feels polished
- [ ] A 150ms fade, as a compromise
- [x] No animation at all
- [ ] A spring with a little bounce

*Why:* It's keyboard-triggered and seen 100+ times a day: both rules say no animation. Any motion becomes a wait between the shortcut and the result.

## In the skill

- https://school.agents.org.in/skill/motion/MOC-motion.md
- https://school.agents.org.in/skill/motion/animation-decision-framework.md
- https://school.agents.org.in/skill/motion/animation-removal-hazards.md
- https://school.agents.org.in/skill/motion/animations-dev-curriculum.md


---

# Easing curves

> Pick the right easing curve for anything that moves, and know why ease-out makes the same animation feel faster.

Module: Motion · Level: beginner · 7 min · https://school.agents.org.in/design/motion/easing

Open both dropdowns at once. They take exactly the same 250 milliseconds. One of them feels sluggish.

*[Interactive demo: Same menu, same duration. Only the curve differs. Try slow-mo to see why.]*

The difference is the easing (How speed changes during an animation: start fast and slow down, start slow and speed up, or both. It decides whether motion feels snappy or sluggish): how the speed changes during the animation. The left one uses ease-in (An easing that starts slowly and speeds up toward the end. It makes UI feel sluggish because nothing happens right when you're watching, so avoid it in interfaces), which starts slowly and speeds up. The right one uses ease-out (An easing that moves fast at the start and slows into the end. The right default for anything entering or leaving, because it reacts the moment you act), which moves most of the way at once and spends the rest of its time settling.

When you click, your eye goes straight to where the menu should appear. Ease-out puts something there in the very first frames. Ease-in makes you watch almost nothing happen, right at the moment you're paying the most attention. **Never use ease-in on UI, exits included.**

## The decision order

You don't need to memorise curves. You need one rule per situation:

| What's happening | Curve |
|---|---|
| Something entering or leaving (menu, toast, modal) | ease-out |
| Something already on screen moving from A to B (a tab indicator) | ease-in-out |
| A hover or a color change | `ease` |
| Constant motion: a progress bar, a marquee, a hold-to-confirm fill | `linear` |
| Not sure | ease-out |

A drag, a flick or anything your finger is steering doesn't use a curve at all. It uses a spring, which gets its own lesson.

## Custom curves beat the built-ins

CSS ships `ease`, `ease-in`, `ease-out` and `ease-in-out`. They were fine defaults a decade ago; today they read as "nobody chose this". A cubic-bezier (A way to write your own easing curve as four numbers, like cubic-bezier(0.23, 1, 0.32, 1). Custom curves feel more deliberate than the built-in ones) lets you write your own curve as four numbers, and a slightly stronger curve is most of what makes motion feel deliberate.

*[Interactive demo: Pick a curve to see its shape and watch the ball run it. The red one is the one to avoid.]*

Here is a small set worth keeping as tokens, mostly from Emil Kowalski's practice:

```css
:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);        /* menus, popovers, toasts, modals */
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);    /* on-screen travel, tab indicators */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);     /* sheets and drawers from an edge */
  --ease-out-soft: cubic-bezier(0.25, 1, 0.5, 1);    /* gentler hovers and menus */
  --ease-back: cubic-bezier(0.34, 1.56, 0.64, 1);    /* overshoots: playful moments only */
}
```

Read the curve like a graph: time runs left to right, progress bottom to top. A steep start means it moves early. A number above 1 (like the `1.56` in back-out) means it shoots past the end and settles back, which is cute on a sticker and wrong on a delete dialog.

> **In plain words.** ease-out = fast then gentle. ease-in = gentle then fast. For anything a person just asked for, fast first.

## Curve and duration go together

A beautiful curve on the wrong duration looks worse than a plain ease-out on the right one. Tune them as a pair, which is the next lesson. Need a curve that isn't on the list? Take one from easing.dev or easings.co rather than hand-rolling four numbers.

  **Before (avoid):**

```css
.menu {
  transition: all 300ms ease-in;
}
```

  **After (do this):**

```css
.menu {
  transition:
    opacity 200ms var(--ease-out),
    transform 200ms var(--ease-out);
}
```

Three fixes in one line: ease-out instead of ease-in, a named list of properties instead of `all`, and a shorter duration.

## A note on writing it down

Small conventions make motion CSS easier to read and review. They come from Ben DC's frontend guidelines:

- **Pick one time unit and stick to it.** This course uses milliseconds (`200ms`) everywhere, because they match JavaScript timers and design tokens. Mixing `200ms` and `0.2s` in one file is the actual bug.
- **Rotate in turns.** `rotate(0.25turn)` is a quarter turn, `rotate(1turn)` a full spin. Keep degrees for arbitrary angles like `14deg`.
- **Unitless line-heights**, `rem` for things that should scale with the reader's font size and `px` for hairlines.
- **Avoid `!important`**; it usually means a specificity fight the cascade should settle.

When you join an existing codebase, its conventions win. These are for when you're starting fresh.

**Fix the easing (paste into your agent):**

```text
Audit the CSS transitions and animations in this project. Replace any ease-in on UI with ease-out. Use these tokens: --ease-out: cubic-bezier(0.23, 1, 0.32, 1) for things entering or leaving, --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1) for things moving across the screen, --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1) for sheets. Use linear only for progress. Name the properties in every transition instead of "all", and keep durations in milliseconds.
```

**Check yourself:** A dropdown uses transition: opacity 250ms ease-in. A user says it feels slow. What's the best first fix?

- [ ] Make it 400ms so it's smoother
- [x] Switch to ease-out (or a strong custom ease-out) at the same or shorter duration
- [ ] Add a spring with bounce
- [ ] Use linear so the speed is even

*Why:* ease-in starts slowly exactly when the eye is watching, so the same 250ms feels slower. A strong ease-out moves at once and feels faster without changing the duration.

## In the skill

- https://school.agents.org.in/skill/motion/easing-curves.md
- https://school.agents.org.in/skill/motion/css-conventions.md


---

# Duration and feedback

> Choose how long an animation should last for each kind of element, and add the small press and tooltip responses that make UI feel answered.

Module: Motion · Level: beginner · 7 min · https://school.agents.org.in/design/motion/duration-and-feedback

Drag the slider to 600ms and open the menu. Then try 200ms. Same curve, same menu: one is a small show, the other just answers you.

*[Interactive demo: One dropdown, one curve. Slide the duration and open it again.]*

The duration (How long an animation lasts. Almost all interface motion belongs under 300 milliseconds; button presses take 100 to 160) is how long the animation lasts. There is no single right number; it depends on what is moving and how often people see it. But there are good ranges, and **almost all UI motion belongs under 300 milliseconds.**

## The table

| Element | Duration |
|---|---|
| Button press | 100–160ms |
| Hover change | 120–200ms |
| Tooltip | 125–200ms, after a first delay |
| Dropdown or popover | 150–250ms |
| Modal | 200–300ms |
| Tab content swap | 200–250ms |
| Page transition | 250–400ms, the longest normal UI motion |
| Skeleton to content | 150ms |
| Celebration, confetti | 600–1200ms (rare, so it can afford it) |
| First-run onboarding | 400–800ms |

Three rules of thumb cover anything not on the list. **Smaller is shorter**: a 12px icon doesn't need 300ms. **More frequent is shorter.** **More important is a little longer**: a modal changes what you're looking at, so it earns more time than a tooltip.

> **If unsure, halve it.** Most animations that "feel slow" are simply too long. Most that "feel jumpy" are too short. When in doubt, halve the number and look again.

## Exits are shorter than entrances

When something closes, the person has already moved on. Make the exit about 60 to 80 percent of the entrance: a panel that opens in 250ms closes in about 180ms.

```css
.panel { transition: transform 180ms var(--ease-out); }              /* closing */
.panel[data-state="open"] { transition-duration: 250ms; }           /* opening */
```

Use transitions here, not keyframes. A transition starts from wherever the element is right now, so if someone closes the panel halfway through opening, it simply turns around. A keyframe animation would jump back to its first frame.

There's a second kind of asymmetry for moments where the person is deciding. A hold-to-delete button fills slowly while you hold (2 seconds, linear) and snaps back fast if you let go (200ms, ease-out). The slow part is their decision; the fast part is the system answering. You'll build that in the choreography lesson.

*(Sonner's toasts run a calm ~400ms on purpose. That's a personality choice, not the default.)*

## Press feedback

The smallest animation with the biggest effect: a button that shrinks a little under your finger. Press and hold each of these.

*[Interactive demo: Press and hold each one, then let go halfway through.]*

```css
.button { transition: transform 120ms var(--ease-out); }
.button:active { transform: scale(0.97); }
```

The canonical numbers: **0.97** for buttons, **0.96** for small controls like icon buttons and chips, and never below **0.95**, where it starts looking squishy. Keep it 100–160ms. Because it's a transition, a release mid-press glides back. Two exceptions: text inputs don't scale (shift the border or background instead), and controls used hundreds of times a day can skip it.

## Tooltips that don't make you wait twice

Tooltips need a delay before the first one appears, roughly 400–700ms, so a pointer passing over a toolbar doesn't spray labels everywhere. But once one is open, the neighbours should appear **instantly**, with no delay and no animation. Move across this toolbar with each setting.

*[Interactive demo: Hover across the buttons, or tab through them, in both modes.]*

This "skip delay" is one of the most reused details in good interfaces; without it, exploring a toolbar feels gluey. Radix UI's tooltip provider has it built in (`skipDelayDuration`), and sets a `data-instant` attribute you can use to drop the animation. Give tooltips and hover popovers a small close delay too, about 150ms, so a slight wobble of the mouse doesn't make them flicker.

One more responsiveness rule lives with popovers: a menu should grow out of the button that opened it. That's the next lesson.

**Durations and feedback (paste into your agent):**

```text
Check every transition in this UI against these budgets: button press 100–160ms, hover 120–200ms, dropdowns and popovers 150–250ms, modals 200–300ms, page transitions up to 400ms. Make exits about 70% of the entrance duration, using transitions (not keyframes) so they can reverse mid-way. Add :active { transform: scale(0.97) } with a 120ms ease-out to buttons (0.96 for icon buttons), but not to text inputs. Give tooltips a ~500ms first delay and make neighbouring tooltips appear instantly while one is open.
```

**Check yourself:** A panel slides in over 250ms. How long should its exit take?

- [ ] 250ms, exits should mirror entrances
- [x] About 180ms, because the person has already moved on
- [ ] 400ms, so the exit feels gentle
- [ ] 0ms, exits don't need animation

*Why:* Exits are usually 60–80% of the entrance. The person has made their decision; a shorter, quieter exit gets out of the way.

## In the skill

- https://school.agents.org.in/skill/motion/duration-table.md
- https://school.agents.org.in/skill/motion/responsive-feedback.md


---

# Entering and leaving

> Make things appear and disappear well, from the right size, out of the right place, with a quieter exit than entrance.

Module: Motion · Level: beginner · 9 min · https://school.agents.org.in/design/motion/enter-and-exit

Open both dialogs. The left one inflates from a dot like a balloon. The right one simply arrives.

*[Interactive demo: Both are 'a scale animation'. Only one looks like software.]*

The left one starts at `scale(0)`. For its first few frames it's invisible, then a blurry speck, then a toy growing out of a single point. The right one starts at `scale(0.95)` with `opacity: 0`. **Nothing in a UI should scale up from zero.** The 5% size change is enough to read as motion; the opacity (How see-through something is, from 0 (invisible) to 1 (solid). Fading opacity is cheap to animate and does most of the work of making things appear) does the work of making it visible.

```css
.dialog {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.dialog[data-state="open"] {
  opacity: 1;
  transform: scale(1);
}
```

Both properties share one duration and one curve, so they read as one move.

> **In plain words.** **transform** moves, scales or rotates an element without disturbing anything around it. **opacity** is how see-through it is, from 0 (invisible) to 1 (solid). Together they do almost all good UI motion.

## Appearing the first time

To animate something the moment it's added to the page, modern CSS has `@starting-style`: you describe where it starts, and the browser animates from there.

```css
.toast {
  opacity: 1;
  transform: none;
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}
@starting-style {
  .toast { opacity: 0; transform: translateY(8px); }
}
```

Where that isn't supported, the common React pattern is a `data-mounted` attribute set one frame after mount, with the start state in the base rule and the end state under `[data-mounted="true"]`. Either way, list the properties; never `transition: all`.

## Grow from where it came from

A menu that opens from a button should look like it unfolds out of that button. The pivot point for a scale is the transform-origin (The pivot point for scaling or rotating an element. Setting it to where a menu's button is makes the menu look like it grows out of that button), which is the center by default. Press the ••• button in each mode.

*[Interactive demo: Center origin grows from nowhere. Origin on the button grows from your press.]*

```css
.popover { transform-origin: var(--popover-origin, top right); }
```

Radix, React Aria and Floating UI all tell you where the trigger is, so set the origin from that. The exception is a modal: it isn't attached to anything, so it stays centered.

## Three transform tricks worth knowing

These come from Emil Kowalski's notes on transform (The CSS property that moves, scales and rotates an element without disturbing anything around it. It's one of the two properties that are cheap to animate):

- **`translateY(100%)` moves an element by its own height.** A sheet hidden with it works whether it's 200px or 800px tall; no magic numbers.
- **`scale()` scales the children too**, text included. If a card grows on hover, its text grows with it. Scale an icon or image inside instead.
- **Order matters, and `transform` is one property.** `translate(10px) rotate(45deg)` is not `rotate(45deg) translate(10px)`. And writing `transform: scale(1.05)` on hover silently throws away a `translateY` set elsewhere; write both.

## Leave quieter than you arrived

Show and hide this check. The first exit plays the entrance backwards. The second keeps three quiet properties and lets go.

*[Interactive demo: Enter with five properties; exit with three, in half the time.]*

This is a signature pattern from Jakub Antalik's transitions.dev: **compose on enter, subtract on exit**. The entrance is the news, so it can use several properties (opacity, scale, a little rotation, a bob, a stroke drawing on). The exit is "going away", so it keeps the essentials (opacity, a hint of scale, a 2px blur) and drops the theatre. A rewound entrance is technically correct and still feels mechanical. Pair it with the shorter exit timing from the last lesson.

Don't skip the exit entirely, though. Something that pops onto the screen with care and then vanishes in one frame looks broken.

## Toasts: a small library's worth of lessons

A toast (A small message that slides in at the edge of the screen to confirm something ("Changes saved") and leaves on its own. One with an Undo button has to stay longer and pause while you look at it) is the little message that slides up to confirm something ("Invite resent"). Emil Kowalski's library Sonner is the most copied toast on the web, and its principles apply to any component you hand to other people.

*[Interactive demo: Each toast rises from the edge it will leave by; the older one steps back instead of vanishing.]*

- **The API is the product.** `toast("Saved")` beats `toast({ message, type, duration })`. Default everything.
- **Good defaults beat options.** "What happens if I pass nothing?" should always look right.
- **Handle edge cases quietly**: stack many toasts, dedupe identical ones, survive rapid clicks.
- **Transitions, not keyframes**, so spam-dismissing reverses smoothly instead of restarting.
- **Match motion to personality.** Sonner is calm, so its toasts use a slower ~400ms `ease`. Vaul is physical; cmdk is snappy. Pick one and stay consistent.
- **Leave by the way you came**: in from the bottom, out through the bottom, with fewer properties.
- **Look again tomorrow.** Build it today, review it after sleep. The cheapest QA there is.

## Errors: a shake that means it

One more entrance: the error. When a form field fails on submit, a short shake points at it. Try both styles with an empty field.

*[Interactive demo: Submit with the field empty. Then type an email to clear it.]*

The version that reads as physical, from Jakub Antalik: 280ms, five stops, shrinking from 4px to 2px, on a snappy curve.

```css
@keyframes shake {
  0%     { transform: translateX(0); }
  28.57% { transform: translateX(-4px); }
  57.14% { transform: translateX(4px); }
  78.57% { transform: translateX(-2px); }
  100%   { transform: translateX(0); }
}
.field--error { animation: shake 280ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }
```

Shake the field's **wrapper**, never the input itself, or the text cursor jumps. Keep the red border after the shake (the shake gets attention; the state explains). Fire it on submit or blur, never on each keystroke.

**Better entrances and exits (paste into your agent):**

```text
Review how elements enter and leave in this UI. Replace any scale(0) with scale(0.95) plus opacity 0 → 1 on the same duration and ease-out curve. Set transform-origin on popovers and menus to the trigger's position (modals stay centered). Make exits shorter (about 70% of the entrance) and quieter: drop translate, rotate and bounce on exit, keep opacity and a small scale. For form errors, shake the field wrapper 280ms with keyframes at 0, 28.57, 57.14, 78.57, 100% (0, -4px, 4px, -2px, 0) and keep the error border afterwards.
```

**Check yourself:** A tooltip enters with scale(0.94), opacity and a small translate. What should its exit do?

- [ ] Play the same three properties in reverse at the same speed
- [x] Fade out with opacity (maybe a hint of scale), faster than it entered
- [ ] Scale down to 0 so it disappears into its anchor
- [ ] Nothing; remove it instantly with display: none

*Why:* Compose on enter, subtract on exit: the leaving element keeps the essentials and drops the theatre, at about 60–80% of the entrance time. Instant removal looks broken; scale(0) is the balloon.

## In the skill

- https://school.agents.org.in/skill/motion/never-scale-from-zero.md
- https://school.agents.org.in/skill/motion/transform-mastery.md
- https://school.agents.org.in/skill/motion/compose-subtract-asymmetry.md
- https://school.agents.org.in/skill/motion/sonner-principles.md
- https://school.agents.org.in/skill/motion/multi-segment-shake.md


---

# Springs

> Know when a spring beats a curve, tune stiffness and damping by feel, and keep hand-written motion landing in the same place at any frame rate.

Module: Motion · Level: intermediate · 8 min · https://school.agents.org.in/design/motion/springs

Send the ball right, then press again before it arrives. It turns around without stopping dead, carrying its speed into the new direction. That is a spring.

*[Interactive demo: Tune the spring, then interrupt it mid-flight.]*

A curve says "get there in 200ms, with this shape". A spring (Motion driven by physics instead of a fixed duration: a pull toward the target plus friction. It keeps its speed when interrupted, which makes drags and flicks feel natural) says "pull toward the target with this much force and this much friction", and the duration falls out of the physics. That difference only matters in one situation, but it matters a lot there: **when the person is steering the motion, use a spring.**

## When springs win, and when they don't

Use a spring when:

- The motion can be **interrupted** halfway: a drag, a swipe, a sheet you grab again.
- It starts from a real **speed**, like the flick of a finger.
- The target can **change while it's moving**.

Use a curve when a discrete event triggers it (a click, a route change), when you need exact timing so several things land together, or when the element is small enough that nobody could tell. A two-state press doesn't need a spring either: a CSS transition already reverses from wherever it is.

> **In plain words.** **Stiffness** is how hard the spring pulls toward its target: higher is snappier. **Damping** is friction: lower lets it overshoot and wobble, higher settles it calmly. **Mass** is how heavy the thing feels; leave it at 1.

## The two springs to remember

**Apple's default is critically damped**: no bounce, a perceived duration around 0.3–0.4 seconds. In Motion you can write it in the form that's easiest to reason about:

```ts
const settle = { type: "spring", duration: 0.35, bounce: 0 };
```

**The drawer spring** settles a sheet or drawer after you let go of it:

```ts
const drawer = { type: "spring", stiffness: 380, damping: 30, mass: 1 };
```

Bounce is earned. A card you flicked may overshoot a touch (bounce 0.1 to 0.3, never more); a menu that simply opened should not. And never leave a spring on library defaults: they tend to be slow and floaty (try "Library default" above). Always set `duration` and `bounce`, or both `stiffness` and `damping`.

Not everything polished is a spring, either. Sonner, the usual example of polished motion, uses plain CSS transitions.

## Writing your own: frame-rate independence

Libraries handle the physics for you. If you write a spring or a decay by hand, there's a trap. Flick both balls below in each mode.

*[Interactive demo: The same flick on a 60fps and a 30fps screen.]*

Code written "per frame" (`velocity *= 0.95` every frame) runs twice as many frames on a 60Hz screen as on a 30Hz one, and the same gesture ends somewhere different. Cheng Lou measured the same input landing up to 359px apart between 10 and 120fps. The rules from his work, in plain words:

  **Step: Step on a fixed clock.** Advance springs in fixed 4ms steps (four per frame at 60fps), or use a formula that gives the exact value at any time, like `x0 · e^(−kt)`.

  **Step: Cap the catch-up.** After the tab freezes, don't run thousands of steps at once. Cap at about 300 (1.2s), then either carry the rest over or snap to the destination, on purpose.

  **Step: Apply input at its timestamp.** Step up to the moment of the press, then apply it, so recorded input replays identically.

  **Step: Store what the person placed.** Keep the panel size they chose; compute the squeezed-to-fit version each frame without saving it.

  **Step: Never decide from last frame's value.** A lagging position is fine to draw; a lagging width that picks a column count makes things jump.

Ending a spring needs care too: it never reaches the target exactly. Stop it when both the distance and the speed are tiny (under about 0.01), then set it exactly on target. The playground above does all of this, which is why interrupting it feels clean.

*(The ball above is stepped every 4ms no matter how fast your screen is.)*

## Live data: breathe as one

A cousin of the spring comes from Benji Taylor's live chart, Liveline. When several things show the same changing number (the line, the price badge, the axis labels), move them all toward the new value at **the same rate**, so the chart inhales as one instead of jittering in pieces. He uses a lerp (Short for linear interpolation: moving a value a fixed fraction of the remaining way toward its target each frame, like 8%. Used to make live data glide instead of jump) of 8% per frame:

```ts
current += (target - current) * 0.08; // for every element that shows this value, in one loop
```

If a new value lands outside the visible range, snap the range out instantly, then let the line glide into it. Run everything in one `requestAnimationFrame` loop that writes to the page directly rather than through React state. And give live data its states: a loading line that breathes and then becomes the chart, a quiet empty message, a paused view that catches up on resume. Test it on ugly data: wild swings, fast reversals, long silences then bursts.

**Springs that feel right (paste into your agent):**

```text
Use springs only where the user drives the motion (drag, swipe, sheets, anything interruptible); keep curves for click-triggered animations. Default to a critically damped spring ({ type: "spring", duration: 0.35, bounce: 0 }); for drawers and sheets after a drag use { stiffness: 380, damping: 30 } and pass the release velocity. Only allow bounce (0.1–0.3) after a flick. Never leave spring parameters on defaults. If you hand-write any physics, step it on a fixed 4ms clock with a capped catch-up, not once per frame.
```

**Check yourself:** Which of these should use a spring rather than a cubic-bezier curve?

- [ ] A modal that opens when a button is clicked
- [x] A bottom sheet the user drags and releases
- [ ] A tooltip fading in
- [ ] A page transition after a link click

*Why:* The sheet is driven by the user's finger, can be grabbed again mid-flight and starts with real velocity. That's exactly where springs beat fixed-duration curves.

## In the skill

- https://school.agents.org.in/skill/motion/spring-animations.md
- https://school.agents.org.in/skill/motion/frame-rate-independence.md
- https://school.agents.org.in/skill/motion/lerp-breathing.md


---

# Choreography

> Make several things move together with intent, using stagger, travelling elements, clip-path reveals and grouped hover.

Module: Motion · Level: intermediate · 9 min · https://school.agents.org.in/design/motion/choreography

Slide the delay to 150ms and press replay. The last row arrives so late it feels like the search is still loading. Now try 40ms.

*[Interactive demo: Search results arriving in a cascade. Watch the total, not the per-row number.]*

A stagger (Playing the same entrance on several items one after another, a few tens of milliseconds apart, so a list arrives as a quick wave) is the same entrance played on several items, each a little after the last. Done well it shows order and pulls the eye down the list. Done badly it makes a fast app feel slow.

## Stagger: small gaps, short cascade

- **30–80ms between items.** Under 30 looks like everything at once with extra steps. Over 80 starts to feel like loading.
- **Keep the whole cascade under about 300ms.** Past around a dozen items, cap it: items past the cap arrive with the last staggered one.
- **Only on first appearance.** The first search result list can cascade; refining the search, sorting or filtering should not re-stagger. Update in place.
- **Only when order means something.** A stagger implies a reading order. Identical items in arbitrary order shouldn't pretend to have one.
- **Exit together.** If you must stagger an exit, reverse it; usually a faster, single fade is better.

For a page or hero section, split it into heading, text and button and offset them ~90–100ms, each entering with opacity, `translateY(8px)` and a 2px blur. And remember that **first paint is not an entrance**: what's already on screen when the page loads should simply be there.

```ts
const delay = (i: number) => Math.min(i, 12) * 0.05; // 50ms apart, capped at 12
```

## Fly, don't teleport

Benji Taylor's rule from the Family app: **if something is on screen before and after a change, it should travel, not disappear and reappear.** Click between tabs that are far apart in each mode.

*[Interactive demo: The active pill is one thing in two places. Let it move.]*

When the pill teleports, your eye has to search for where it went. When it flies, you watch it get there. The same goes for a list item moving to a new position, a card expanding into a full screen, or a stacked modal: the one underneath recedes slightly (scale 0.96, a little blur) because it's coming back, rather than vanishing.

The technique behind most of these is called FLIP (A technique for animating an element to a new position: measure First and Last positions, Invert the difference with a transform, then Play it back to zero): measure where the element is (First), let it move to its new spot (Last), shift it back with a transform so it *looks* unmoved (Invert), then transition the transform away (Play). Motion's `layoutId` does this for you. Crossfading is still right when two things are genuinely different content; forcing travel between unrelated cards is decoration.

*(The pill in this demo moves by clip-path, not width. More on that next.)*

## clip-path: the underrated one

clip-path (A CSS property that cuts away part of an element, like a mask. It animates cheaply and smoothly, which makes it great for reveals, wipes and sliding highlights) cuts away part of an element, and it animates smoothly and cheaply. `inset(top right bottom left)` clips from each edge; both ends of a transition must use the same shape function. Hold this button down.

*[Interactive demo: Hold to delete. Let go early and it snaps back.]*

```css
.fill { clip-path: inset(0 100% 0 0); transition: clip-path 200ms var(--ease-out); }   /* release: fast */
.button:active .fill { clip-path: inset(0 0 0 0); transition: clip-path 2s linear; } /* hold: slow */
```

The press is slow and `linear` because it's progress and you're deciding; the release answers in 200ms. Put a single duration on the base rule and an aborted hold crawls back for two whole seconds.

Other patterns from Emil Kowalski's notes:

- **Tabs whose text flips color exactly under a sliding pill**: two text layers, the top one clipped to the pill.
- **Image reveals on scroll**: `inset(20% 0 20% 0)` to `inset(0)`, like a letterbox opening.
- **Before/after sliders**: the "after" image clipped by a CSS variable that follows the pointer.
- **Directional swaps**: one panel clips out to the left as the next clips in from the right, with no layout shift.

Combine it with rounded corners using `inset(… round 12px)` rather than `border-radius`, which double-clips in Safari.

## Grouped hover: the falloff

Hover or tap a face in this stack. Its neighbours lift too, a little less with each step away.

*[Interactive demo: lift × falloff^distance. Slide the falloff and compare.]*

From Jakub Antalik's avatar group: `shift = lift × falloff^distance`, with the same idea for scale. With a lift of −8px and a falloff of 0.45, the neighbour moves −3.6px, the next −1.6px, and by four away it's under a pixel. **0.4–0.5 is the sweet spot**: 0.8 drags the whole row along, 0.2 is just one face. The lift uses ease-out; the return uses a slight overshoot, so it settles with life. Skip it on dense data tables and lists over ~20 items, and note it can't fire on touch-only screens without a tap.

**Choreograph a list and tabs (paste into your agent):**

```text
Stagger the search results only on their first appearance: 40ms between rows, capped so the whole cascade stays under 300ms, opacity plus translateY(8px), ease-out. Don't re-stagger on filter or sort. Make the active tab indicator one persistent element that slides between tabs (FLIP, layoutId, or clip-path inset on a full-width pill) with cubic-bezier(0.77, 0, 0.175, 1) over ~300ms, instead of re-rendering under each tab.
```

**Check yourself:** A results list staggers 120ms per row across 15 rows every time the user changes a filter. What's wrong?

- [ ] Nothing; stagger makes lists feel polished
- [ ] Only the curve; switch to a spring
- [x] The gap is too long, the cascade far over 300ms, and it re-staggers on every filter change
- [ ] It should stagger from the bottom up

*Why:* 15 rows at 120ms is a 1.7s cascade, the gap is past 80ms, and stagger belongs on first appearance only. Refinements should update in place.

## In the skill

- https://school.agents.org.in/skill/motion/stagger-choreography.md
- https://school.agents.org.in/skill/motion/fly-not-teleport.md
- https://school.agents.org.in/skill/motion/clip-path-tricks.md
- https://school.agents.org.in/skill/motion/distance-falloff-propagation.md


---

# Morphs and swaps

> Change a label, an icon or an image in place so it reads as the same thing changing, not two things trading places.

Module: Motion · Level: intermediate · 7 min · https://school.agents.org.in/design/motion/morphs

Press the button in each mode and watch the first three letters.

*[Interactive demo: Continue becomes Confirm. In one mode, 'Con' never moves.]*

In the crossfade, the whole word fades out and a new word fades in. Your brain sees two buttons. In the shared-letter version, "Con" stays put and only the ending changes, so it reads as one button changing its mind: "got it, now confirm". That's the last lesson's rule applied to text. **What stays the same should stay still; only the difference moves.**

## Shared-letter text

This is a Benji Taylor pattern from Family. The recipe: split the label into characters, keep the characters that match at the same position, and animate only the ones that change (a short fade and an 8px slide, about 180ms, with a tiny stagger).

```tsx
// inside Motion's AnimatePresence, mode="popLayout"
text.split("").map((char, i) => (
  <motion.span key={`${i}-${char}`} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }}>
    {char}
  </motion.span>
));
```

The key, made of the position and the character, is what keeps shared letters mounted. Good pairs share a start: Save → Saved, Connecting → Connected, 12 → 13. Skip it when nothing is shared (Cancel → Done: crossfade instead), for long sentences, and for translated text, where the shared letters change by language.

## Icons that morph

Pick icons one after another and watch the big one.

*[Interactive demo: Every icon is exactly three lines; spares collapse to an invisible dot.]*

Benji Taylor built the Claude app's animated icons on one constraint: **every icon is three SVG lines**. An icon that needs only two collapses its third line to an invisible point at the center. Because every icon has the same shape of data, any icon can morph (Changing one shape smoothly into another in front of the viewer, so it reads as the same object changing rather than two objects swapping) into any other by moving coordinates.

Two strategies, depending on the pair:

- **Different shapes** (menu → close, plus → check): interpolate each line's coordinates.
- **Same shape, different direction** (the four arrows, or plus → × by 45°): just rotate. It's cheaper and reads more clearly as the same object turning.

It works for a small active set (play/pause, expand/collapse, chevrons, a dot → ring → check status), roughly 5 to 20 icons. It's stroke-only, so filled icons and big marketing sets belong to a normal icon pack.

When two stock icons can't share a schema, like copy → check, swap them with a deep scale instead: `scale 0.3 → 1`, opacity `0 → 1` and `blur(3px) → 0`, a 0.35s spring with no bounce. A timid 0.8–0.9 scale looks like a glitch.

> **In plain words.** A **morph** changes one shape into another in front of you. A **crossfade** fades one thing out while another fades in on top of it.

## When you must crossfade, add blur

Some swaps are between things that can't morph: two photos, two illustrations, a sun and a moon. Tap the icon several times in each mode.

*[Interactive demo: Halfway through a plain crossfade you see two ghosts. Blur turns them into one soft shape.]*

Jakub Antalik's most reused trick: pair the crossfade (Fading one thing out while another fades in on top of it. Fine for unrelated content; halfway through it shows two ghost images, which blur can hide) with a small blur, 2px on desktop and 4px on phones, on the same timing.

```css
.swap {
  opacity: 0;
  filter: blur(2px);
  transition: opacity 250ms var(--ease-out), filter 250ms var(--ease-out);
}
.swap[data-state="visible"] { opacity: 1; filter: blur(0); }
```

At the midpoint of a plain crossfade, both images sit at half opacity, a double exposure your eye reads as two objects. Blur replaces that moment with one out-of-focus shape that resolves into the next. Don't blur text (it reads as broken focus), don't use it on hover, and test anything bigger than ~400px on a slow phone.

*(Blur is cheap to move but expensive to draw on big surfaces.)*

## Animated icons: hover by default, a trigger for everything else

If you ship animated icons as components, the pattern from lucide-animated.com is worth copying. The icon plays on `:hover` of its button with zero setup, which is fast, needs no JavaScript and can fire on keyboard focus too. But touch screens have no hover, and some moments aren't hovers at all (a "saved" celebration, an onboarding sequence). So the component also exposes an imperative handle:

```tsx
const ref = useRef(null);
// the like button: onClick={() => ref.current?.trigger()}
// the heart icon component receives ref={ref} and plays when trigger() is called
```

Gate the hover rule behind `@media (hover: hover)` so taps don't leave icons stuck mid-animation. Don't add `trigger()` to icons that don't animate, and don't wrap a permanently spinning loader in a ref.

**Morph instead of swap (paste into your agent):**

```text
For the submit button that changes from "Continue" to "Confirm", keep the shared letters ("Con") in place and animate only the changing characters (opacity and 8px translateY, ~180ms) using AnimatePresence mode="popLayout" with keys of index+char. For icon swaps that can't morph (copy → check), use scale 0.3 → 1, opacity, and blur(3px) → 0 with a spring { duration: 0.35, bounce: 0 }. For image crossfades, add filter: blur(2px) (4px on mobile) on the same timing as the opacity. Never blur text.
```

**Check yourself:** A theme toggle swaps a sun icon for a moon. What's the best transition?

- [ ] An instant swap
- [ ] A plain opacity crossfade
- [x] A crossfade paired with a small blur (2px desktop, 4px mobile) on the same timing
- [ ] Scale the sun down to 0, then the moon up from 0

*Why:* The shapes are too different to morph, and a plain crossfade shows two ghosts at the midpoint. Cross-blur makes that moment one soft shape. Scaling from 0 is the balloon again.

## In the skill

- https://school.agents.org.in/skill/motion/shared-letter-morph.md
- https://school.agents.org.in/skill/motion/morphing-icons.md
- https://school.agents.org.in/skill/motion/cross-blur-transitions.md
- https://school.agents.org.in/skill/motion/hover-default-imperative.md


---

# Gestures, sheets and trays

> Make drags, flicks and bottom sheets feel native, from velocity on release to presses that land on moving things.

Module: Motion · Level: advanced · 10 min · https://school.agents.org.in/design/motion/gestures

Drag the sheet down slowly, about a third of the way, and let go. It snaps back. Now flick it down from the same spot. It's gone.

*[Interactive demo: Drag slowly, then flick. Switch to 'position only' to feel what's missing.]*

With "position only", the sheet ignores how you let go. A quick flick near the top snaps back, and it feels like the app didn't listen. With velocity, a throw is a throw. When a drag "feels off", it's almost always one of three things: **no velocity on release, no resistance at the edges, or no pointer capture.**

## The three fixes

**Velocity on release.** Measure how fast the finger was moving when it lifted. Above roughly 0.11 pixels per millisecond, treat it as a flick and dismiss in that direction; below, snap to whichever position is closer. Don't compute the velocity (How fast and in which direction something is moving, often in pixels per millisecond. Measured when a finger lifts, it tells a flick from a slow release) from the last two events; one late event can halve or double it. Keep the last ~20 pointer samples and measure across the final ~100ms, using each event's own timestamp.

**Resistance at the edges.** Drag the sheet *up* past its open position: it moves, but less and less. That's Apple's rubber band (The resistance you feel when dragging past an edge on iOS: the content follows less and less the further you pull, then springs back when you let go):

```ts
// x = distance past the edge, d = the container's size
const rubberband = (x: number, d: number, c = 0.55) => (x * d * c) / (d + c * Math.abs(x));
```

Near the edge it follows at about 55% of the finger; far past it the offset levels off, so no drag can fling content off-screen. A hard stop feels like hitting a wall.

**Pointer capture.** Call `setPointerCapture(e.pointerId)` on pointer down, and the drag keeps tracking even when a fast finger leaves the element. Also ignore second fingers (`if (!e.isPrimary) return`) so a stray touch doesn't break the gesture.

> **In plain words.** **Velocity** is speed with a direction, here in pixels per millisecond. **Pointer capture** tells the browser "keep sending this finger's moves to me until it lifts".

## After the finger lifts

A flick is a small input asking for a big output. The sheet above does two more things, both from Apple's *Designing Fluid Interfaces* talk as Emil Kowalski distilled it.

**Project, then pick.** Work out where the flick would come to rest, the way scrolling decelerates, then snap to the target nearest *that* point, not the release point:

```js
const project = (v /* px/s */, d = 0.998) => (v / 1000) * d / (1 - d);
const target = nearestSnapPoint(current + project(releaseVelocity));
```

At 1000px/s that's about 500px ahead; a gentle 200px/s stays on its current snap. Decide commit-or-reverse by the velocity's **direction**: a sheet dragged 70% open and flicked down closes.

**Hand the speed to the spring.** Start the spring with the release velocity so there's no seam between finger and animation. Projection without the hand-off picks the right target and then starts from rest, which looks like a stall. A few more rules for interruptible motion:

- Start any new animation from where the element is **drawn**, never from where it was heading.
- Never lock input during a transition. Grab a closing sheet and it follows your finger at once.
- Keep the **grab offset**: the element stays where your finger took it, never recentred.
- Two-dimensional throws get **one spring per axis**.

## Presses on moving things

A stack of toasts slides up after you dismiss one. You press the same spot three times quickly. Which toasts close?

Cheng Lou's answer, measured in his handwired project: test presses against where each item is **heading**, not where it's drawn. Then three quick presses close three toasts, the way rapid clicks close Safari tabs one after another. Test against the drawn position and only the first press lands; the rest hit the gap a toast is still sliding into. This is called hit testing (Working out which element a press landed on. For things still animating, test against where they are heading, so quick repeated presses land where people expect).

- **A press** goes to the destination. **The start of a drag** grabs what's drawn under the finger.
- **While dragging**, pin the spring to the pointer (position and target both follow it), so there's no springy lag.
- **After release**, keep the item above its neighbours until it settles, or it slides *under* them on the way home.
- **In a scrolling list**, select on release, not on press: a press that starts a scroll would otherwise select every row it touches.

## Trays: Benji Taylor's six rules

A tray (A sheet that rises from the bottom over the current screen for one short task; the next step stacks over it. Each tray has a title, a dismiss and its own height) is a sheet that rises from the bottom over the current screen, and the next one stacks over it. Family, the crypto wallet Benji Taylor designed, is built on them.

*[Interactive demo: A send flow in trays: three heights, one job each, the screen still there behind.]*

  **Step: User-initiated.** Trays open because someone tapped something. Never on load or on a timer; that's a banner's job.

  **Step: Heights vary.** Each tray in a stack is noticeably taller or shorter than the last. Same-height trays look like nothing happened. This is the rule teams break most.

  **Step: One job each.** One question, one choice, one form. A second question is a second tray.

  **Step: Title and dismiss, always.** A clear title, an × button, tap-outside and swipe-down to close (with velocity, not "more than 50px").

  **Step: Overlay, don't navigate.** The previous screen stays visible, dimmed, behind. You've risen a layer, not left.

  **Step: Short tasks only.** Confirm, enter a code, pick a person, rename. Onboarding, settings and long forms get real screens.

*(The tray in the demo is one tall sheet moved with transform, so its edge glides without animating height.)*

## On a phone app

If you build for React Native or Expo, the same rules apply with three changes: there's no hover, there are two threads, and a finger is on the element. Emil Kowalski's digest:

- Run motion on the UI thread with **Reanimated**, gestures with `Gesture.Pan()`, and never `setState` inside a gesture handler.
- **Use the platform's navigation** (native stack, form sheets, native tabs). Tabs never slide.
- Springs for anything a finger touched, with the release velocity: settle `{ duration: 400, dampingRatio: 1 }`, sheet `{ duration: 300, dampingRatio: 0.8, velocity }`.
- Press is `scale: 0.97` in 100–150ms; targets 44pt (48dp on Android), grown with `hitSlop` rather than visually.
- **Haptics on the causal frame**, once per action: a light impact when something snaps home, a notification for success or error. Never as the only feedback.
- Judge feel on a **release build** on your slowest phone. The simulator hides exactly the problems that matter.

**A sheet that feels native (paste into your agent):**

```text
Make the bottom sheet draggable with pointer events: setPointerCapture on pointerdown, ignore non-primary pointers, keep the grab offset. Past the open position apply a rubber band (x*d*0.55 / (d + 0.55*|x|)). On release, compute velocity over the last ~100ms of samples (event timestamps). Dismiss if velocity > 0.11 px/ms downward or it's past halfway; reverse if flicked up. Hand the release velocity to a spring { stiffness: 380, damping: 30 } and start from the drawn position. Never block input during the animation.
```

**Check yourself:** A user flicks a sheet downward quickly but releases it only 20% of the way down. What should happen?

- [ ] Snap back open, because it's less than 50% down
- [x] Dismiss it, because the release velocity says the user threw it
- [ ] Stop exactly where it was released
- [ ] Bounce several times, then settle open

*Why:* Above roughly 0.11 px/ms the release reads as a throw, so the direction of the velocity decides, not the position. Ignoring it is why position-only sheets feel like they didn't listen.

## In the skill

- https://school.agents.org.in/skill/motion/gesture-momentum.md
- https://school.agents.org.in/skill/motion/momentum-projection.md
- https://school.agents.org.in/skill/motion/animated-hit-testing.md
- https://school.agents.org.in/skill/motion/tray-rules.md
- https://school.agents.org.in/skill/motion/react-native-motion.md


---

# Smooth, kind and checked

> Keep animation smooth on slow phones, respect people who ask for less motion, and debug or review motion before it ships.

Module: Motion · Level: intermediate · 10 min · https://school.agents.org.in/design/motion/performance-and-review

Tap a property you might animate. Some cost the browser almost nothing per frame. Others make it recalculate the whole page, sixty times a second.

*[Interactive demo: What each property costs per frame, and what to animate instead.]*

A screen redraws 60 times a second (16.7ms per frame), and a 120Hz phone gives you 8ms. Design against 8. One layout pass on a busy page can take longer than that by itself.

## Cheap properties only

There are three tiers. **Composite**: the browser hands a finished picture to the graphics chip and slides it around; that's `transform` and `opacity` (and, in current browsers, `filter` and `clip-path`). **Paint**: it redraws pixels every frame, like `box-shadow` or `color`. **Layout**: it re-measures everything and moves the neighbours, like `width`, `height`, `top`, `margin`.

**Animate transform and opacity; swap everything else down a tier.** Size becomes `scale()`, position becomes `translate()`, showing and hiding becomes `opacity`, a growing shadow becomes the opacity of a layer that already has it. A collapsing panel uses `grid-template-rows: 0fr → 1fr` rather than animating `height`.

  **Before (avoid):**

```css
.card {
  transition: all 300ms;
}
.card:hover {
  width: 320px;
  box-shadow: 0 12px 32px #0002;
}
```

  **After (do this):**

```css
.card {
  transition: transform 200ms
    var(--ease-out);
}
.card:hover {
  transform: translateY(-2px);
}
```

## The rest of the discipline

Most jank is self-inflicted. From Emil Kowalski's performance notes:

- **Name the properties.** `transition: all` watches every style change. Tailwind's bare `transition` class compiles to the same thing; use `transition-transform` or `transition-[opacity,transform]`.
- **`will-change` is a targeted fix, not a sprinkle.** Add `will-change: transform` only to an element where you've seen a first-frame hitch. On every card, it eats graphics memory and fixes nothing.
- **Never animate a CSS variable on a shared parent**; every child recalculates. Vaul's drawer got laggy at ~20 items for exactly this reason.
- **Frames never go through React state.** Write per-frame values through refs, the Web Animations API or the library's own loop. In Motion, the string `transform: "translateX(100px)"` is hardware-accelerated; the `x: 100` shorthand runs on the main thread, which only hurts when the page is busy.
- **Use CSS for planned motion** that has to stay smooth while the page loads. Vercel's dashboard tab indicator dropped frames during navigation until a CSS animation replaced a JavaScript one.
- **Virtualize lists past ~50 rows**, pause off-screen loops, reserve space for images and loading content, and mute all transitions for a frame while switching themes so colors don't tween in a ragged cascade.

When it stutters, name the cause before changing code, in this order: a non-composite property, jank that lines up with loading, React re-rendering per frame, slowdown as content grows, an animated CSS variable, a large blur, and only then `will-change`.

## Reduced motion

Some people set their device to reduce motion (A device setting people turn on when movement makes them unwell or distracted. Interfaces should drop slides, zooms and parallax, but keep short fades): people with vestibular conditions, for whom sliding and zooming cause real nausea, and others who simply prefer less. Try each setting here.

*[Interactive demo: Four ways to honour the setting. Only one is right; one is a real bug.]*

Reduced motion doesn't mean "no animation"; it means **no travel**. Drop large slides, zooms, parallax, autoplay and spinning loaders. Keep short opacity fades and color changes, which explain a change without moving anything.

```css
@media (prefers-reduced-motion: reduce) {
  .modal { transform: none; transition: opacity 150ms ease-out; }
}
```

In React with Motion, `useReducedMotion()` lets you set the slide distance to 0 and keep the fade. Some teams take the stricter line and remove the fades too; either way, ship the reduced version with the animation, not later.

*(The worst bug: text that starts at opacity 0 and only fades in under "no-preference".)*

The worst bug is in the demo's last mode. If an entrance starts from `opacity: 0` and the animation only runs when motion is allowed, people who reduce motion never see the content at all. The reduced state is the **finished** state. Test with the setting on.

## Debugging: watch it slowly

Open this popover at full speed. It looks fine. Now try 10%.

*[Interactive demo: A bug that only shows when you slow down. Then apply the fix.]*

At full speed, the eye can't see that the box finishes growing while it's still half see-through, because its scale and opacity run on different curves and times. At quarter speed it's obvious. Almost every "it feels off" bug is visible at 25% and invisible at 100%.

  **Step: Slow it down.** Chrome DevTools has an Animations panel with 10%, 25% and 50% playback, no code changes needed. Or wrap a Motion app in its MotionConfig component with `transition={{ duration: 2 }}`. Scope any `!important` speed hack to the one component, and never ship it.

  **Step: Look frame by frame.** Record in the Performance panel. Frames over 16ms, and layout or paint during an animation, point at the property you're animating.

  **Step: Use a real phone.** Connect it over USB and inspect it (Safari's Develop menu for iPhone, `chrome://inspect` for Android). Desktop throttling doesn't model a weak graphics chip.

  **Step: Record before and after.** A fix to one animation quietly changes others. Record the same interaction twice and compare by moments (the click, the peak, the settle), not milliseconds.

When you review someone else's motion, slow it to 25% before you approve it.

**Make motion fast and kind (paste into your agent):**

```text
Audit the animations in this app. Replace any transition: all (including Tailwind's bare transition class) with named properties. Rewrite animations of width, height, top, left, margin or box-shadow to use transform, opacity, or the opacity of a pre-shadowed layer. Remove blanket will-change. Add a prefers-reduced-motion: reduce block for each animated component that removes translate and scale but keeps a short opacity fade, and make sure no content stays at opacity 0 when motion is reduced. Tell me which animations you checked at 25% speed and what you found.
```

**Check yourself:** With reduced motion on, a hero heading never appears. Its CSS sets opacity: 0 and runs a fade-in only inside @media (prefers-reduced-motion: no-preference). What's the fix?

- [ ] Set every animation to 0ms with !important
- [x] Make the visible, finished state the default and only add the hidden start state when motion is allowed (or keep a short fade under reduce)
- [ ] Remove the heading for those users
- [ ] Use a spinner while the heading loads

*Why:* The reduced state must be the finished state. The start state (opacity: 0) belongs inside the motion-allowed rule, or the reduced path keeps a short fade so the text still arrives.

## In the skill

- https://school.agents.org.in/skill/motion/transform-opacity-only.md
- https://school.agents.org.in/skill/motion/performance-discipline.md
- https://school.agents.org.in/skill/motion/prefers-reduced-motion.md
- https://school.agents.org.in/skill/motion/debugging-animations.md


---

# Color you can do math with

> Read and write colors in OKLCH, build an even color ramp from one hue, and get an agent to migrate a hex palette without breaking it.

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/oklch

Drag the **L** slider below. The color gets lighter, and it stays the same blue. Now drag **H**: the hue spins round the wheel, and the color stays exactly as light. That is the whole trick of this lesson.

*[Interactive demo: One color, three numbers. The ramp underneath is built from the same hue.]*

## Three numbers you can read

Most colors on the web are written as hex (The familiar #3b82f6 style of writing a color: amounts of red, green and blue in base 16. Easy for screens, meaningless to read: you can't tell how light #3b82f6 looks): `#3b82f6`. A screen understands that. A person doesn't. You can't look at `#3b82f6` and know whether it is light or dark, or what happens if you nudge it.

OKLCH (A way of writing a color as three numbers: L (how light it looks to a person), C (how vivid) and H (which hue, as an angle). Because the numbers follow what eyes see, you can do math with them) writes the same blue as `oklch(0.623 0.188 260)`, three numbers in a row:

- **L**, lightness (How bright a color looks to a person, from 0 (black) to 1 (white). In OKLCH it is measured, so two colors with the same L really look equally light): how bright it looks to a person, 0 (black) to 1 (white).
- **C**, chroma (How vivid a color is. 0 is gray; around 0.3 is as saturated as most screens go. The highest chroma you can use depends on the hue and the lightness): how vivid. 0 is gray; around 0.3 is the loudest most screens show.
- **H**, hue (Which color it is, as an angle on a wheel: about 25 is red, 90 yellow, 145 green, 260 blue, 330 pink): which color, as an angle. Roughly 25 red, 90 yellow, 145 green, 260 blue, 330 pink.

> **In plain words.** OKLCH is a way of writing colors where the numbers mean what they say. Two colors with the same L really look equally light, so you can build palettes, fix contrast and make dark mode with simple arithmetic instead of guesswork.

## Why not HSL?

You may have met HSL (An older color notation (hue, saturation, lightness). Its lightness is a guess: yellow and blue at the same HSL lightness look nothing alike, so ramps built in HSL come out uneven), which also has a "lightness". The problem is that it lies. Toggle between the two rows below: at "50% lightness" HSL's yellow glows and its blue sinks. OKLCH's L 0.7 is one level across every hue.

*[Interactive demo: The gray bars measure how light each swatch really looks.]*

A ramp spaced on HSL inherits that lie: some steps jump, others barely move, and the light tints drift toward violet. **Never generate a scale in HSL.**

## Building a ramp

A ramp (One hue in a row of steps from nearly white to nearly black, usually numbered 50 to 950. A palette is a few ramps, not a pile of colors) is one hue in eleven steps, named 50, 100, 200 … 900, 950. The recipe the skill teaches has four parts, and you can watch each one in the demo above.

  **Step: Anchor L and space it evenly.** Step 50 sits near L 0.97, step 950 near L 0.22, and the steps between are evenly spaced. Even spacing is the point: every neighbor looks equally far apart. Never run to pure 0 or 1, where the color stops reading as its hue.

  **Step: Shape chroma as a curve.** Give each step a share of the most chroma it can hold: about 30% at the ends, rising to about 85% around 400–500. Tints stay airy, shades stay rich.

  **Step: Clamp per step.** The ceiling moves with lightness, so a vivid blue necessarily loses chroma toward both ends. That is the geometry of color, not a bug.

  **Step: Keep the hue fixed.** One H for all eleven steps. If an inherited scale wanders more than about 10° end to end, rebuild it on its middle hue.

## Colors a screen can't show

Push **C** up in the first demo and a warning appears: "past sRGB". OKLCH can name colors no ordinary screen shows. The range a screen can show is its gamut (The set of colors a screen can actually show. OKLCH can name colors outside it; those get clamped to the nearest one the screen can display), and its edge moves with hue and lightness. At L 0.5, violet can go to C 0.29 while cyan tops out near 0.09.

So two hues never share one chroma number. A brand blue, a success green and a warning amber feel like one family when each sits at the **same L** and the **same share of its own ceiling**.

When a value goes past the edge, clamp it yourself: keep L and H, lower C until it fits. Left alone, the browser maps it in a way you didn't choose.

```css
.cta { background: oklch(0.66 0.2 330); }                       /* safe everywhere */
@media (color-gamut: p3) { .cta { background: oklch(0.66 0.32 330); } } /* extra punch on wide-gamut screens */
```

That second line is for Display P3 (A wider gamut than standard sRGB, found on most recent phones and Macs. Extra-vivid colors can be layered on for these screens with a media query), the wider gamut on recent phones and Macs. *(The punch goes in the P3 layer. Raising the base color's chroma breaks it on every ordinary screen.)*

## Migrating a hex palette

Moving an existing file to OKLCH is a value-for-value swap. Nothing else in the file should change: not the order, not the comments, not the gradient shapes. Watch an agent do it properly.

*[Interactive demo: The agent converts values, keeps keywords, and flags a contrast problem instead of quietly fixing it.]*

The rules it followed:

1. Hex first, then `rgb()`/`rgba()`, then `hsl()`.
2. Alpha goes behind a slash: `rgba(255,255,255,.12)` becomes `oklch(1 0 0 / 0.12)`.
3. Never convert keywords (`currentColor`, `transparent`, `inherit`) or anything a third-party library reads as hex.
4. Re-check contrast on the pairs that matter, and report every changed line, old beside new.

**Migrate a palette to OKLCH (paste into your agent):**

```text
Migrate the colors in src/styles/tokens.css to oklch().
- Value for value: hex first, then rgb()/rgba(), then hsl(). Alpha goes after a slash.
- Keep comments, order and indentation; don't touch keywords (currentColor, transparent) or hex a library parses.
- Clamp anything out of sRGB by lowering chroma, holding L and H.
- Re-run contrast on text pairs and list any that fail 4.5:1. Don't fix them silently.
- Report every changed line: old value, new value, one-clause reason.
```

**Check yourself:** A brand teal and a brand violet should feel equally vivid. What do you copy between them?

- [ ] The same C value, e.g. 0.2
- [x] The same L and the same share of each hue's own chroma ceiling
- [ ] The same hex brightness
- [ ] The same HSL saturation

*Why:* Cyan-ish hues have a far lower chroma ceiling than violet. Copying one C over-saturates the roomy hue or flattens the tight one; matching L and the share of each ceiling keeps them a family.

## In the skill

- https://school.agents.org.in/skill/surface/MOC-surface.md
- https://school.agents.org.in/skill/surface/color-scales-oklch.md
- https://school.agents.org.in/skill/surface/oklch-gamut-and-migration.md


---

# One neutral, one accent

> Build a palette from a gray ramp and one accent, give every step a job, and name your colors so a dark theme is a five-minute change.

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/palette

Here is the same little project list twice. Flip between them and notice where your eye goes first.

*[Interactive demo: Six colors compete; one accent leads.]*

With a color per thing, everything shouts and nothing wins. In grays with one blue, your eye lands on the blue button and the one live project, which is exactly where it should land.

## The whole palette is about fourteen colors

The skill's recipe, borrowed from how Linear, Vercel and Notion work:

1. One neutral (The gray ramp that carries most of an interface: backgrounds, borders and text. It can be pure gray or tinted very slightly warm or cool) ramp, 8 to 12 steps of gray. Pure gray is fine; a hint of warm or cool is nicer.
2. One accent (The one saturated brand color, kept for what you can act on: the main button, links, the selected item. It works because almost nothing else is colorful), saturated and deliberate, kept for things you can act on.
3. Optionally, quiet status colors (danger, warning, success) for the states your product actually shows.

That's it. A tokens file with sixty colors isn't richer; it's undecided.

> **The purple gradient.** A violet-to-fuchsia gradient, or "a different bright color per card", is the single most recognizable sign of a generated page. Gradient text on a hero headline counts too. Delete it first.

Three small rules keep the set coherent:

- **Neutrals share the brand's temperature.** A blue brand wants cool grays. An untinted `#808080` next to a tinted surface reads as a placeholder.
- **Light tints come from lower chroma, not lower opacity.** A see-through accent turns gray on some backgrounds.
- **Disabled gets its own token**, never `opacity: 0.5`, which passes contrast on one surface and fails on the next.

## Every step has a job

A ramp (One hue in a row of steps from nearly white to nearly black, usually numbered 50 to 950. A palette is a few ramps, not a pile of colors) isn't a gradient to pick from by eye. Each step exists because some part of the UI uses it: the page background, the hover state, the border, the button fill, the text. Tap a role below and watch which parts of the card light up.

*[Interactive demo: Tap a role. Toggle the names to see Tailwind's numbers and Radix's.]*

Two naming styles exist. Tailwind numbers steps **by lightness** (50 is lightest), so in dark mode the mapping flips. Radix numbers them **by role** (step 9 is always the solid fill, in light and dark), so component CSS never changes. Match the project you're in; for a new system the role model is kinder.

Notice the line under the card. **Solid fills sit on 600, not 500**, because white text on most 500 hues fails the 4.5:1 contrast you'll meet in the next lesson.

A few more rules that hold across every palette:

- **One color, one meaning.** Hues within about 15° look like the same color, so a link blue and a decorative blue heading are one meaning used twice.
- **Status sits at least 15° from the accent.** A red brand can't also use red for "delete"; give destruction a distinct treatment.
- **One filled button per view.** The fill is what says "primary" from across the room.

## Name colors by job, not by look

Now the part that makes a palette survive a redesign. Tokens come in two tiers.

Primitives (A token that names a raw value by what it is, like --blue-600. Primitives never get used directly inside a component) name a value: `--blue-600`, `--neutral-200`. They say what the color *is* and never change meaning.

Semantics (A token that names a job, like --color-bg-page, and points at a primitive. Components use only these, so a dark theme just repoints them) name a job and point at a primitive: `--color-text-secondary: var(--neutral-700)`. **Components use only these.**

*[Interactive demo: Switch to dark. The primitives and the component stay as written; only the arrows move.]*

That's why the split matters. If a component says `--blue-600` directly, adding dark mode means reading every usage to guess which ones meant "the accent". If it says `--color-accent-solid`, dark mode repoints one line.

  **Named by look (avoid):**

`--color-blue-button`, `--color-sidebar-gray`, `--color-light-gray` (which is the dark one in dark mode), `--color-text-2`.

  **Named by job (do this):**

`--color-accent-solid`, `--color-bg-surface`, `--neutral-200` (a primitive), `--color-text-secondary`.

Two naming rules from Jakub Krehel's work save real pain:

- **One word per concept.** Pick `text`, `bg`, `border`, `accent` and never mix in `fg`, `foreground` or `brand`. Someone who has seen `--color-text-primary` should be able to guess `--color-text-disabled`.
- **Call the brand `accent`, not `primary`.** Otherwise `--color-primary` (the brand) sits next to `--color-text-primary` (body text), the most common collision there is.

Build against a full list of roles from day one (page, surface, raised, sunken; text primary, secondary, disabled, on-accent; border subtle, default, strong, focus; separator), not one token at a time as screens ask. *(Separator and border are two roles even when they share a color today. They split the first time someone restyles inputs.)*

**Set up a palette (paste into your agent):**

```text
Build our color tokens:
- One neutral ramp (11 steps, OKLCH, slightly cool to match the blue brand) and one accent ramp. Status colors only for danger and success, each ≥15° of hue from the accent.
- Two tiers: primitives named by hue and step (--blue-600), semantics named by role (--color-bg-page, --color-text-secondary, --color-accent-solid). Components use semantics only.
- Solid fills on step 600. Disabled gets its own tokens, never opacity.
- Before restructuring anything existing, list every color literal in the repo, sorted by hue and lightness, and show me the inventory first.
```

**Check yourself:** A component uses bg-blue-600 directly. You're adding dark mode. What's the fix?

- [ ] Add a dark: variant on that component
- [x] Point the component at a semantic token like --color-accent-solid, and repoint that token for dark
- [ ] Lower the button's opacity in dark mode
- [ ] Invert the colors with a CSS filter

*Why:* Components should reference only semantic tokens. Then dark mode (or any theme) repoints the semantics in one place and no component changes.

## In the skill

- https://school.agents.org.in/skill/surface/color-monochromatic.md
- https://school.agents.org.in/skill/surface/palette-step-roles.md
- https://school.agents.org.in/skill/surface/color-token-tiers.md


---

# Contrast you can read

> Check text against 4.5:1 and APCA, repair a failing pair by moving lightness only, make hover states raise contrast, and pick the space a gradient mixes in.

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/contrast

The gray in this box is `#999` on white, the most popular "subtle" text color on the web. Read the paragraph, then look at the score.

*[Interactive demo: Drag the text's lightness, or press the fix. Only L moves.]*

2.85:1. It fails. Light gray on white looks calm and refined in a mockup, and on a sunny phone or a tired pair of eyes it simply isn't there.

## What the numbers mean

A contrast ratio (How different the text color is from its background, as a number like 4.5:1. Body text needs at least 4.5:1 to be readable for most people; large text 3:1) compares how light the text is with how light its background is. 1:1 is invisible; 21:1 is black on white.

The rule most laws point to comes from WCAG (The Web Content Accessibility Guidelines, the standard most laws point to. Its contrast rule is the familiar 4.5:1 for body text and 3:1 for large text):

| Text | Needs at least |
|---|---|
| Body text | **4.5:1** |
| Large text (about 24px, or bold from about 18.5px) | **3:1** |
| Icons, borders, focus rings | **3:1** |

APCA (A newer contrast formula that models how people actually perceive text, scored as Lc: about 75 for body text, 60 for labels, 45 for headlines) is a newer formula that tracks how people actually perceive text, especially light text on dark. It scores in **Lc**: 75 or more for body text (90 is better), 60 for labels, 45 for headlines, 30 as the floor for icons and disabled states. The skill's advice: **run both, pass both.**

> **In plain words.** WCAG's 4.5:1 is the legal line. APCA is the better eye test. A color pair that clears both reads well for nearly everyone.

## Fix contrast by moving L only

Here is where OKLCH from the first lesson pays off. Contrast lives almost entirely in the gap between the two **lightness** values. Chroma and hue barely move it.

So to repair a failing pair, push the text's L away from the background's L and leave C and H alone. "Make it more saturated so it pops" does nothing for readability.

A fast triage before you measure:

- On near-white backgrounds (L 0.85 and up), text wants L 0.45 or below.
- On near-black backgrounds (L 0.25 and down), text wants L 0.75 or above.
- Anything with L above about 0.6 wants dark text on it.

  **Before (avoid):**

`color: #999;` on white. 2.8:1, fails body text.

  **After (do this):**

`color: oklch(0.5 0 0);` on white. 6:1, passes. Same hue, same chroma, lower L.

Two more traps:

- **No text on a see-through color.** A color with alpha mixes with whatever is under it, so no single measurement describes it. Text sits on solid tokens.
- **Text on a gradient** has a different contrast at every pixel. Measure the worst spot, not the average, or put a solid scrim behind the words.

## Hover should raise contrast, not lower it

A common reflex is to fade a button on hover. Backwards. The skill's rule: hover makes an element a little *more* contrasty, pressed more again. **Disabled is the only state that lowers contrast**, and it does so with its own tokens, not opacity.

```css
.button        { background: var(--bg-elevated);        border-color: var(--border-default); }
.button:hover  { background: var(--bg-elevated-hover);  border-color: var(--border-strong); }
.button:active { background: var(--bg-elevated-active); }
.button:disabled { background: var(--bg-disabled); color: var(--text-disabled); }
```

## Tell the browser which theme you drew

Two small declarations make the browser's own parts match your page.

`color-scheme` (A CSS property that tells the browser whether to draw its own parts (scrollbars, form controls) light or dark. Declare only the themes you actually designed) tells it whether to draw scrollbars, checkboxes and `<select>` menus light or dark. Declare only what you designed: a light-only site writes `color-scheme: light`. Writing `light dark` on a light-only site hands dark-mode users dark native dropdowns on a white page.

`theme-color` (A meta tag that colors the phone browser's address bar and status bar so they blend with the top of your page) colors the phone's address bar to match the top of your page, so your design doesn't stop abruptly at a gray browser strip.

```html
<meta name="theme-color" content="#fdfdfc">
```

## Gradients: the space they mix in

The same two colors make three different gradients, depending on the color space the browser mixes them in. That is gradient interpolation (The color space a gradient mixes its stops in. The same two colors make a muddy, an even, or a vivid gradient depending on it (sRGB, OKLab or OKLCH)). Try blue to yellow below.

*[Interactive demo: Same stops, three spaces. Blue to yellow goes gray in the middle of sRGB.]*

- **sRGB**, the default, darkens and muddies the middle.
- **`in oklab`** keeps brightness even across. It is the best default.
- **`in oklch`** travels round the hue wheel, so it stays vivid, and may pass through a hue nobody asked for (blue to pink goes via purple). Add `longer hue` and it sweeps most of the rainbow.

Colors on opposite sides of the wheel, like blue and yellow, pass near gray in a straight-line space. Either switch to `in oklch` or add a third stop between them.

Large, low-contrast gradients can also show banding (Visible stripes in a gradient that should be smooth, common on large, low-contrast gradients. A faint noise layer hides it), visible stripes where it should be smooth. Widen the gap between stops, shrink the area, or lay a faint noise texture over it. *(Picking the space is taste. A gray dead zone, banding, or unreadable text on the gradient is a bug.)*

**Audit contrast (paste into your agent):**

```text
Audit text contrast in this project.
- For every text/background pair, report the WCAG 2 ratio and the APCA Lc. Body text must pass 4.5:1 and Lc 75; labels Lc 60; large text 3:1.
- Fix failures by moving the foreground's OKLCH L only; leave C and H.
- Hover and active states must raise contrast, never lower it. Disabled uses its own tokens, not opacity.
- No text on colors with alpha. Set color-scheme to only the themes we actually ship.
- Gradients: use "in oklab" unless the design wants a hue sweep; flag any with text whose worst region fails.
```

**Check yourself:** Gray caption text measures 3.1:1 on white. What's the right repair?

- [ ] Increase its chroma so it pops
- [ ] Make the font a little bolder
- [x] Lower its OKLCH L until it passes 4.5:1, keeping C and H
- [ ] Add a text-shadow

*Why:* Contrast lives in the lightness gap. Moving L fixes it; chroma, hue and effects barely change the score.

## In the skill

- https://school.agents.org.in/skill/surface/contrast-and-color-scheme.md
- https://school.agents.org.in/skill/surface/gradient-interpolation.md


---

# Shadows, borders and corners

> Lift surfaces with shadows that whisper, separate them with borders, nest rounded corners correctly, and trust your eye over the ruler.

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/shadows-and-corners

Four ways to lift the same card. Start on "The default", then try "Whisper".

*[Interactive demo: The heavy one announces. The whisper you feel without seeing.]*

`0 2px 8px rgba(0, 0, 0, 0.1)` is the most copied shadow on the web. Nothing is wrong with it except that it is everywhere, so it reads as "template".

## Shadows should whisper

A shadow's job is to hint that a surface floats, which designers call elevation (How far a surface seems to float above the page: a card a little, a menu more, a dialog most. Shown with shadows in light mode and lighter surfaces in dark mode). If you can clearly *see* a soft gray blur, it's too strong.

The fix is a layered shadow (Several faint shadows stacked in one box-shadow: a tight one, a wide soft one, sometimes a 1px ring. It reads as real depth where one blurry shadow reads as a template): several faint shadows stacked in one declaration.

```css
--shadow-md:
  0 1px 2px  rgba(17, 17, 17, 0.05),
  0 4px 8px  rgba(17, 17, 17, 0.05),
  0 8px 24px rgba(17, 17, 17, 0.04);
```

What makes it work:

- **Several layers** at different blurs (2px, 8px, 24px) read as physical depth. One big blur looks flat.
- **Opacity at 4–6%, not 10%.** This is the part most people get wrong.
- **A near-black base** like `#111`, not pure black.
- **The y-offset never bigger than the blur.**

Build at least three, for three heights: cards (+1), dropdowns and popovers (+2), dialogs (+3).

## Shadows raise; borders separate

Pick by the job the line is doing.

- **Raised** things (cards, menus, popovers, dialogs) get a shadow. Because a shadow is see-through, one token works over any background.
- **Separated** things (table rows, list dividers, input edges) keep a border. A visible input edge is an accessibility feature.

**The rule is about elevation, not every line.** An agent that reads "use shadows instead of borders" and strips the edges off inputs and table cells has made the UI worse. And use **one depth cue per surface**: border plus shadow plus ring plus gradient on one card is hedging.

A few finishing lines from Emil Kowalski's practice:

- **Hairlines (The thinnest possible line: 1px, or 0.5px on high-density screens. Used for dividers and the outline around images)** at 0.5px on high-density screens, 1px elsewhere. Retina dividers at 1px look heavy.
- **Every image gets a 1px inset outline** in pure black (or white in dark) at 10%. A tinted gray picks up the background and looks like grime.
- **Fade with `mask-image`**, not a gradient laid on top, and never fade content people are scrolling toward.

## Corners: a scale, and the nesting rule

Border radius (How rounded a box's corners are, in pixels. A system uses a few deliberate sizes, not the same radius on everything) is a personality dial. The same radius on everything is lazy; a small scale used on purpose looks crafted.

```css
--radius-sm: 4px;   /* badges */
--radius-md: 8px;   /* inputs, small buttons */
--radius-lg: 12px;  /* cards */
--radius-xl: 16px;  /* modals */
--radius-full: 9999px; /* pills, avatars */
```

Mixing far-apart sizes (4px badges in a 16px modal) is fine. Mixing near ones (4px and 8px side by side) looks like a mistake. And if a button is a pill, everything inside it commits to the pill too.

Now the rule people miss most. When a rounded box sits inside another, the inner radius isn't the same as the outer one. It's smaller, by the gap:

**inner radius = outer radius − padding**

*[Interactive demo: Turn the rule off and watch the corners pinch. Past about 24px of gap, the two surfaces read as separate.]*

With equal radii, the gap between the two curves gets thinner at each corner and the inner box seems to bulge. With the subtraction, the curves run parallel. *(Copying the parent's radius is the single most frequent giveaway of an unpolished UI.)*

## Trust your eye over the ruler

Perfect math sometimes looks wrong. A triangle centered by its bounding box looks shoved left, because most of its box is empty. A circle the same height as a square looks smaller, because it doesn't fill its corners.

*[Interactive demo: Mathematical versus optical. The guides show the math; the right-hand version is what designers ship.]*

The fix is optical alignment (Nudging things until they look aligned to the eye, even when the measurements say they were already aligned. A play triangle moves a little right; a circle grows a little): nudge until it *looks* right. The play glyph moves a pixel or two right; the circle grows about 5%. If it looks wrong, it is wrong.

The same idea, a little imperfection on purpose, extends further on marketing pages: a faint paper-grain texture instead of a flat fill, a hand-drawn stroke, an asymmetric layout that feels composed. Keep it there. Inside the product (forms, tables, settings) consistency wins, and "imperfect" just means "broken".

**Fix shadows and corners (paste into your agent):**

```text
Review the surfaces in this component:
- Replace any single-blur box-shadow with our layered tokens (--shadow-sm/md/lg: 2–3 layers, 4–6% opacity, base #111, y-offset ≤ blur).
- Shadows only on raised surfaces (cards, menus, dialogs). Keep borders on inputs, table cells and dividers.
- One depth cue per surface.
- Nested rounded elements: inner radius = outer radius − padding.
- Images get outline: 1px solid rgb(0 0 0 / .1) with outline-offset: -1px.
Return a Before | After | Why table.
```

**Check yourself:** A dialog has border-radius 16px and padding 6px. What radius should the panel inside it get?

- [ ] 16px, to match
- [x] 10px
- [ ] 22px
- [ ] 6px

*Why:* Inner radius = outer − padding: 16 − 6 = 10px. Equal radii pinch the gap at every corner.

## In the skill

- https://school.agents.org.in/skill/surface/shadows-whisper.md
- https://school.agents.org.in/skill/surface/depth-and-nesting.md
- https://school.agents.org.in/skill/surface/border-radius.md
- https://school.agents.org.in/skill/surface/visual-imperfection.md


---

# Designing a dark theme

> Build a dark theme from the same color scale as the light one, with near-black surfaces, softened text, lighter layers for depth and calmer accents.

Module: Color, depth and surfaces · Level: intermediate · 7 min · https://school.agents.org.in/design/surface/dark-mode

This course is light only, so here is a dark theme in a box. It starts the way most generated dark modes start. Flip the four switches, one at a time.

*[Interactive demo: Start broken, fix one thing at a time. The light screen beside it uses the same scale.]*

The first version is the light theme run through an inverter: pure black, pure white, shadows that vanish into nothing, and a blue that buzzes. Each switch is one of the skill's rules.

## Never pure black

Pure `#000000` under pure `#ffffff` is harsh. Surfaces feel cheap, and on OLED phones scrolling text smears. Use a near-black with a slight hue, like `#18181b`.

```css
--bg-base:     #18181b; /* the page */
--bg-elevated: #1f1f23; /* +1: cards */
--bg-overlay:  #2a2a2f; /* +2: menus, dialogs */
```

Keep `#000` for photo backdrops, video players and OLED themes that opt in on purpose.

## Up means lighter

In light mode, elevation is a shadow. On a dark page there is nothing dark enough for a shadow to fall on, so **in dark mode, higher surfaces are lighter**, and the topmost layer is the lightest.

A raised card swaps its shadow stack for a faint white ring:

```css
.dark { --elevation-raised: 0 0 0 1px rgb(255 255 255 / .09); }
```

Rings are for elevation only. **Dividers stay solid** dark grays, around `#2a2a2a` on `#18181b`: table rules, list separators, input edges. A see-through white line glows.

## Soften the white

Full white text on near-black is still glaring. White at 90–93% reads softer:

```css
--text-primary:   rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.62);
```

There's a catch. Text with alpha is only safe on **one known surface**. Over a lighter card, an image or a tinted panel, the alpha mixes with whatever is underneath and the contrast changes. Everywhere else, use solid tokens checked against their real background.

## Remap, don't invert

The deeper idea: dark mode is the same color ramp with the roles pointed at mirrored steps. The page used step 50 in light; it uses 950 in dark. Text used 900; it uses 50. Borders swap 200 for 800.

  **Inverted (avoid):**

Every color flipped by a filter or by hand-picking a second palette. Yellows turn muddy, the accent vibrates, and the two themes drift apart.

  **Remapped (do this):**

One scale. Semantic tokens point at different steps in a `.dark` block. Every pair is then re-checked for contrast.

Because OKLCH lightness is measured, mirrored steps stay just as distinct after the flip. Then two adjustments:

- **Desaturate the accent 20–30%.** A brand blue that looks crisp on white buzzes on black.
- **Re-measure every pair.** The swatch looking right isn't the same as the screen reading right.

This is the payoff of the token tiers from the palette lesson: components never change, only the semantic layer does. *(Per-component `dark:` overrides scatter the theme across the codebase and rot on the first palette change.)*

## The details that give it away

- **Icons and logos** with a hard-coded `fill="#000"` vanish in dark. Use `fill="currentColor"` so they follow the text color.
- **Native controls**: set `color-scheme: dark` in the dark theme, or scrollbars and `<select>` menus stay light.
- **Images** get a white 10% inset outline instead of the black one.
- **The phone's status bar**: one `theme-color` per scheme, matched to the top of the page.

## Half done is broken

A dark mode with one white modal, one light dropdown or one black logo on black makes the whole product look broken. The skill treats it as a blocker, not a nit. If the theme isn't finished, don't ship the toggle and don't declare `color-scheme: dark`. That is also why this course has no dark mode: one well-made theme beats two half-made ones.

**Build a dark theme (paste into your agent):**

```text
Add a dark theme by remapping our existing scale, not inverting it:
- Surfaces: near-black (e.g. #18181b), getting lighter as they rise (card, menu, dialog). No pure #000.
- Elevation in dark is a lighter surface plus a ring (0 0 0 1px rgb(255 255 255 / .09)), not shadows. Dividers stay solid grays.
- Text: 92% white primary, 62% secondary, only on one known surface; elsewhere solid tokens.
- Repoint semantic tokens in a .dark block; no per-component dark: overrides.
- Desaturate the accent 20–30%. SVGs use currentColor. Set color-scheme: dark and a dark theme-color.
- Re-check every text pair for contrast and list the results.
```

**Check yourself:** In a dark theme, how should a dropdown menu show it floats above a card?

- [ ] A bigger, darker shadow
- [x] A lighter surface than the card, with a faint white ring
- [ ] Pure black, to stand out
- [ ] A bright accent border

*Why:* Shadows have nothing to fall on in the dark. Higher layers are lighter surfaces, and a low-opacity white ring defines the edge.

## In the skill

- https://school.agents.org.in/skill/surface/dark-mode.md


---

# Glass and layers

> Use translucent surfaces where they help orientation, keep text on them legible, and stack layers with a plan instead of z-index 9999.

Module: Color, depth and surfaces · Level: intermediate · 7 min · https://school.agents.org.in/design/surface/glass-and-layers

Scroll the photos under the toolbar. The bar is see-through, so you never lose track of what's behind it.

*[Interactive demo: Drag the sliders, then stack a second glass layer on top and watch the label drown.]*

That is what a translucent material (A see-through, blurred surface (CSS backdrop-filter) such as a toolbar that content scrolls under. Its job is to keep you oriented, not to decorate) is for. A see-through bar lets content run under the chrome instead of losing a fixed strip of screen to it, and keeps you oriented. Chosen for looks alone, it fails at both.

## Glass by hierarchy

Emil Kowalski distils Apple's guidance into a few rules:

- **Heavier, more opaque glass for structure** (sidebars, tab bars), lighter for the interactive bits you want noticed.
- **Never light glass on light glass.** Two blurs compound and text legibility collapses. Make one of them solid. (That's the second switch above.)
- **Bigger reads thicker.** A full sheet gets more blur and a deeper shadow than a small chip.

```css
.toolbar {
  background: rgb(255 255 255 / 0.6);
  backdrop-filter: blur(20px) saturate(180%);
  border-top: 1px solid rgb(255 255 255 / 0.4); /* a lit top edge */
}
```

## Modal or parallel

A **modal task**, one that blocks everything until you answer, pairs its surface with a scrim (A dark see-through layer laid over the page behind a dialog, which pushes the page back so the dialog has your full attention): a dark see-through layer that pushes the page back. A **parallel** panel that you work beside stays translucent with no scrim, so your flow isn't broken.

When glass appears, let it *materialize*: animate the blur and a slight scale together, so it arrives as a material rather than fading in like a ghost.

## Text on glass

Flat mid-gray text dies over a backdrop that keeps changing. On glass:

- raise the contrast and step the weight up one,
- add a hair of letter-spacing (about 0.01em),
- put brand color on a solid layer, never on the see-through foreground,
- measure contrast over the **worst** thing the bar can sit on.

And respect the people who asked for less of it:

```css
@media (prefers-reduced-transparency: reduce) {
  .toolbar { background: var(--surface); backdrop-filter: none; }
}
@media (prefers-contrast: more) {
  .toolbar { background: var(--surface); border-color: var(--border-strong); }
}
```

> **Glass that renders flat.** `backdrop-filter` only blurs what is painted behind the element inside its own group of layers. A parent with `overflow: hidden`, a `transform` or `isolation` can leave it sampling nothing, so the glass shows as a flat tint. Check in Safari and Firefox.

That warning leads straight into the second half of this lesson: layers.

## Who's on top

When things overlap, CSS draws them in order. `z-index` (The CSS number that decides which overlapping thing is drawn on top. It only competes with siblings inside the same stacking context) changes the order. Here is the same screen with three different strategies; each one is real CSS painting live.

*[Interactive demo: Magic numbers bury the toast. A transform traps the modal under the header.]*

`z-index: 9999` is a confession that nobody knows the order. The skill, drawing on Cheng Lou's notes, gives three tools, in order of preference:

  **Step: No z-index at all.** Later elements in the HTML paint over earlier ones. Arrange the markup so source order matches stacking order and most layers need nothing.

  **Step: One global file for app-wide layers.** Nav, menus, tooltips, modals and toasts compete across the whole app, so give them sequential constants in a single file, in order. Adding a layer means inserting one line in the right place.

  **Step: Local numbers inside an isolated group.** When a component's own parts must overlap, wrap it in `isolation: isolate` and use 1 and 2 inside. Nothing escapes the wrapper, so local numbers can't collide with global ones.

```ts
let d = 1
export const nav = d++, contextMenu = d++, tooltip = d++, modal = d++, toast = d++
```

## The sealed box

The "transformed parent" case is the one that confuses everyone. Some properties turn an element into a stacking context (A sealed group of layers. Things inside it are sorted among themselves and the whole group is drawn as one layer, so a z-index of 9999 inside can't beat something outside): a sealed box of layers. Everything inside is sorted among itself, then the whole box is painted as one layer. A child with `z-index: 9999` beats its siblings and nothing outside.

What creates the box: a `z-index` on a positioned element, `position: fixed` or `sticky`, `opacity` below 1, and the quiet ones, **`transform`, `filter` and `will-change: transform`**.

That's the classic bug. Someone adds a `transform` for an entrance animation, or `will-change` "for performance", and a modal inside can no longer rise above the header, while a tooltip that used to escape its card gets clipped. *(After adding any animation to a container, re-check what's drawn on top.)*

**Fix the layers (paste into your agent):**

```text
Clean up stacking in this codebase:
- List every z-index. Remove the ones DOM order already handles.
- App-wide layers (nav, menu, tooltip, modal, toast) become sequential constants in one file; no magic numbers like 50 or 9999.
- Components that overlap internally get isolation: isolate and local values 1–2.
- Flag any transform, filter or will-change on a container that holds a modal, menu or tooltip.
- Translucent bars: no light glass on light glass; add prefers-reduced-transparency and prefers-contrast fallbacks to a solid surface.
```

**Check yourself:** A modal has z-index: 9999 but still appears under the sticky header. What's the likely cause?

- [ ] 9999 isn't high enough; use 99999
- [x] A parent of the modal has a transform (or similar), making a stacking context it can't escape
- [ ] The header has position: sticky
- [ ] The modal needs opacity: 1

*Why:* Inside a stacking context, z-index only competes with siblings. Move the modal out (a portal) or remove the property that created the context.

## In the skill

- https://school.agents.org.in/skill/surface/translucent-materials.md
- https://school.agents.org.in/skill/surface/z-index-strategy.md


---

# A type scale and its rhythm

> Set every text size from a short scale named by role, give each step the right line-height and tracking, and load fonts so nothing fakes or jumps.

Module: Typography · Level: beginner · 8 min · https://school.agents.org.in/design/typography/type-scale

Pick a ratio below. Every size on the page recalculates from one base number, and each step already knows its line spacing and letter spacing.

*[Interactive demo: One base, one ratio, five roles. Change either and the whole page moves together.]*

That list is a type scale (A short, fixed list of text sizes a product is allowed to use, often each one a set ratio bigger than the last. Every piece of text picks a step instead of a one-off size). It is the single most effective thing you can do for text on a screen, and the thing generated UIs skip most often.

## Why a short list beats free choice

When every component picks its own size (15px here, 17px there, 13.5px in a badge), the page feels cramped in one spot and airy in the next, and nobody can say why. The cause is one-off sizes.

A scale is a short, closed list. A new size needs a reason. Two common ways to build one:

- **A ratio**: each step is the last one times 1.2 or 1.25, starting from a 16px body.
- **A stock scale**, like Tailwind's `text-sm`, `text-base`, `text-lg`.

Both are fine. What matters more is the **names**. On a team, name steps by role, `text-caption`, `text-card-title`, `text-page-title`, because a role polices itself in a way `text-lg` can't. Nobody puts a page title in `text-caption`.

> **In plain words.** A type scale is a menu of five or six text sizes. Every piece of text orders from the menu. It makes the page feel consistent even when nobody can say why.

Sizes go in rem (A CSS unit relative to the root font size (usually 16px). Sizes in rem grow when a reader sets a bigger default text size; px sizes ignore them), not px, so a reader who bumped up their browser's text size gets bigger text. Pixels are for hairlines, not words.

## Leading follows the role

Leading (The vertical space from one line of text to the next. Body text wants about 1.5 times the font size; big headings about 1.1) is the space between lines, set in CSS as `line-height`.

- **Body text: 1.5 to 1.6.** Paragraphs need room for the eye to find the next line.
- **Headings and display: about 1.1.** Big letters carry their own space; 1.5 on a headline looks like double spacing.

And always **unitless**. Push the slider below as if the reader zoomed in.

*[Interactive demo: Fixed 24px leading stacks lines on top of each other as the text grows. Unitless 1.5 grows with it.]*

`line-height: 24px` looks identical to `1.5` at 16px, then breaks on the first size change or zoom. Space between paragraphs is about one line-height, not some arbitrary pixel number.

## Tracking follows the size

Fonts are spaced for body sizes. At headline sizes that spacing looks loose; on tiny labels it looks cramped. So:

| Size | Letter-spacing |
|---|---|
| Tiny uppercase labels | about **+0.06em** |
| Body, 14–18px | **0** |
| Headings, 20–48px | **−0.01 to −0.03em** |
| Display, 64px+ | **−0.04em** or so |

Track in `em`, so it scales with the text. You'll play with this properly in the next lesson.

## Files that load cleanly

Three rules from Emil Kowalski's practice stop the most common font bugs:

1. **Ship woff2 (The compressed font file format every modern browser reads. It is the only format a website needs to ship) and nothing else.** It's compressed and universal. A `.ttf` in `public/fonts` is a bug. Load only the weights you use.
2. **`font-synthesis: none` at the root.** Ask for a bold you never loaded and the browser fakes one by smearing the letters (faux bold (When a page asks for a bold or italic the font didn't load, the browser fakes one by smearing or slanting letters. font-synthesis: none turns that off so the problem is visible)). Turn that off so a missing file looks missing.
3. **Use the high-level property, not the raw code.** `font-weight: 620`, not `font-variation-settings: "wght" 620`; `font-variant-numeric: tabular-nums`, not `"tnum" 1`. Properties still do something sensible when a fallback font loads; raw codes silently do nothing.

```css
:root {
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}
```

Light weights are size-bound: a 300 weight that looks elegant at 32px is unreadable at 11px.

## Never change weight on hover or select

One last trap, and it's everywhere. Click through these tabs in both modes.

*[Interactive demo: Bold-on-select pushes every label after it sideways. Color doesn't move anything.]*

Bold letters are wider, so the selected word grows and shoves its neighbours. **Hold the weight constant and show state with color** (or an underline). *(The same goes for hover: a menu item that turns bold under the pointer makes the whole row twitch.)*

## Pairing, briefly

Two typefaces cover most products; three is the ceiling. Pair across categories, a serif for display over a sans for text, never two similar sans faces, which read as an accident. And because two families at the same `font-size` rarely look the same size (the x-height (How tall a font's lowercase letters are. Two fonts at the same font-size can look very different sizes because of it) decides), re-tune sizes whenever you swap a face.

**Set up type (paste into your agent):**

```text
Set up our typography:
- A closed type scale of 5–6 steps in rem, named by role (text-caption, text-body, text-card-title, text-section, text-page-title). Replace every one-off font-size with a step.
- Unitless line-height: 1.5–1.6 for body, ~1.1 for headings. Paragraph spacing ≈ one line-height.
- Tracking in em: 0 for body, -0.01 to -0.03em for headings, +0.06em for small uppercase labels.
- woff2 only, only the weights we use; font-synthesis: none on :root; high-level properties instead of font-variation-settings.
- No font-weight change on hover or selected states; use color.
```

**Check yourself:** A heading is set with line-height: 32px. What's wrong with it?

- [ ] Nothing, 32px is a good value
- [x] It's fixed: when the font size changes or the reader zooms, the lines overlap or gape. Use a unitless value like 1.1
- [ ] Headings shouldn't have a line-height
- [ ] It should be in rem

*Why:* Unitless line-height scales with the font size. A fixed pixel value breaks on the first size tweak or zoom.

## In the skill

- https://school.agents.org.in/skill/typography/MOC-typography.md
- https://school.agents.org.in/skill/typography/type-scale-and-rhythm.md


---

# Line length and tracking

> Keep paragraphs between 45 and 75 characters a line, tighten big type and open tiny labels, and choose how headings and blurbs wrap.

Module: Typography · Level: beginner · 7 min · https://school.agents.org.in/design/typography/line-length

This paragraph starts too wide on a laptop. Pull the width slider left until the count turns green, and read it again.

*[Interactive demo: The count is how many characters fit on a line at that width. Widen your window to see very long lines.]*

The text didn't change. The reading did.

## 45 to 75 characters

The number of characters per line is called the measure (How many characters fit on one line of a paragraph. 45 to 75 reads comfortably, about 65 is the sweet spot). Robert Bringhurst's rule, which typographers have used for decades: **45 to 75 characters**, with about 65 as the sweet spot for prose.

Why it matters: your eye reads a line, then swings back to find the start of the next. Too long and it loses its place on the way back. Too short and the constant swinging turns reading into work.

In CSS it's one line, thanks to the ch unit (A CSS unit equal to the width of the digit 0 in the current font. max-width: 65ch makes a column roughly 65 characters wide):

```css
.prose { max-width: 65ch; }   /* Tailwind: max-w-prose */
```

*(Re-check when you change font: the same width holds a different number of characters in a different face.)*

On a wide screen this means a narrow column with space either side. That's correct. A 2,400px-wide paragraph is not "using the space", it's unreadable.

## Tracking: tighter as type grows

Tracking (Extra space added evenly between all letters. Big headings want it slightly tighter (negative), tiny uppercase labels slightly looser) is space added evenly between all letters (`letter-spacing`). It runs opposite to size. Drag the size up to a headline, then find the spacing that looks right.

*[Interactive demo: The band is the skill's starting range for each size. Snap applies it.]*

| Size | Tracking |
|---|---|
| 10–12px | **+0.04 to +0.08em** (often uppercase) |
| 14–18px body | **0** |
| 20–28px | **−0.01 to −0.02em** |
| 32–48px | **−0.02 to −0.04em** |
| 64px+ | **−0.04 to −0.06em** |

**Forgetting to tighten display text is one of the most common amateur tells.** A 56px headline at default spacing looks loose and slightly cheap.

Don't confuse tracking with kerning (The font's own spacing adjustments between specific letter pairs, like AV or To. Unlike tracking, it is built into the font and should stay on), the font's own fixes for specific pairs like "AV". Kerning is built in and stays on.

## Uppercase has rules

All-caps text is harder to read because every letter is the same height. Use it only:

- at small sizes (10–12px) with wide tracking, for nav, badges and table headers,
- or for very short phrases, under about four words.

Never for paragraphs. And be careful with the tiny tracked label above every heading (the "eyebrow"): taste-skill's gallery study found it on more than half of weak sites and a sixth of strong ones. Usually its words belong in the heading itself.

## Choose how lines break

Two newer CSS values decide where lines wrap. Try them on a narrow column.

*[Interactive demo: balance evens a heading's lines; pretty saves a blurb's last word.]*

- **`text-wrap: balance`** for headings. It evens out the line lengths so you don't get a long line and a one-word line.
- **`text-wrap: pretty`** for card blurbs, standfirsts and empty-state copy. It stops the stranded last word, the orphan (A single short word left alone on the last line of a paragraph or heading. It looks like a mistake; text-wrap: pretty fixes most of them).
- **Neither on long paragraphs.** Browsers cap balance at a few lines, and a balanced paragraph narrows every line.

These two (CSS that controls where lines break. balance evens out the lines of a heading; pretty stops a paragraph ending on one stranded word) are supported in current Chrome and Safari, so you can ship them without a fallback; older browsers simply wrap normally.

## Left-aligned, almost always

The ragged right edge is the eye's landmark. Justified text without hyphenation opens "rivers" of white space; centered text works for a short headline and fails past two lines.

**Fix reading comfort (paste into your agent):**

```text
Tune the reading experience:
- Cap every reading column at max-width: 65ch (check the count again if the font changes).
- Letter-spacing by size, in em: 0 for body, -0.02em around 32px, -0.04em at 64px+, +0.06em on small uppercase labels.
- text-wrap: balance on h1–h3; text-wrap: pretty on card descriptions and empty states; nothing on article paragraphs.
- Left-align body copy. Uppercase only for short labels at small sizes.
```

**Check yourself:** A blog post's paragraphs run the full 1200px width of a laptop. What's the fix?

- [ ] Increase the font size until it fits
- [x] max-width: 65ch on the text column
- [ ] text-wrap: balance on the paragraphs
- [ ] Justify the text

*Why:* Long lines make the eye lose its place. A 65ch column keeps lines in the 45–75 character range. Balance on paragraphs would quietly narrow them; justify opens rivers.

## In the skill

- https://school.agents.org.in/skill/typography/line-length-tracking.md
- https://school.agents.org.in/skill/typography/line-behavior.md


---

# When text runs out of room

> Decide per field whether text wraps, truncates at the end or the middle, or stays whole; stop long words breaking boxes; and hold the size and contrast floors.

Module: Typography · Level: intermediate · 7 min · https://school.agents.org.in/design/typography/when-text-runs-out

Three files, all named almost the same. Narrow the column with end-truncation on, and try to tell which is which.

*[Interactive demo: End, middle, or wrap. Tap a row to see the full name.]*

With end-truncation they all become `Q3-board-report-fi…`. Identical. Switch to middle-truncation and the part that differs, the end, survives.

## Truncation deletes information

Truncation (Cutting text off with an ellipsis (…) when it doesn't fit. Only allowed when there is a way to see the full text, such as a tooltip or detail view) means cutting text with an ellipsis (…). The skill treats it as deleting information, so it is only allowed **with a route back** to the whole string: a tooltip that also opens on keyboard focus, an expand control, or a detail view. A clipped ID with no way to read it is data loss wearing CSS.

Choose per field, never one rule for the whole app:

| Field | Do this |
|---|---|
| What identifies the thing (a name, a page title) | **Wrap.** Two lines is fine; four means the column is too narrow. |
| Secondary info whose start matters (a role, a message preview) | **End-truncate.** |
| Things that differ at the end (file names, paths, hashes, emails on one domain) | **Middle-truncate.** CSS can't; split the string in code and keep the tail. |
| Card previews | **Clamp at 2 lines** with `line-clamp` (CSS that cuts text after a set number of lines, adding an ellipsis. Used to keep card previews the same height) so cards stay one height. |
| Prices, counts, dates, IDs | **Never cut.** Widen the column or truncate the label beside them. |

The CSS for the two common cases:

```css
.one-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.preview  { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
```

## Guard against overflow both ways

Two opposite failures, each with one line of CSS.

**A long word punches out of its box.** A URL, a hash or a user's 40-letter username has nowhere to break, so it pushes past the card edge. Add `overflow-wrap: anywhere` (CSS that lets a very long word, like a URL or a code, break onto the next line instead of punching out of its box) wherever user text, links or codes can appear.

*(Not `break-word`: it looks the same but leaves the element's minimum width alone, so a flex item still overflows.)*

**A short phrase breaks where it shouldn't.** A badge that wraps to "In / review", or a button label split over two lines, reads as a bug. Those get `white-space: nowrap`.

  **Before (avoid):**

A comment card with `https://example.com/projects/acme/issues/4821?view=board` running straight off its right edge.

  **After (do this):**

The same card with `overflow-wrap: anywhere` on its body: the URL breaks onto a second line inside the card.

## Casing and punctuation are presentation

- **Store copy in sentence case.** If a label should look uppercase, use `text-transform: uppercase`. ALL-CAPS in the source gets rewritten at the next redesign and some screen readers spell it out.
- **Type real punctuation.** Curly quotes in prose (“like this”), an en dash for ranges (Mon–Fri), an em dash for asides, the single ellipsis character …, and a non-breaking space between a number and its unit (`12&nbsp;MB`) so they never split. Code keeps straight quotes.

## Underlines that look designed

Default underlines often slice through descenders and sit at a random thickness. Take the metrics from the font:

```css
a {
  text-decoration-thickness: from-font;
  text-underline-position: from-font;
  text-decoration-skip-ink: auto;
}
```

A dotted underline means "there's more here", which is why the glossary terms in this course have one. Never underline something that isn't a link. And if you want an animated underline, only its *color* animates reliably; a growing line is a separate element.

## The floors

A few numbers you never go under:

- **Inputs at 16px or more on phones.** Below that, iPhone Safari zooms the whole page when you tap the field. Don't "fix" it with `maximum-scale=1`; that removes zoom for everyone, an accessibility failure.
- **Body 16px, dense controls 14px, captions 13px.** Under 12px needs an unusual excuse.
- **Contrast 4.5:1 for body, 3:1 from about 24px**, as in the surface module.
- **Direction-neutral CSS**: `text-align: start` and `padding-inline-start` instead of left, and a `lang` on the page, so the layout mirrors for right-to-left languages.

**A typography fix should be the smallest diff.** It never restructures markup unless an expandable truncation or an animated underline needs an element.

**Handle long and short text (paste into your agent):**

```text
Review how text fits in these components:
- Per field, choose: wrap (names, titles), end-truncate (secondary metadata), middle-truncate (file names, paths, hashes) or line-clamp: 2 (card previews). Never cut prices, counts, dates or IDs.
- Every truncation needs a way to the full string (tooltip that also opens on focus, expand, or detail view).
- overflow-wrap: anywhere wherever URLs or user text appear; white-space: nowrap on badges and button labels.
- Inputs at least 16px on mobile; never maximum-scale=1.
- Sentence case in source, text-transform for caps; real quotes, dashes and … in copy.
```

**Check yourself:** A list shows file paths that share a long common prefix. How should they be shortened?

- [ ] End-truncate with an ellipsis
- [x] Middle-truncate, keeping the end where they differ, with a way to see the full path
- [ ] Shrink the font until they fit
- [ ] Hide the paths

*Why:* End-truncation makes them identical. Middle-truncation keeps the distinguishing tail, and the full path stays reachable.

## In the skill

- https://school.agents.org.in/skill/typography/line-behavior.md


---

# Numbers, features and faces

> Line numbers up with tabular figures, reach the extras inside a font file, and choose a typeface that doesn't read as the default.

Module: Typography · Level: intermediate · 7 min · https://school.agents.org.in/design/typography/numbers-and-faces

Watch the right-hand column of this price table for a few seconds. Then flip the switch.

*[Interactive demo: Proportional digits jiggle as they tick. Tabular digits hold still.]*

Most fonts draw a "1" narrower than an "8", which looks lovely in a sentence and terrible in a column. Every update shifts the number sideways and nothing lines up.

## Tabular figures for anything that changes

Tabular figures (Digits that all take the same width, so numbers line up in columns and a ticking counter doesn't wiggle. Turned on with font-variant-numeric: tabular-nums) give every digit the same width. Columns align, and a ticking timer only moves the digits that actually changed.

```css
.price, .timer, td.number { font-variant-numeric: tabular-nums; }
```

Use them on timers, prices, counters, scores, table columns of figures, anything live. Leave running prose on the default figures, which read more naturally in a sentence. Numbers people compare also right-align, so the decimal points stack.

## What's hidden in a font file

A good font carries far more than one set of letters. These extras are called OpenType features (Optional extras hidden inside a font file: tabular digits, a slashed zero, real fractions, small caps, alternate letters. CSS switches them on), and CSS switches them on. Try a few on Inter, the face this demo uses:

*[Interactive demo: A slashed zero for codes, real fractions, and a weight axis that takes any number.]*

The useful ones:

- **`slashed-zero`** wherever 0 and O can be confused: order codes, serial numbers, coupons.
- **`diagonal-fractions`** for ½-style fractions in recipes and specs.
- **Real small caps (Capital letters drawn at about lowercase height, designed to match the weight of the text. Shrinking normal capitals instead makes them look thin)** with `font-variant-caps: small-caps`, never shrunken capitals, which come out too thin.
- **Real superscripts** (m², CO₂) with `font-variant-position: super | sub`, instead of the default `<sup>`, which is too big and pushes lines apart.
- **Stylistic sets** (`ss01` to `ss20`) are numbered slots whose meaning differs in every font. Read the font's specimen, then comment what the slot does: `font-feature-settings: "ss02" 1; /* single-storey a */`.

> **Features fail silently.** If the font doesn't contain the feature, nothing happens and nothing warns you. Check that the glyphs exist, and prefer the named CSS property over raw four-letter codes, because properties fall back sensibly.

## Variable fonts and their axes

A variable font (One font file that holds a whole range of a style, usually weight, so any value like font-weight: 473 is real instead of only 400 and 700) is one file holding a whole range of a style. Weight is the common one, which is why the slider above accepts 430 or 610, not just 400 and 700. Each range is an **axis**:

| Axis | Varies | Set it with |
|---|---|---|
| `wght` | weight | `font-weight` |
| `wdth` | width | `font-stretch` |
| `opsz` | optical size (Versions of a typeface tuned for a size: sturdier and looser for small text, finer and tighter for big headlines. Some fonts adjust it automatically) | `font-optical-sizing: auto` |
| `slnt` / `ital` | slant, italic | `font-style` |

A font exposes only the axes it was built with. One or two weights usually ship lighter as separate static files; many weights or an optical-size axis favour the variable file. Measure rather than assume.

Some families ship separate cuts for size, like Text and Display versions of one design: sturdier and looser for paragraphs, finer and tighter for headlines. Pick the cut for the size you set.

## Choosing a face that isn't the default

Now the human part. Flip between these three versions of the same hero.

*[Interactive demo: The default, a pair across categories, and one family doing everything.]*

Inter and SF Pro are excellent, neutral and on nearly every generated page. That ubiquity is the problem: on a landing page, the stock face at stock weights signals "default" before anyone reads a word.

The skill's advice:

- **On marketing pages, pick something with character.** Foundries worth knowing include Pangram Pangram, Displaay, Grilli Type and Vercel's Geist. Pair across categories when you use two (a serif display over a sans text face), never two lookalikes.
- **Inside the product, Inter or SF Pro are fine.** They were built for screens and stay legible at 12px. But then you have used your one default: don't also ship the default colors and the default icons.
- **The single-weight rule.** For a first version, use **one weight** of one face for everything and get hierarchy from size and space. It's cheaper, faster and usually cleaner. Add weights when a real need shows up.

**Test the face at every size you actually use.** Geometric faces that look great at 16px can turn to mush at 11px, which is when Inter's boring legibility wins. *(A typeface is a tell only on surfaces where people judge the brand. In a dense settings screen, legibility is the brand.)*

**Numbers and fonts (paste into your agent):**

```text
Typography details pass:
- font-variant-numeric: tabular-nums on every price, timer, counter and number column; right-align compared figures.
- Use named CSS properties for font features (font-variant-numeric, font-variant-caps, font-variant-position, font-weight); font-feature-settings only for stylistic sets, with a comment saying what the slot does in this font.
- slashed-zero on order codes and serials.
- Marketing pages: propose two typefaces that aren't Inter/SF Pro, paired across categories, and start with a single weight. Keep Inter for dense product UI.
```

**Check yourself:** A dashboard counter updates every second and the number shivers sideways. What's the one-line fix?

- [ ] letter-spacing: -0.01em
- [x] font-variant-numeric: tabular-nums
- [ ] font-weight: 600
- [ ] text-align: center

*Why:* Tabular figures give every digit the same width, so only the digits that change move and nothing shifts.

## In the skill

- https://school.agents.org.in/skill/typography/opentype-features-and-axes.md
- https://school.agents.org.in/skill/typography/typography-humanity.md


---

# Grouping by space

> Show what belongs together with space before lines or boxes, keep controls looking like controls, and line everything up on a few shared edges.

Module: Layout and pages · Level: beginner · 7 min · https://school.agents.org.in/design/layout/grouping-by-space

This form has two groups, and right now you can't tell. Every gap is 16px. Drag the second slider to 32px.

*[Interactive demo: Same fields, same boxes. Only the gaps changed.]*

Suddenly there are two sections, and you never added a line, a card or a heading color. That's grouping by space (Showing which things belong together just by putting them closer to each other than to anything else. The gap between groups is at least twice the gap inside one), and it's the cheapest structure in design.

## Space, then shape, then line

Our eyes read things that sit close together as one group. Designers call it proximity (The tendency to read things that sit close together as one group. It is the cheapest structure in design: no lines or boxes needed). A divider, a card and a tinted background all say "these belong together"; space says it without adding a single pixel of ink.

The skill's order, from Jakub Krehel's layout rules:

  **Step: Space.** The gap **between** groups is at least **2×** the gap **inside** one. 8px inside means 16px or more between.

  **Step: A background shape.** Only when space alone is ambiguous: a sunken well, a raised card.

  **Step: A line.** Last, where neither works: a dense table, a long settings list. A divider that space could carry is a deletion.

Turn on the divider switch in the demo with the ratio already at 2×. It adds nothing; the space had already done the job.

> **More padding is not grouping.** "Add more whitespace" makes every gap bigger and equal. Equal gaps say nothing belongs together. **The ratio is the point, not the size.**

## Controls look like controls

Clean screens sometimes strip buttons down until they look like plain text. Every interactive thing needs something that says "press me": a background shape, a border, an underline, or a consistent home such as a toolbar.

  **Before (avoid):**

"Edit · Share · Delete" in the same gray text as the paragraph above them. They read as a sentence.

  **After (do this):**

Three small buttons with a quiet background shape, in a toolbar row. They read as things you can press.

## A few shared edges

Pick a small set of alignment edges (An invisible vertical line that several elements start from. Fewer edges make a page look calm; text starting on many different lines looks ragged) and put everything on them. Text that starts on six different vertical lines looks ragged even when every element is fine on its own.

Use one spacing step per level of nesting (16px is a useful default), and take your steps from a spacing scale (A short list of allowed gaps (for example 4, 8, 12, 16, 24, 32px) that every margin and padding picks from, so spacing stays consistent) if the project has one. If it does, use its steps; the numbers in this lesson are starting points for projects that don't.

## Clearance between controls

How far apart should two buttons be? It depends on whether you can see their edges.

| Between | Starting gap |
|---|---|
| Bordered or filled controls (buttons, inputs) | **12px** |
| The visible glyphs of borderless text or icon buttons | **24px** |
| Unrelated groups of controls | **24px** or more |

With borderless buttons nothing marks where one ends, so the space *is* the boundary. Tap areas can be bigger than the visible glyph, but they never overlap.

## Edges, bleeds and the one primary action

- **Inset buttons from the screen edge.** A full-width button on a phone sits about 16px in from each side, with the bottom safe area added below it. Glued to three edges, it looks like system chrome and clips on curved corners.
- **Content bleeds, controls float.** Backgrounds and images can run to the edge of the screen (full bleed (A background or image that runs right to the edge of the screen, past the margins. Content can bleed; text and controls stay inside the margins)); text and buttons stay inside the margins.
- **One primary action (The one thing you most want people to do on a screen, usually the one filled, strongest button. A screen should have one) per view.** Past three secondary actions, the rest go behind a menu.

A full-bleed image inside an article, in one line of grid:

```css
.article { display: grid; grid-template-columns: 1fr min(65ch, calc(100% - 48px)) 1fr; }
.article > * { grid-column: 2; }
.article > .full-bleed { grid-column: 1 / -1; }
```

*(Before reaching for a card or a border, try doubling the gap. It's usually enough.)*

**Group by space (paste into your agent):**

```text
Rework the spacing on this screen:
- Group with space first: gaps between groups at least 2× the gaps inside a group (e.g. 8px inside, 16–24px between). Use our spacing scale's steps.
- Remove dividers and cards that space already carries; keep lines only for dense tables and long lists.
- Every control must look interactive: a background shape, border, underline or a toolbar zone.
- 12px between bordered controls, 24px between borderless text/icon buttons.
- Put text on a small number of shared left edges. One primary action per view.
```

**Check yourself:** Fields inside each form section are 12px apart, and sections are 12px apart too. What's the smallest fix that makes the sections readable?

- [ ] Add a divider line between sections
- [x] Raise the gap between sections to 24px or more
- [ ] Put each section in a card
- [ ] Make the section headings bigger

*Why:* Space groups first. With between-group gaps at least twice the inside gaps, the sections separate without any extra ink.

## In the skill

- https://school.agents.org.in/skill/layout/MOC-layout.md
- https://school.agents.org.in/skill/layout/grouping-by-space.md


---

# The page frame, on every screen

> Get the frame readers judge first right (equal margins, no holes, a first screen that says what this is) and design each screen size on purpose.

Module: Layout and pages · Level: beginner · 8 min · https://school.agents.org.in/design/layout/page-frame

This little landing page has four faults that readers notice before they read a single word. The pink marks show where. Fix them one switch at a time.

*[Interactive demo: Lopsided margins, a half-empty section, a hole, and a headline that says nothing.]*

Before anyone notices a typeface or a shadow, they judge the frame (The overall shape of a page that readers judge before any detail: its margins, how the width is used, the gaps between sections, and whether the first screen says what this is). Their complaints sound vague ("everything is shifted to one side", "big random blank spaces", "I can't tell what this is"), but each one is measurable.

## Five measurable rules

taste-skill measured 97 award-winning sites in October 2026, and the strong pages agree on these:

| Rule | Fails when |
|---|---|
| **One pair of equal margins** | The left and right distances differ, or the header sits on one inset and the page on another. |
| **Text never touches the edge** | Under 16px on desktop, under 12px on a phone. |
| **Use the width you set up** | A text section sits in one half with nothing in the other. Give the empty half the section's picture, a second column or a list. |
| **No holes** | Over about half a screen holds nothing, or the gap between two sections passes about 40% of a screen. |
| **Say what this is, first** | The first screen's headline is missing, or only a slogan. 15 of the 97 sites never said what the product was, and lost marks for it. |

And: give every section of 30+ words a heading, and start most text on the same few left edges.

Spacing that holds the frame: sections about **120–240px apart on desktop** and **72–112px on a phone**, the gap between sections clearly larger than any gap inside one, and at least twice as much space above a heading as below it.

## A rest is not a hole

Empty space is fine when something frames it: a picture with the type docked to its edge, a headline big enough to span it. When nothing closes it off, it's a hole (A stretch of a page with nothing in it and nothing closing it off, like a giant gap between sections. Different from a deliberate rest, which something frames). Holes usually come from stretched section gaps, text in one half of the width, or content waiting on a scroll animation to appear.

**When a section looks empty, make its main element bigger or cut it.** Adding a decoration is almost never the fix. *(Scroll-triggered reveals cause holes: until they fire, that section is bare ground.)*

## Each screen size gets its own design

Responsive (Making a layout work on every screen size. Done well, each size gets its own composition, not a squeezed copy of the desktop one) doesn't mean "squeeze the desktop layout until it fits". It means each size gets a composition designed for it. Drag the frame's handle below; the layout answers to the frame, not your window.

*[Interactive demo: Wide: sidebar and a three-card grid. Narrow: a bottom tab bar and a row of cards that peeks so you know it scrolls.]*

Typical swaps from the skill:

| Desktop | Phone |
|---|---|
| Sidebar + content | Bottom tab bar + full-width content |
| 3-column card grid | A swipeable row, or a stack in a different order |
| Wide hero, image right | Stacked hero, image full width below |
| Dense table | One card per row with the key fields first |

What stays the same: the **components**. A button looks the same on every screen; the screen it's on changes.

That demo uses a container query (CSS that lets a component change its layout based on the width of the box it sits in, rather than the whole window. A card can then work in a sidebar and in a wide grid), so the component adapts to the box it sits in. Its breakpoints (The width at which a layout changes, for example from one column to two. Good breakpoints come from where the content stops fitting, not from a device list) are where the content stops fitting, not a device list.

## Narrow columns are valid

A common mistake is making content full-width because the screen allows it. A 2,400px paragraph is unreadable, so reading content keeps a narrow column (about 65ch) even on a huge monitor, as in the demo.

For everything else, a 12-column grid (A page divided into 12 equal columns. 12 splits into halves, thirds, quarters and sixths, so it fits most layouts) is the flexible base: 12 divides into halves, thirds, quarters and sixths. On marketing pages you can then break the grid on purpose (a pull quote that hangs into the margin, a bento of different-sized tiles). Inside the product, hold the grid; forms stay single-column, dashboards stay symmetric.

**Check the page frame (paste into your agent):**

```text
Review this landing page at 1440, 768 and 390 wide:
- One pair of equal left/right margins at each width; header and body share the same inset; no text closer than 16px (12px on phones) to the edge.
- No section with text in one half and nothing in the other; no empty run over half a screen; section gaps 120–240px desktop, 72–112px mobile.
- The first screen's h1 says what the product is, readable on first paint. Every section of 30+ words has a heading.
- Design the 390 layout on its own (tab bar, peeking card row, stacked hero) rather than shrinking desktop. Reading columns max 65ch.
```

**Check yourself:** A feature section has two lines of text on the left half and nothing on the right. What does the skill suggest?

- [ ] Add a decorative blob on the right
- [x] Give the empty half the section's picture, a second column or a list, or make the main element bigger
- [ ] Center the text
- [ ] Increase the section's padding

*Why:* Use the width you set up. An empty half reads as a hole; fill it with real content or enlarge the main element. Decoration is almost never the fix.

## In the skill

- https://school.agents.org.in/skill/layout/page-frame.md
- https://school.agents.org.in/skill/layout/viewport-custom-design.md


---

# Layouts that survive real content

> Stress-test a layout with long words, translations and big text, fix the classic overflow traps, and write CSS that mirrors itself for right-to-left languages.

Module: Layout and pages · Level: intermediate · 8 min · https://school.agents.org.in/design/layout/growth-and-overflow

This card looks finished. Now feed it real-world content: switch on German, a long URL, and 200% text. The CSS is live, so it breaks for real.

*[Interactive demo: Fits the mockup, fails the world. Then apply the fixes.]*

A layout that fits the copy in the mockup has been tested exactly once. Translations lengthen text, readers zoom, names are long and APIs return surprises. **Design for the worst content, not the sample.** That's a stress scenario (One realistic but hostile input for a component: the longest real name, zero items, a 320px box. You render them side by side to see what breaks before users do).

## Grow, don't clip

- **Size text containers with `max-width` and `min-height`**, never a fixed `width` or `height`. A `width: 96px; overflow: hidden` button is fine in English and cut in German.
- **Let rows wrap**: `flex-wrap: wrap; gap: 12px`.
- **If a box must cap its height**, pair `max-height` with `overflow-y: auto`, so the content scrolls instead of vanishing.
- **Short strings are the riskiest.** A one-word button can double in length in translation.
- **Never park a critical action where clipping can reach it**: the bottom of a modal taller than the screen, or anything the on-screen keyboard covers.

Test with pseudo-localization (Replacing interface text with longer, accented fake text (like [Ŝéţţîñĝš ~~~]) to see what breaks when real translations arrive): fake text like `[Ŝéţţîñĝš ~~~~]` that runs 30–40% longer, plus one long real language such as German.

## The overflow traps

Most "why is there a sideways scrollbar" bugs are one of these:

| You see | Fix |
|---|---|
| A grid column or flex child with long text pushes off-screen | `minmax(0, 1fr)` on the track, `min-width: 0` on the child |
| `width: 100vw` scrolls sideways on desktop | `width: 100%` (100vw includes the scrollbar) |
| `height: 100vh` panel cut off on a phone | `100dvh` (next lesson) |
| A modal taller than the screen | `max-height: 100dvh`, a scrolling body, the buttons outside the scroll area |

The first one has a name: min-content blowout (When a grid column or flex item refuses to shrink smaller than its longest word or widest table, pushing the layout off the side of the screen. minmax(0, 1fr) and min-width: 0 fix it). Grid tracks and flex items refuse to shrink below their longest unbreakable thing, so one URL can push a whole layout off the side.

```css
.layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); }
.row > .text { min-width: 0; overflow-wrap: anywhere; }
```

> **overflow: hidden is not a fix.** Clipping the parent hides the sideways scrollbar and the content with it. Find the child that refused to shrink.

## Breakpoints and containers

Break where the layout stops fitting, not at 768px because a preset says so. Write queries in `rem`, so a reader with a larger default font gets the narrow layout sooner.

A reusable component should adapt to its container, not the window: `container-type: inline-size` on the parent, `@container (width < 400px)` on the child. Two traps: a container query can only style the container's children, never the container itself; and a container with no definite width (a shrink-to-fit flex item) collapses to zero.

## Hint at what scrolls

A row of cards that ends exactly at the screen edge looks complete, and nobody scrolls it. Let the next card **peek 16–32px**. A "Show more" button names what it hides: "Show 12 more results". Content scrolling under a sticky header uses `scroll-padding-block-start` so focused fields and anchor targets aren't hidden under it.

## Start and end, not left and right

Arabic and Hebrew read right to left. A layout written in `left` and `right` has to be rewritten for them; one written in *start* and *end* mirrors itself. Switch the direction below.

*[Interactive demo: Physical CSS stays put. Logical CSS mirrors, arrow included.]*

Logical properties (CSS written as start and end instead of left and right (margin-inline-start, text-align: start), so the layout mirrors by itself for right-to-left languages) are the start/end versions of every physical one:

| Physical | Logical |
|---|---|
| `margin-left`, `padding-right` | `margin-inline-start`, `padding-inline-end` |
| `left`, `right` (positioned) | `inset-inline-start`, `inset-inline-end` |
| `text-align: left` | `text-align: start` |
| Tailwind `ml-4`, `pl-4` | `ms-4`, `ps-4` |

Rules for RTL (Languages like Arabic and Hebrew that read from right to left. The whole layout should mirror for them, except things like logos, clocks and play buttons) from Jakub Krehel's work:

- **Keep visual order equal to DOM order.** Moving things with `order` or `row-reverse` makes keyboard and screen-reader order disagree with what's on screen.
- **Mirror only icons that point along the reading direction**: back and forward arrows, reply, progress chevrons. Never a play button, a clock, a checkmark or a logo.
- **Physical stays physical where the glass is physical.** Safe-area insets are about the phone's real left and right edges, so they need an RTL override.
- **Wrap mixed-direction values in `<bdi>` (An HTML tag that isolates a piece of text whose direction may differ from its sentence, such as an English name inside Arabic, so punctuation lands on the right side)**, like the English company name inside the Arabic sentence above, so punctuation lands on the correct side.

*(Flipping `dir` in devtools checks the layout, not the content. Test with real RTL text and an English name inside it.)*

**Stress-test and fix (paste into your agent):**

```text
Stress-test this component and fix what breaks:
- Try pseudo-localized strings (+40%), German labels, a 120-character URL, a 40-character username and 200% text.
- Text containers use max-width/min-height, never fixed width/height; rows flex-wrap with gap.
- Grid tracks minmax(0, 1fr); flex children with text min-width: 0; overflow-wrap: anywhere on user text.
- width: 100% instead of 100vw; no overflow: hidden used to hide a scrollbar.
- Convert left/right to logical properties (margin-inline-start, text-align: start, ms-/ps- in Tailwind). Mirror only direction-bearing icons; wrap mixed-direction values in <bdi>.
```

**Check yourself:** A two-column grid uses grid-template-columns: 240px 1fr, and a long URL in the main column makes the page scroll sideways. What's the fix?

- [ ] overflow: hidden on the body
- [x] grid-template-columns: 240px minmax(0, 1fr), plus overflow-wrap: anywhere on the text
- [ ] width: 100vw on the grid
- [ ] Shrink the font

*Why:* A 1fr track won't shrink below its min-content width. minmax(0, 1fr) lets it, and overflow-wrap lets the URL break inside it.

## In the skill

- https://school.agents.org.in/skill/layout/growth-and-overflow.md
- https://school.agents.org.in/skill/layout/logical-properties-rtl.md


---

# The phone's platform layer

> Make a web app feel installed on a phone with the right viewport tag, dvh and svh, safe areas, keyboard handling and overscroll, and know why each one exists.

Module: Layout and pages · Level: intermediate · 7 min · https://school.agents.org.in/design/layout/mobile-layer

Here's a web app on a phone, built edge to edge. Watch the title and the tab bar.

*[Interactive demo: Without insets the title hides under the Dynamic Island and the tabs sit on the home indicator. One line of CSS pads them back.]*

Most "it feels like a website on my phone" complaints aren't design problems. They're a handful of platform defaults (the address bar, the notch, the keyboard, the rubber-band bounce), each fixed by one declaration. Emil Kowalski's rule: reach for a media query or meta tag before JavaScript, and know the reason for every line.

## The baseline, before any component

```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="#ffffff" />
```

```css
html { -webkit-tap-highlight-color: transparent; -webkit-text-size-adjust: 100%; }
```

In Next.js these live in the root layout's `viewport` export. This course's own layout sets exactly these.

> **Never block zoom.** `user-scalable=no` and `maximum-scale=1` take zoom away from everyone who needs it. If an input zooms the page on tap, the input is under 16px: fix the input.

## Safe areas

`viewport-fit=cover` in that viewport tag (The <meta name="viewport"> line that tells a phone how to size the page. viewport-fit=cover lets the page paint under the notch so safe-area insets work) lets your page paint under the notch and the home indicator. Then `env(safe-area-inset-*)` tells CSS how big each covered edge is, so you can pad content back into the safe area (The part of a phone screen not covered by the notch or Dynamic Island, the rounded corners or the home indicator. env(safe-area-inset-*) tells CSS how big those edges are).

```css
.header  { padding-top: env(safe-area-inset-top); }
.tabbar  { padding-bottom: env(safe-area-inset-bottom); }
.fab     { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
```

Headers take the top, tab bars and sheets take the bottom, sidebars take left and right in landscape. **Without `viewport-fit=cover`, every inset is 0px**, which is why "I added safe-area padding and nothing happened" is so common.

## 100vh lies on phones

`100vh` is the screen height with the address bar *hidden*. On load the bar is showing, so a 100vh panel runs past the bottom, and its button is out of sight. Toggle the bar and the units below.

*[Interactive demo: vh assumes the bar is gone; dvh follows it; svh is the small, steady height.]*

The dynamic units (Viewport height units that understand phone browser bars. 100dvh changes as the address bar shows and hides; 100svh is the smaller, steady height. Plain 100vh assumes the bar is hidden):

- **`100dvh`** follows the bar as it shows and hides. Use it for app shells and anything pinned to the bottom.
- **`min-height: 100svh`** is the small, steady height. Use it for a hero, because a `dvh` hero re-lays-out mid-scroll and jumps.

Tailwind's `h-screen` carries the old bug; use `h-dvh` and `min-h-svh`. *(dvh everywhere is the overcorrection. Shells track the browser bar; first screens don't.)*

## The keyboard

iPhone shrinks the visible area when the keyboard opens; Android Chrome lays the keyboard over the page. `interactive-widget=resizes-content` makes Android resize too, so a chat composer at the bottom of a `100dvh` column rides up above the keyboard with no script.

Two more rules: a fixed bottom button hides the last form field, so pad the scroll area by the button's height plus the bottom inset; and never autofocus an input on a touch device, which throws the keyboard up uninvited.

## Scrolling and bounce

- **`overscroll-behavior` (CSS that controls what happens when you scroll past the end: the bounce, pull-to-refresh, or the page behind scrolling instead. contain keeps it inside a panel): `none` on the root** stops pull-to-refresh and the page bounce inside an app. Leave it off on a document where pull-to-refresh is welcome.
- **`contain` on inner scrollers** keeps their own bounce without dragging the page along.
- **Scroll lock behind a sheet** goes through a dialog primitive (Base UI, Radix, vaul). `overflow: hidden` on `body` is ignored by iPhone touch scrolling.
- **Never `touchmove` + `preventDefault()`**: it blocks scrolling and makes the listener slow.
- **Anchor links** clear a sticky header with `scroll-margin-top` equal to its height.

## The status bar

One `theme-color` per color scheme, matched to the top of your page (the header), not your brand color. If the theme can be toggled, update the tag too. An installed web app's manifest colors are the same decision.

## Prove it on a real phone

Emulators reproduce none of this. Serve your dev server on your network, open it on a phone by its LAN address, and debug through Safari's Develop menu (iPhone) or `chrome://inspect` (Android). Use a phone a few years old, open the keyboard, turn it to landscape once, and install it as a web app if you ship one.

**Phone platform pass (paste into your agent):**

```text
Make this web app feel native on phones:
- Viewport: width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content. No user-scalable=no or maximum-scale.
- Safe areas: header padding-top env(safe-area-inset-top), tab bar/sheets padding-bottom env(safe-area-inset-bottom, 0px), sidebars left/right in landscape.
- Heights: app shell 100dvh, hero min-height 100svh, never 100vh (h-dvh / min-h-svh in Tailwind).
- overscroll-behavior: none on html for the app, contain on inner scrollers; scroll lock through our dialog primitive.
- One theme-color per scheme matched to the header. Inputs 16px+. No autofocus on touch.
Tell me what to check on a real device.
```

**Check yourself:** You added padding-bottom: env(safe-area-inset-bottom) to the tab bar, but it still sits on the home indicator. What's missing?

- [ ] A bigger padding value
- [x] viewport-fit=cover in the viewport meta tag
- [ ] position: fixed
- [ ] A media query for iPhone

*Why:* Without viewport-fit=cover the page doesn't extend under the system areas, so every safe-area inset resolves to 0px.

## In the skill

- https://school.agents.org.in/skill/layout/mobile-platform-layer.md


---

# The URL, structure and scroll position

> Keep what the user sees in the URL so links and the back button work, arrange screens the way people think, and stop lists jumping when content loads.

Module: Layout and pages · Level: intermediate · 8 min · https://school.agents.org.in/design/layout/url-and-structure

Pick a tab, filter to "Assigned to me", change the sort. Then press "Open link in a new tab". Try it in both modes.

*[Interactive demo: State in memory resets when shared. State in the URL is the view.]*

When the view lived in component state, the new tab forgot everything. When it lived in the URL, the link *was* the view: same tab, same filter, same sort. And Back stepped through your changes.

## The URL is the source of truth

The rule from Vercel's interface guidelines: **anything a user can see right now should be rebuildable from the URL.** Paste it in a new tab and you land on the same view. That's URL as state (Keeping what the user is looking at (tab, filters, sort, open panel) in the web address, so a link, a bookmark or the back button brings back exactly the same view).

| State | Where | Example |
|---|---|---|
| The page | path | `/projects/acme/issues` |
| Tab | query parameter (The part of a URL after the ?, like ?tab=closed&sort=created. It is where page state such as filters lives) | `?tab=closed` |
| Filters | query | `?status=open,blocked&assignee=me` |
| Sort | query | `?sort=-updated_at` |
| Pagination | query | `?cursor=eyJpZCI6...` |
| Open panel or modal | query | `?modal=invite` |
| Search | query | `?q=auth+bug` |

What does **not** go in the URL: form drafts (local storage), login state (cookies), whether a toast is showing, hover and focus.

> **In plain words.** If you could send someone a screenshot of what you're looking at, the link should take them to the same thing. A link you can share, bookmark and go back to is a feature you get almost for free.

Three rules make it work:

- **Deep-link every modal.** `?modal=invite-team` should open the invite dialog straight from the link. That's a deep link (A link that opens a specific view inside an app, such as one open modal or one filtered list, instead of the home page).
- **One source.** Read state from the URL (`useSearchParams` in Next.js) and write through the router. Copying URL state into `useState` creates two versions that drift.
- **Back restores scroll.** Normal links and most routers do this; a custom scroll lock or an intercepted click often breaks it. Check yours.

## Structure the way people think

Information architecture (How a product's screens and features are grouped, named and connected, so people can find things. It is decided before any pixel is styled) decides what people can find before any pixel is styled. The rule under all of it: arrange and name things the way users think about them, not the way the code or the org chart is split.

- **Long settings get sections.** Past about a dozen options, split them under a side nav with named groups (Account, Billing, Notifications), each a short page.
- **Progressive disclosure (Showing what most people need first and revealing advanced options only when asked or when they become relevant)**: show what most people need; put rare options behind "Advanced", or reveal a feature when it becomes relevant. Benji Taylor describes how Family kept the basics at hand and revealed the rest by context.
- **Name by the user's words.** "Send messages in the background", not "Enable async relay mode".
- **Tabs are views of one thing** (this week / month / year), not unrelated destinations.
- **Two levels of menu, then stop.** Deeper than that, use pages with breadcrumbs and a URL.
- **Search logs are rename requests.** If people keep searching "invoices" and the nav says "Billing", rename it.

  **Dead end (avoid):**

"Something went wrong." and a Back button.

  **A way forward (do this):**

"That link expired. Request a new one." with the button that does it.

**Renaming beats restructuring.** Before redrawing the sitemap, check whether the labels are the problem; a label is a one-line fix.

## Keep the reader's place

One more way a page loses people: you're reading a chat log, older messages load above, and the message you were on jumps away. Try it with anchoring off, then on.

*[Interactive demo: Load older messages with anchoring off, then on. The highlighted message is where you were reading.]*

Scroll anchoring (Keeping the row you are reading in the same spot on screen when content above it loads or changes size, instead of letting the page jump) means tying the view to a row, not to a pixel offset. Cheng Lou's pattern:

  **Step: Pick an anchor when the user scrolls.** The first row whose bottom is below the top of the view. Record how far it is from the top of the view.

  **Step: When content changes, restore that distance.** After the new rows render, set the scroll position so the anchor row sits exactly where it was.

  **Step: Detect user scrolling by value.** Compare the scroll position with the one you stored. Don't trust "a scroll event fired": your own scroll fires one too.

  **Step: Store what the browser actually set.** Read the scroll position back after setting it, because browsers round. Storing the value you asked for makes the next frame think the user scrolled.

If you anchor in code, set `overflow-anchor: none` on that scroller so the browser's built-in anchoring doesn't move it a second time. For a simple page, the browser's own `overflow-anchor` is the cheaper option. *(A chat that was showing the latest message should keep showing it after a new one arrives.)*

**URL state and stable lists (paste into your agent):**

```text
Refactor this list page:
- Tab, filters, sort, search, pagination cursor and any open panel or modal live in URL query params. Read with useSearchParams, write through the router; no mirrored useState.
- Every modal is deep-linkable (?modal=...). Back must restore the previous view and its scroll position.
- When older items load above the view, keep the row in view anchored: record the first visible row's offset before the update and restore it after; set overflow-anchor: none on the scroller.
- Check the labels against what users search for and suggest renames before restructuring the nav.
```

**Check yourself:** A user filters a table and sends the link to a colleague, who sees the unfiltered table. What's the fix?

- [ ] Store the filters in localStorage
- [x] Put the filters in the URL query and read them from there
- [ ] Add a Share button that emails a screenshot
- [ ] Cache the filters on the server per user

*Why:* Visible state belongs in the URL, so a shared or bookmarked link rebuilds the same view.

## In the skill

- https://school.agents.org.in/skill/layout/url-as-state.md
- https://school.agents.org.in/skill/layout/information-architecture.md
- https://school.agents.org.in/skill/layout/scroll-anchoring.md


---

# Sticky headers and launch-ready pages

> Pick a header behaviour that isn't the generated default, never hijack the scroll, and ship public pages that load fast and preview well when shared.

Module: Layout and pages · Level: intermediate · 8 min · https://school.agents.org.in/design/layout/sticky-and-launch

Scroll the article in this frame with the default header. Then try the other three.

*[Interactive demo: Four headers. The first is on nearly every generated landing page.]*

The first one, 70% white, 12px blur, no edge, pinned on every page, has become a tell. Nothing about it is broken; it is simply what every generated site does.

## Pick a header on purpose

A sticky header (A top bar that stays pinned while the page scrolls under it (position: sticky). Useful, overused, and often given a blur that has become a tell) stays pinned while the page scrolls. The skill offers four choices:

- **Solid with a border.** The page color plus a 1px border. Calm, cheap to render, nothing to notice.
- **Hide on scroll down, show on scroll up.** It gets out of the way while you read and comes back the moment you head up toward it.
- **Not sticky.** For long reading (articles, docs), the nav can simply scroll away. The calmest option.
- **Blur, but tuned.** If you really want glass: about 6px of blur, your own page color at around 85%, a 1px bottom border, and only on marketing pages.

## Never hijack the scroll

Scroll hijacking (When a page intercepts your scroll and does something else: snapping between sections, playing an animation, or slowing down. It makes the scroll feel broken) is a page that takes your scroll and does something else with it: snaps between sections, slows down, locks until an animation finishes, or moves the background at a different speed. The same gesture behaves differently from every other page, so the scroll feels broken.

Scroll snapping is right in a few contained places: a horizontal carousel, a gallery of explicit slides, a step-by-step onboarding. **Snap the contained scroller, never the page.**

Scroll-driven animation can be fine when it follows your scroll one-to-one: a reading progress bar, a gentle parallax no slower than about 0.7× scroll speed. Wrong whenever it changes how scrolling behaves, slows it down, or plays differently going up than down.

## Marketing pages: a bigger budget, still small

A visitor sees a landing page once, not a hundred times a day, so it can afford a little more expression. Emil Kowalski's checklist keeps it honest:

- **Motion maps to input.** No scroll-triggered fade-ups, no auto-advancing carousels. If the visitor didn't cause it, cut it.
- **Intro animations play once per session**, gated with `sessionStorage` (not `localStorage`, which would kill them forever).
- **Readable on the first frame.** No preloader; the headline readable on first paint. taste-skill's capture of 99 award sites found 60 opening on a loading screen and a third still unreadable 4.5 seconds in. Design marks were the same either way; only usability dropped.
- **Pre-render content pages** and preload the fonts and hero image, so nothing shifts on load.
- **Content stays in the DOM.** Nav submenus are visually hidden, not mounted on hover, so crawlers and screen readers see them.
- **CTAs know who's looking**: "Get started" logged out, "Open app" logged in.
- **Docs are for copying**: a copy button on every snippet and every page available as markdown.

*("Marketing tolerates more motion" gets read as "marketing wants more motion". The extra is one first-run hero moment, not every section animating in.)*

## What a link looks like when it's shared

Launch day *is* people pasting your link into chats. Flip the switches to build the preview a chat app shows.

*[Interactive demo: Bare URL, framework default title, a broken image from localhost, then a real preview.]*

The preview comes from Open Graph tags (Meta tags (og:title, og:description, og:image) that chat apps and social sites read to build the preview card when someone pastes your link). The rules:

- **A real title on every public route.** One template in the root layout (`%s · Product`), a per-route title, and generated titles for dynamic pages. Take the words from the page's own `h1`; never ship the framework's default.
- **A description** from copy that already exists, like the hero's supporting line.
- **An image at least 1200×630**, with the large-image card type.
- **`metadataBase` set to the production domain.** Without it, relative image URLs resolve to localhost and the preview breaks in production.
- **Crawl rules.** A leftover staging `Disallow: /` or site-wide noindex (Instructions that tell search engines not to list a page or site. Left over from a staging site, they quietly hide your launch from search) quietly hides the whole site from search. That's a launch blocker.
- **Icons**: your own favicon, an SVG icon and an `apple-touch-icon`.
- **The paying path works end to end**: landing, button, signup or checkout, first screen. Every link resolves and the price on the page matches the price at checkout.

Then paste the production URL into two chat apps and look.

**Launch pass (paste into your agent):**

```text
Pre-launch pass on our public pages:
- Header: solid page color with a 1px border (or hide on scroll down / show on up). No default 12px/70% blur.
- No scroll hijacking, no scroll-triggered fade-ups, no auto-carousels; snap only contained carousels.
- No preloader; h1 readable on first paint; intro animation gated by sessionStorage; fonts and hero image preloaded; content pages statically generated.
- Metadata: title template + per-route titles taken from each page's h1, descriptions from existing copy, a 1200×630 OG image, metadataBase = production origin.
- Check robots/sitemap for staging leftovers, add favicon + SVG icon + apple-touch-icon, and walk landing → CTA → signup end to end. List anything you can't fix (missing images, price mismatches) for me.
```

**Check yourself:** Your OG image works locally but every shared link in production shows a broken image. Most likely cause?

- [ ] The image is too large
- [x] No metadataBase, so the relative image URL resolves to localhost
- [ ] Chat apps don't support OG images
- [ ] The page uses client components

*Why:* Relative metadata URLs need a base. Without the production origin set, they resolve to the development host.

## In the skill

- https://school.agents.org.in/skill/layout/sticky-and-scroll-tells.md
- https://school.agents.org.in/skill/layout/marketing-surface-rules.md
- https://school.agents.org.in/skill/layout/share-and-crawl-metadata.md


---

# Every list has four states

> Design the loading, empty, error and full branches of any list or page, and get an agent to build all four.

Module: Components and states · Level: beginner · 7 min · https://school.agents.org.in/design/components/four-states

Watch a list arrive. First its outline, then a problem, then a retry, then the rows.

*[Interactive demo: A skeleton in the rows' own shape, an error that offers a way out, then the content.]*

That took about five seconds and showed you three different screens. Most generated UI only ever designs the last one, because the test account always has data. **The states are the work.** Components are where people touch your product, and their states are where most interfaces get lazy.

## Four branches, every time

Every list, table, feed and dashboard has four states:

| State | When you see it | Its job |
|---|---|---|
| **Loading** | The data is on its way | Hold the shape so nothing jumps |
| **Empty** | There is no data yet | Explain what belongs here and how to add it |
| **Error** | The data could not be fetched | Say what happened and offer a real fix |
| **Full** | The data is here | The screen you actually designed |

Flip through them below, then switch to **Agent default** to see what you get when nobody asks.

*[Interactive demo: Same list, same box. The designed version keeps its height in every state.]*

*(A missing branch in the code is a finding, even if the screen looks done.)*

## Loading: delay, then a skeleton

Speed is a feeling, so the indicator depends on how long the wait will be.

| Expected wait | Show |
|---|---|
| Under about 150ms | Nothing. Delay every indicator ~150ms so fast answers never flash one |
| By 400ms | A skeleton (A gray placeholder in the exact shape of the content that is loading: circles where avatars go, bars as wide as the words. When the real content arrives, nothing moves) for a content area; a spinner only inside the button that started the work |
| 3 to 15 seconds | A progress bar with a percentage |
| Over 15 seconds | Progress, time left, and a cancel button |

A skeleton is a gray placeholder in the exact shape of the content: circles where avatars go, bars as wide as the words. Size each row from the real row (48px because the loaded row is 48px, not a guess). When the content arrives, nothing moves.

**A centred spinner as the loading state of a whole list is a defect**: the page collapses to a dot, then everything jumps when the data lands. That jump is called layout shift (When something on the page jumps because content above or around it changed size, like a list collapsing to a spinner and then expanding when the data lands), and it is the first thing that makes a screen feel cheap.

> **In plain words.** **Skeleton**: a gray outline of the content shown while it loads. **Spinner**: the rotating circle. Spinners are fine inside a button; skeletons are for areas.

## Empty: three jobs

An empty state (What a list or page shows when there is no data yet. A good one says what belongs there and offers the action that fills it, instead of just "No data") is often the very first thing a new user sees. "No data" wastes it. A good one does three things:

1. **Says what belongs here.** "No projects yet."
2. **Points to the next action.** A real button: "Create project".
3. **Optionally, adds context.** One line on why it matters, or an example.

Size it like the full state, so the first item does not shove the layout. An empty search result says what was searched and offers to clear it. And never auto-redirect from an empty state ("nothing here, taking you home..."): it feels like the app broke.

  **Default (avoid):**

**No data**

  **Designed (do this):**

**No projects yet**<br />A project keeps one client's files, notes and invoices together.<br />`Create project`

## Error: what happened, and a way out

An error state answers two questions: what went wrong, and what can I do now. "Something went wrong." answers neither. "Couldn't load projects. The server didn't answer. Your projects are safe." plus a **Try again** button that really refetches answers both. The framework's built-in error page and 404 count as missing states, too.

## One more state: the first paint

A theme that flashes light before going dark, a sidebar that renders open then snaps shut: both happen because a saved value was read *after* the page painted. The fix is to decide before the first paint. A tiny script in the page head sets the theme class; layout choices like a collapsed sidebar come from a cookie the server reads; anything else waits to render until its value is known.

## Getting an agent to do it

Agents build the full state because that is what their test data shows. Ask for the other three by name, with the numbers.

**All four states (paste into your agent):**

```text
For every list, table and feed in this screen, build all four states: loading, empty, error and full.
- Loading: delay any indicator 150ms; then a skeleton with the loaded rows' exact height, widths and gaps. No centred spinner.
- Empty: a title saying what belongs here, one line of why, and the primary action button. Same height as the full list.
- Error: what happened in plain words, that nothing was lost (if true), and a Try again that refetches.
- Persisted UI state (theme, sidebar) is set before first paint, not in an effect.
List any view where a branch was missing before you started.
```

**Check yourself:** A list takes about 2 seconds to load. Which loading state fits?

- [ ] A centred spinner in the middle of the empty page
- [ ] Nothing at all until the data arrives
- [x] A skeleton with the shape of the loaded rows, shown after a ~150ms delay
- [ ] A progress bar with a percentage and a cancel button

*Why:* Content areas get a skeleton by 400ms, delayed ~150ms so fast loads never flash it. A centred spinner collapses the page; progress bars are for waits over 3 seconds.

## In the skill

- https://school.agents.org.in/skill/components/MOC-components.md
- https://school.agents.org.in/skill/components/empty-loading-states.md


---

# A button has eight states

> Give every control its hover, pressed, focus, loading, done, error and disabled states, keep them from shifting the layout, and run the polish pass that makes a component feel finished.

Module: Components and states · Level: beginner · 8 min · https://school.agents.org.in/design/components/button-states

Press the Save button. Then walk through the picker, one state at a time.

*[Interactive demo: One button, eight looks. The width never changes, so nothing around it moves.]*

An agent will happily give you the resting button and a hover color. The other six states are where a button earns trust: it answers your press, tells you it is working, and confirms it worked, all in the same spot.

## The eight, in one line each

- **Rest.** A label that starts with a verb: "Save changes".
- **Hover.** One step of background. Only on devices that can hover. This is the hover state (How a control looks while a mouse pointer is over it. Good ones are subtle (one step of background, a 1px lift) and only appear on devices that can hover).
- **Pressed.** `scale(0.97)` on `:active`, through a ~150ms transition. Never below 0.95.
- **Focus.** A visible ring for keyboard users, through `:focus-visible` (A CSS selector that matches when the browser decides focus should be shown, after Tab but not after a mouse click. Use it to show a focus ring only to keyboard users). More in the keyboard lesson.
- **Loading.** The label says what is happening ("Saving…"), clicks are ignored, the width holds.
- **Done.** It confirms on itself ("Saved"), holds a beat, returns to rest.
- **Error.** It says what did not happen; the details sit beside the form.
- **Disabled.** Faded and inert, with the reason written next to it.

*(The loading label and the done label live in the same box, stacked. Swapping them never resizes the button.)*

## Hover is not lift and shadow

The reflex hover is "lift the card 4px and add a big shadow". It is dated, and it is one of the tells of generated UI. Hover over both cards:

*[Interactive demo: Real hover with a mouse; the switch shows it on a touch screen.]*

Quieter patterns read as more considered:

| Element | Hover |
|---|---|
| Rows, list items, table cells | A background shift, about `rgba(0,0,0,0.04)`, over 120ms |
| Cards | `translateY(-1px)` and one step of tint |
| Inputs | The border gets one step darker |
| Icon buttons | The icon inside scales to 1.05, not the button |

A 1px lift is felt more than seen. 4px looks bouncy.

Two rules keep hover honest. First, **gate it**: wrap hover styles in `@media (hover: hover) and (pointer: fine)`. A phone fires `:hover` on tap and leaves it stuck, which looks broken. Second, **never change layout on hover**: no margin, padding, width or font weight. Animate only `transform`, `opacity`, `background-color`, `border-color` or `box-shadow`, so the neighbors stay put.

## Pressed, by product

How much a button squishes depends on who presses it. A consumer app can press to `scale(0.96)`; it reads as delight. A tool someone uses all day stays subtler, `scale(0.98)` or just a background change, because a springy button pressed four hundred times a day gets tiring.

## Disabled, layered right

`opacity: 0.5` and `cursor: not-allowed` is fine for a simple button. On a text input, give it an explicit disabled background and muted text instead; a flat opacity on a complex element looks washed out. And the classic mistake: a submit button disabled until the form is valid tells the user nothing about *why*. Let them press it and show the errors.

## The polish pass

Once the component works, run one pass over it. Each item is invisible on its own; together they are the difference between "works" and "finished".

  **Step: Hold the weight.** Never change font weight on hover or selection; bold text is wider and reflows the row. Signal with color.

  **Step: Tabular numbers.** `font-variant-numeric: tabular-nums` on anything that counts, so digits stop jiggling.

  **Step: No layout shift.** Skeletons, images and placeholders carry the loaded size or an `aspect-ratio`.

  **Step: States that stay put.** Hover transitions only the properties that change, ~150ms ease-out. Focus rings stay neutral and never vanish.

  **Step: One z-index scale.** Dropdown 100, modal 200, tooltip 300, toast 400, or `isolation: isolate`. Never `z-index: 9999`.

  **Step: Hit areas and optics.** Targets of 44px on touch, grown with a pseudo-element. Nudge a play triangle 1 to 2px toward its point so it looks centred.

  **Step: Decoration is inert.** `pointer-events: none` on glows and ornaments, so they never swallow a click.

Polish applied to the wrong structure gets thrown away with it. If the hierarchy or spacing is wrong, fix that first.

## Personality comes last, and sparingly

Once every state is right, a product can earn a little character in how it responds. **Personality lives in interactions, not in static screens**: a success message that names what happened ("Sent to 47 recipients" instead of "Success!"), a loading line that informs ("Fetching your last 30 days…"), a Delete that answers 100ms slower than Save because deliberation suits destruction.

Easter eggs belong where people are curious (a 404 page, Settings, a long press on the logo), never in the middle of signup or payment. And restraint wins: one well-placed surprise feels alive; ten feel exhausting. In a tool analysts use all day, skip it.

**Every state of a control (paste into your agent):**

```text
Give this button all of its states: rest, hover, pressed, focus, loading, done, error, disabled.
- Hover only inside @media (hover: hover) and (pointer: fine); one step of background, no lift, no layout change.
- Pressed: scale(0.97) on :active, 150ms ease-out.
- Focus: :focus-visible ring, 2px, neutral color, 2px offset.
- Loading: label "Saving…", ignore repeat clicks, fixed width; the labels are stacked layers that crossfade.
- Done: "Saved" on the button for ~1.5s, then back to rest. Error: "Not saved", details beside the form.
Then run a polish pass: constant font weight, tabular-nums on numbers, no layout shift, no z-index above the scale.
```

**Check yourself:** A tab bar makes the selected tab bold. What goes wrong, and what is the fix?

- [ ] Nothing; bold is the clearest signal
- [x] Bold text is wider, so the tabs shift sideways; keep the weight and signal with color
- [ ] Bold is too slow to animate; use a 300ms transition
- [ ] Bold fails contrast; make it uppercase instead

*Why:* Changing weight reflows the text and shoves its neighbors. Hold the weight constant and show selection with color or an indicator.

## In the skill

- https://school.agents.org.in/skill/components/hover-states-subtle.md
- https://school.agents.org.in/skill/components/ui-polish-pass.md
- https://school.agents.org.in/skill/components/interaction-personality.md


---

# Forms that don't nag

> Validate fields at the right moment, write errors that fix themselves, and wire the dozen small behaviors every form owes its user.

Module: Components and states · Level: beginner · 8 min · https://school.agents.org.in/design/components/forms

The field below opens mid-word, already scolding you for `maya@`. Switch to **On blur**, type `maya@` again, then tap the name field.

*[Interactive demo: The same email field. One scolds you mid-word; the other waits until you are done.]*

The keystroke version shows an error after the first letter. You were not wrong; you were not finished. That is the cardinal sin of form validation: punishing someone for an action still in progress.

## The cadence

Inline validation (Checking each form field where it is, as the person moves through the form, instead of listing every error at the top after they press Submit) means checking each field where it is, as the person moves through the form, instead of one list of errors after Submit. The timing is the whole craft:

  **Step: While typing: positive signals only.** No red borders, no error text. The only feedback allowed is good news: a small check when the field becomes valid.

  **Step: On blur: the real check.** When the person leaves the field (blur (The moment focus leaves a form field, by tabbing or clicking elsewhere. It is the right time to check the field, because the person has signalled they are done with it)), validate. If it fails, show the error now. They said "I'm done"; tell them if it's wrong.

  **Step: Once an error shows: check on every change.** From then on, re-validate on each keystroke so the error clears the moment the value is fixed. Making them leave the field again to find out reads as the form still being cross.

  **Step: On submit: a final pass.** Catch what blur missed (rules across fields, server checks). If anything fails, move focus to the first invalid field.

Here it is in a real product: the error lands only when the cursor moves on to Password, and clears as the ending is typed.

*[Interactive demo: Error on blur, cleared while fixing, then the button confirms on itself.]*

## What an error looks like

Each failing field gets three things, together: a danger-colored border, a message directly under the field (13 to 14px, in its own lane that exists before the error does, so the form never jumps), and `aria-invalid` plus `aria-describedby` on the input so a screen reader reads the message with the field.

Rules that span two fields, like "passwords match" or "start date before end date", are checked when the **second** field blurs. The first one cannot be wrong on its own.

  **Before (avoid):**

Invalid input.<br />Please enter a valid email.

  **After (do this):**

Add the domain, like name@company.com

Good error copy says what is wrong and how to fix it, skips "Please", never says "Invalid", and sounds like a person.

## When not to validate

- A single search box: just search.
- A comment or chat message: there is no wrong answer.
- An inline rename: validate on Enter, not on blur, since the person may click away with the value intact.

And mark only the fields that are truly required; label the rest "Optional".

## Everything else a form does

Validation is one job. A form owes about a dozen more, and every one is easy to forget.

| Behavior | The rule |
|---|---|
| Labels | Every input has a real `<label>`, so clicking it focuses the field. A placeholder is not a label |
| Input types | `type="email"`, `tel`, `url`, `password`: the right phone keyboard and autofill come free |
| Font size | Inputs at 16px or more on phones, or iOS zooms the page in |
| Enter | Wrap fields in a `<form>` so Enter submits; a textarea submits on Cmd or Ctrl+Enter |
| Submitting | Disable the button while the request runs and change its label ("Saving…"); re-enable it in `finally`, or one failure locks the form |
| Failure | A failed submit says so and **keeps every value**. Never make anyone retype |
| Icons in the field | Positioned over the input with padding reserved, never a sibling the click misses |
| Autofocus | First field in a desktop dialog, never on touch, where it throws the keyboard up |
| Prefill | Anything you already know: the current username, the `?email=` from an invite link |
| Accidents | Long forms warn before leaving, or keep a draft, so a stray back-swipe costs nothing |

**`fetch` does not throw on a server error.** An agent's `try { await fetch() } catch {}` treats a 500 as success. Check `response.ok`, and never call `reset()` before you know it worked.

> **Autocomplete, curated.** Keep `autocomplete` on for names, addresses and payment. Turn it and spellcheck off for usernames, codes and search. Use `inputmode="numeric"` and `autocomplete="one-time-code"` on verification codes, and `current-password` vs `new-password` so password managers do the right thing.

*(Auto-advancing code boxes feel clever until someone needs to fix one digit.)*

**Validation cadence (paste into your agent):**

```text
Rework this form's validation:
- While typing: no errors; show a small check inside the field once it is valid.
- On blur: validate and show the error under the field in a reserved message lane (no layout shift).
- Once a field has shown an error, re-validate on every change so it clears as soon as it is fixed.
- On submit: run everything, focus the first invalid field. Cross-field rules run when the second field blurs.
- Error copy says what is expected ("Add the domain, like name@company.com"), no "Please", no "Invalid".
- Inputs have <label>, the right type, 16px font, aria-invalid and aria-describedby.
- The submit button disables with "Saving…" while in flight, re-enables in finally; a failed submit keeps every value and checks response.ok.
```

**Check yourself:** When should an email field first show 'Add the domain, like name@company.com'?

- [ ] As soon as the first character is typed
- [x] When the person leaves the field with an invalid value
- [ ] Only after Submit, in a list at the top
- [ ] Never; disable the Submit button instead

*Why:* Hard validation happens on blur, when the person has signalled they are done with the field. After that, it re-checks on every change so the message clears when fixed.

## In the skill

- https://school.agents.org.in/skill/components/forms-validation.md
- https://school.agents.org.in/skill/components/forms-behavior.md


---

# Instant actions, undo and toasts

> Make likely-success actions feel instant, choose between undo and a confirmation for destructive ones, and use toasts (and Sonner) without the usual bugs.

Module: Components and states · Level: intermediate · 9 min · https://school.agents.org.in/design/components/optimistic-and-undo

Both hearts get tapped once as they scroll in. Same server, same 700ms delay; tap them again yourself.

*[Interactive demo: Left waits for the server. Right updates now and only rolls back if the server says no.]*

The right one feels instant because it *is* instant, as far as you can tell. That is an optimistic update (Changing the screen right away as if an action succeeded, sending the request in the background, and rolling back only if it fails. It makes likes and toggles feel instant): change the screen immediately, send the request in the background, and undo the change only if it fails. Tick "Make the server fail" to watch the rollback.

It is the single biggest speed win available without touching the servers.

## When to be optimistic

```ts
const previous = liked;
setLiked(!liked);                // update now
try {
  await api.toggleLike(post.id); // confirm in the background
} catch {
  setLiked(previous);            // roll back
  toast.error("Couldn't update the like. Try again.");
}
```

| Be optimistic | Don't |
|---|---|
| Toggles: like, bookmark, follow, mute | Payments and bookings: the confirmation *is* the design |
| Marking a to-do complete | Anything only the server can validate, like "is this username free?" |
| Renaming with client-side checks | Sign-up and login, where a failure is information |
| Reordering a list | Destructive actions **without** undo |

If the server creates an id, give the new item a temporary client id and swap it when the response lands. And test the rollback path on purpose: a rollback that yanks the page somewhere else is worse than a 200ms spinner.

## Undo or confirm?

Now the scary actions. Archive a project below, then try deleting the whole workspace.

*[Interactive demo: Archive acts at once with undo. Delete workspace can't be undone, so it asks you to type its name.]*

A confirmation on every delete trains people to click through it, so the one that matters gets clicked through too. Decide first:

| The action | Pattern |
|---|---|
| Reversible and frequent: archive, move, delete to trash | Do it now; offer **Undo** in the result |
| Irreversible, one item | Confirm |
| Irreversible, many items | Confirm, with the count |
| Affects other people | Confirm, and say who: "The 8 members lose access" |
| An account, workspace or repository | Type its name to confirm; the button stays disabled until it matches |

Undo is cheaper for the user and more honest. It is also the optimistic pattern: remove the row now, and let the real delete run during the undo window.

## A confirmation you can answer without reading the body

  **Before (avoid):**

**Are you sure?**<br />This action cannot be undone.<br />`OK` `Cancel`

  **After (do this):**

**Delete workspace "acme-web"?**<br />This permanently deletes 4 projects and their 212 files.<br />`Cancel` `Delete workspace`

- **Title** names the action and the object. Never "Are you sure?".
- **Body** says what is lost, with counts.
- **Buttons** are verb plus object, never Yes and No.
- **Focus starts on Cancel**, so a reflexive Enter does no harm.
- **Only destruction is red.** If the brand itself is red, give destruction a different treatment (outlined, with an icon).

*(Delete sitting in a menu right under Rename, with no gap, no color and no undo, is the most common unguarded delete there is.)*

## The undo toast is not a normal toast

A toast (A small message that slides in at the edge of the screen to confirm something ("Changes saved") and leaves on its own. One with an Undo button has to stay longer and pause while you look at it) is a small message that slides in at the edge of the screen and leaves on its own. The undo toast carries an action, so it plays by stricter rules: at least 5 seconds, **paused while hovered or focused**, and announced politely without stealing focus. An undo link that vanishes on a timer is data loss on a schedule.

Try the stack: fire a few, then hover or tap it.

*[Interactive demo: Newest in front. The stack spreads and every timer pauses while you look.]*

A plain "Changes saved" can leave after 4 seconds. An error that needs action stays until dismissed. Reading time scales with the words (about 200 to 250 words a minute), never a flat 2 seconds.

## Sonner in practice

Sonner, by Emil Kowalski, is the toast library most React apps reach for. It needs exactly two pieces: **one** `Toaster` component mounted once near the root (the root layout in Next.js), and `toast()` called from client code.

| Need | Call |
|---|---|
| A message with a second line | `toast("Saved", { description })` |
| A typed icon | `toast.success`, `.error`, `.info`, `.warning` |
| Tied to a promise | `toast.promise(p, { loading, success, error })` |
| Update a toast later | `const id = toast.loading("…")`, then `toast.success("Done", { id })` |
| A button | `action: { label: "Undo", onClick }` |
| Your own design | `toast.custom((t) => …)` |

The bugs are almost always one of these:

| Symptom | Cause |
|---|---|
| Nothing appears | No Toaster mounted, or `toast()` ran on the server |
| Appears twice | Two Toasters (layout and page), or a React effect ran twice; fire from the click handler or pass a stable `id` |
| Hidden behind a modal | An ancestor created a stacking context; mount the Toaster at the document root |
| Stuck on loading | The promise never settles |
| "Dark mode is broken" | `theme` defaults to `"light"`; pass `theme="system"`. Colors stay gray until `richColors` is set |

**Optimistic with undo (paste into your agent):**

```text
Make archiving instant: remove the row immediately, then call the API. Show a Sonner toast "Archived 'NAME'." with an Undo action that restores the row at its old position and cancels the request.
- The toast stays at least 6s, pauses on hover and focus, and does not take focus.
- If the API fails, restore the row and toast.error("Couldn't archive. Restored.").
- Move focus to the next row's action (or the previous one) when the focused row disappears.
- One Toaster, with theme="system" and richColors, in the root layout only.
For deleting the workspace, use our dialog: title "Delete workspace 'NAME'?", counts in the body, Cancel focused, "Delete workspace" disabled until the name is typed.
```

**Check yourself:** Which action should get an undo toast instead of a confirmation dialog?

- [ ] Deleting a whole workspace and its repositories
- [x] Archiving one email from the inbox
- [ ] Charging a customer's card
- [ ] Removing every member from a team

*Why:* Archiving is frequent and reversible, so act at once and offer undo. Irreversible or many-item actions, and anything involving money, get a confirmation.

## In the skill

- https://school.agents.org.in/skill/components/optimistic-updates.md
- https://school.agents.org.in/skill/components/destructive-actions.md
- https://school.agents.org.in/skill/components/sonner-usage.md


---

# Keyboard, focus and touch

> Make every control reachable by keyboard and thumb, show focus without showing it to mouse users, and give custom widgets the key maps people expect.

Module: Components and states · Level: intermediate · 9 min · https://school.agents.org.in/design/components/keyboard-and-touch

Focus is already on one of these buttons. Can you tell which? Click into the toolbar and press Tab a few times in both modes. (On a phone, use Step.)

*[Interactive demo: With outline: none, focus is still moving. You just can't see where.]*

`outline: none` is one of the most common lines an agent writes, because the default focus outline looks a little clumsy after a mouse click. The fix is not to delete it. It is to show it only to the people who need it.

## Focus you can see, only when it helps

`:focus-visible` (A CSS selector that matches when the browser decides focus should be shown, after Tab but not after a mouse click. Use it to show a focus ring only to keyboard users) is a CSS selector that matches when the browser decides focus should be shown: after Tab, not after a mouse click. So:

```css
button:focus { outline: none; }               /* no ring on click */
button:focus-visible {
  outline: 2px solid currentColor;            /* ring for the keyboard */
  outline-offset: 2px;
}
```

Even simpler: keep the browser's own ring and add only `outline-offset: 2px`. It already adapts to the platform and to high-contrast modes. A custom ring is fine if it clears 3:1 against every color it sits on, and the focused element is never hidden under a sticky header.

## The floor below taste

Accessibility is below polish and below brand. A screen that fails it is broken, whatever it looks like. The baseline every component passes:

- **Everything works by keyboard**, reached by Tab in source order. The DOM order is the visual order; never reorder with CSS, never use a positive `tabindex`.
- **Real elements.** `<button>` beats `<div onClick>`, which keyboards and screen readers can't use.
- **Icon-only buttons get an `aria-label` (An attribute that gives an element its spoken name for screen readers. Every button that shows only an icon needs one that names the action, like "Close dialog")** that names the action: "Close dialog", never "icon".
- **Skip link** at the top: "Skip to content", hidden until focused.
- **One `<main>`, one `<h1>`**, headings that never skip a level, and `<html lang>` set.
- **Not by color alone.** Errors get an icon and red; color-blind users (about 8% of men) need the second cue.

The four-key test catches most of it: Tab through the page. Can you reach everything? Is the ring visible? Do Enter and Space activate? Does Escape dismiss?

## Custom widgets owe a key map

Native elements ship their keyboard behavior. A custom menu, tabs or combobox has to rebuild it, and the rebuild is where it fails. Drive this menu with the keyboard, or with the keys beside it:

*[Interactive demo: Down opens on the first item, Up on the last, Escape closes and hands focus back.]*

`tabindex` (An attribute that controls whether something can take keyboard focus. Only two values are legal: 0 (joins the Tab order) and -1 (focusable from code only). A positive number breaks the page's order) has two legal values. `0` adds a custom control to the Tab order; `-1` makes something focusable from code only. A composite widget (tabs, menu, toolbar, radio group) takes **one** Tab stop: the active item is `0`, the rest `-1`, and the arrow keys move both focus and the `0`. That is a roving tabindex (A way to give a group of controls (a menu, tabs, a toolbar) a single Tab stop: the active item has tabindex 0, the rest -1, and the arrow keys move focus between them).

| Widget | Keys |
|---|---|
| Dialog | Tab cycles inside and wraps; Escape closes; focus returns to the trigger |
| Menu button | Enter, Space or ↓ opens on the first item; ↑ on the last; Escape closes and refocuses the button |
| Tabs | Arrows move and wrap; Home and End jump; Tab leaves to the panel |
| Disclosure | A `<button aria-expanded>`; Enter and Space toggle |
| Combobox | ↓ opens; Enter accepts; Escape closes; typing filters |

Escape always closes whatever opened **last**: tooltip, then menu, then dialog. Prefer `<dialog>` with `showModal()`, which makes the background inert and handles Escape for free. And after a client-side route change, set the page title and move focus to the new `<h1>`, or a screen reader hears nothing happen.

**A `role="tab"` with no key handler looks finished in every screenshot and fails every keyboard.** Grep for `role="button"`, `role="tab"` and `role="menuitem"` and check each one.

## Touch: hover enhances, never enables

A thumb has no hover and is about 44px wide. Tap the icons quickly in both modes:

*[Interactive demo: The icons look the same either way. Grown, their invisible targets reach 44px.]*

The touch rules:

1. **Hover is a bonus.** Gate it with `@media (hover: hover) and (pointer: fine)`, and never put the only way to a control behind it. Card actions shown on hover are always visible on touch.
2. **Targets of 44px** on touch (40px on desktop). Keep the icon small and grow the hit area (The invisible region that responds to a click or tap. It can be bigger than the icon you see, so a 24px icon can still have a 44px target for a thumb) with a pseudo-element: `inset: -10px` on a 24px icon makes 44px. Hit areas never overlap.
3. **`touch-action: manipulation`** on controls kills the double-tap zoom delay.
4. **Hidden means hidden.** A closed drawer moved off-screen with a transform, or faded to `opacity: 0`, is still in the Tab order. Use `inert`, `visibility: hidden` or `display: none`.
5. **Focus moves in and comes back.** A dialog focuses its first control on open and returns focus to its trigger on close. Delete the focused row, and focus goes to the next row.
6. **Timers pause** when the tab is hidden, and on hover and focus.

*(Tabbing into a closed drawer is the classic miss: the keyboard user lands inside something they can't see.)*

A few phone fixes come up in every project: `-webkit-tap-highlight-color: transparent` for the gray tap flash, `100dvh` instead of `100vh`, 16px inputs to stop zoom, and `user-select: none` on controls (never on the whole page) so a long press doesn't select button text.

**Keyboard and touch pass (paste into your agent):**

```text
Run a keyboard and touch pass on this component:
- Replace outline: none with :focus-visible rings (2px, offset 2px, 3:1 against the background).
- Every icon-only button gets an aria-label naming the action. No div onClick.
- Custom menu: Enter/Space/Down open on the first item, Up on the last, arrows wrap, Home/End, Escape closes and refocuses the trigger, roving tabindex (one Tab stop).
- Hover styles inside @media (hover: hover) and (pointer: fine); nothing reachable only by hover.
- Touch targets ≥ 44px via a pseudo-element; touch-action: manipulation on controls.
- Hidden panels use inert or visibility: hidden, never opacity or transform alone.
```

**Check yourself:** A closed side drawer is hidden with transform: translateX(100%). What breaks?

- [ ] Nothing, it is off-screen
- [x] Its links are still in the Tab order, so keyboard focus lands on invisible controls
- [ ] It can't animate open
- [ ] Screen readers announce it twice

*Why:* Transforms and opacity hide things visually but leave them focusable. Use inert, visibility: hidden or display: none when it is closed.

## In the skill

- https://school.agents.org.in/skill/components/accessibility-baseline.md
- https://school.agents.org.in/skill/components/keyboard-patterns.md
- https://school.agents.org.in/skill/components/touch-and-focus.md


---

# What screen readers hear, and the WCAG numbers

> Announce changes with the quietest mechanism that works, hide things the right way, and cite the WCAG 2.2 thresholds that turn an opinion into a finding.

Module: Components and states · Level: intermediate · 8 min · https://school.agents.org.in/design/components/announcements-and-wcag

Below is a transcript of a screen reader (Software that reads the interface aloud (or to a braille display) for people who can't see the screen, like VoiceOver on Apple devices and NVDA on Windows) reading an invoice aloud. It presses Save by itself once a lap; flip the mode, or press Save yourself mid-sentence.

*[Interactive demo: Polite waits for the sentence to end. Assertive interrupts it.]*

A sighted person sees "Saved" appear. A screen-reader user hears it only if something announces it, and how it is announced decides whether it helps or barges in.

## The ladder: stop at the first rung that fits

  **Step: Focus moves there anyway.** An opened dialog, the first invalid field after Submit. Moving focus *is* the announcement. Add nothing.

  **Step: It belongs to one control.** A field's error, a character count: `aria-describedby` on the input, so it is read with the field.

  **Step: Not urgent, not tied to a control.** A toast, "Saved", "12 results", "Loading…": a polite live region (A part of the page that screen readers watch and read aloud when its text changes. role="status" waits politely; role="alert" interrupts), `role="status"`.

  **Step: Urgent, not tied to a control.** A form-level failure, a session about to expire: `role="alert"`, and nothing else gets it.

`role="status"` is the polite one; `role="alert"` is assertive and interrupts. **Default to polite.** The common over-correction is an assertive success toast: "Saved" now cuts off whatever the user was listening to.

## Making the region actually speak

Live regions are fussy. Three rules make them reliable:

- **Render the region empty first, then change its text.** A region that mounts already holding its message is announced inconsistently.
- **Keep messages short and whole.** The region re-reads everything inside it on each change.
- **Never move focus to a toast.** Announce it; leave focus where the person is working.

  **Before (avoid):**

```tsx
{msg && <div role="status">{msg}</div>}
```

  **After (do this):**

```tsx
<div role="status" className="sr-only">{msg}</div>
```

For loads, put `aria-busy="true"` on the updating area, say "Loading…" politely, then the outcome: "Loaded, 12 results".

## Hiding, two opposite ways

| You want | Use |
|---|---|
| Invisible on screen, still read aloud | `.sr-only`: 1px box, `clip-path: inset(50%)`, `white-space: nowrap`. One pixel, not zero: some readers skip empty boxes |
| Visible on screen, not read aloud | `aria-hidden="true"` on decorative icons and visual duplicates |

Never put `aria-hidden` on or above a button, link or input. Tab still lands there, and the reader says nothing exists. To hide something interactive, use `inert`.

## Alt text by purpose

| The image | `alt` |
|---|---|
| Decorative, or repeats nearby text | `alt=""`: present and empty |
| Informative | What it adds: "Ticket QR code" |
| The image is the button | The action: "Search", not "magnifying glass" |
| A chart | A one-line summary, with the data in a table nearby |

A missing `alt` is worse than an empty one: the reader falls back to the file name.

## The numbers that make a finding

In a review, "this feels inaccessible" is an opinion. A WCAG (The Web Content Accessibility Guidelines, the standard most laws point to. Its contrast rule is the familiar 4.5:1 for body text and 3:1 for large text) criterion with its number is a finding. WCAG 2.2 Level A and AA are the rules products are held to:

| Criterion | The rule |
|---|---|
| 1.4.3 Contrast | Text 4.5:1; large text (24px, or 18.67px bold) 3:1 |
| 1.4.11 Non-text contrast | Input borders, icons, focus rings: 3:1 against every adjacent color |
| 1.4.1 Use of color | Status never by color alone |
| 2.5.8 Target size | 24×24px, or spaced so a 24px circle on it touches no other target |
| 2.5.7 Dragging | Every drag has a single-click alternative |
| 2.4.11 Focus not obscured | The focused element is never fully hidden by sticky chrome |
| 1.4.4 / 1.4.10 | Survives 200% text; works at 320px wide with vertical scroll only |
| 2.2.2 Pause, stop, hide | Anything moving or updating for more than 5 seconds has a pause |
| 1.4.2 Audio | Nothing autoplays with sound |

Disabled controls and logos are exempt from contrast. The 44px touch target and "one `<h1>` per page" are good advice but not AA, so they are recommendations, never the top severity.

> **Fixes the numbers imply.** Focus under a sticky header: `scroll-padding-block-start` equal to the header height. Zoom: never `maximum-scale=1`; text boxes take `min-height`, not `height`. Windows High Contrast drops box-shadows, so a shadow ring needs a `1px solid transparent` border underneath. A toast with an action stays at least 5 seconds and pauses on hover.

*(Measure contrast against what the text actually sits on, not the page background.)*

**Announcements and a WCAG check (paste into your agent):**

```text
Audit this screen for screen-reader output and WCAG 2.2 AA:
- Pick the quietest announcement per change: focus move, aria-describedby, role="status", role="alert" (failures that block only).
- Live regions are always in the DOM and start empty; never focus a toast.
- Decorative icons aria-hidden; nothing aria-hidden contains a focusable element.
- Alt text by purpose; empty alt for decoration.
- Report each failure as: element, WCAG criterion number, measured value, threshold. Don't change colors unless I ask.
```

**Check yourself:** A 'Changes saved' toast should be announced with…

- [ ] role="alert", so nobody misses it
- [x] role="status", a polite region that waits for the current speech
- [ ] Moving keyboard focus onto the toast
- [ ] Nothing; screen readers read toasts automatically

*Why:* Success is not urgent and not tied to a control, so it gets a polite status region. Alert is for failures that block the task, and focus should never jump to a toast.

## In the skill

- https://school.agents.org.in/skill/components/screen-reader-announcements.md
- https://school.agents.org.in/skill/components/wcag-thresholds.md


---

# Words in the interface

> Write labels, errors and status copy that guide, build strings that survive translation, and name components by what they do.

Module: Components and states · Level: beginner · 8 min · https://school.agents.org.in/design/components/words-in-the-ui

Set the count to 1. Then to 0. Then switch the locale.

*[Interactive demo: The glued sentence breaks at 1 and 0. The ICU message and Intl never do.]*

"You have 1 new messages" is what you get when a sentence is glued together around a number. It is a small thing, and people notice it every single time. Microcopy (The small words in an interface: button labels, error messages, empty states, tooltips, status lines. Small, read constantly, and a big part of how a product feels), the small words on buttons, errors, empty states and tooltips, is design. It ships every day, and the wrong word quietly ages the product.

## The voice rules

- **Active voice.** "Saved your changes" beats "Your changes have been saved".
- **Talk to "you".** "You can invite 3 more people", not "Users can…". In errors, "we" sounds like deflection.
- **Specific beats vague.** "Couldn't connect to the server" beats "Something went wrong".
- **Skip the throat-clearing.** "Saved." beats "Awesome, your changes were saved!".
- **Numerals for counts.** "3 projects", even small ones. A non-breaking space keeps "3 MB" together.
- **Real characters.** The ellipsis `…`, curly quotes, an en dash for ranges (5–10).

Pick one capitalization for buttons and headings, Title Case or sentence case, and hold it across the whole product. Mixing them on one screen is the mistake, not either choice.

*(Don't be cute in errors. "Oops! Something tripped on the wires!" is funny once and annoying the tenth time.)*

## Errors that guide the exit

A good error answers three questions: what went wrong, why (if it helps), and what to do next.

  **Before (avoid):**

Something went wrong.<br />Error: 500 Internal Server Error

  **After (do this):**

Changes not saved. Check your connection and try again.

## Status copy stays parallel

Async states read best as one family: same object, same order.

| State | Pattern | Example |
|---|---|---|
| In progress | -ing verb + object | Saving changes… |
| Done | object + past tense, no "successfully" | Changes saved |
| Failed | what did not happen + next step | Changes not saved. Check your connection and try again. |
| Unavailable | why, beside the control | Add a payment method to publish |
| Partial | both counts | 9 of 12 files uploaded. Retry 3 failed files |

## Labels that do their job

- **Buttons start with a verb** naming the action: "Send", "Save draft", "Delete project". Never "OK" or "Let's go!". An icon button's `aria-label` follows the same rule.
- **Links name their destination**: "Read the billing docs", not "Click here". Two "Learn more" links on one page each get a suffix.
- **Toggles describe the ON state**: "Send read receipts", never "Don't send read receipts", which turns the switch into a double negative.
- **Match the device**: "tap" on touch, "click" with a mouse, "select" when it could be either.
- **A placeholder is an example**, like `name@example.com`, never the label and never an instruction.

## Strings that survive translation

Before writing new copy, find where the existing copy lives (the `t(` calls and the `locales/*.json` files) and list the words already in use. If the menu says "Archive", the toast does not say "Move to storage". One thing, one name.

Then build every sentence whole. Word order changes between languages, and many languages have more than two plural forms, so `n === 1 ? "" : "s"` is wrong outside English. Use one ICU message (A standard format for writing one translatable sentence with placeholders and plural rules inside it, like {count, plural, one {# file} other {# files}}, instead of gluing strings together in code) with placeholders:

```text
inbox.count = {count, plural,
  =0 {No new messages}
  one {# new message}
  other {# new messages}
}
```

And format every number, date and list through `Intl` (The browser's built-in formatter for numbers, money, dates, relative times and lists in any language. It knows that Germans write 1.234,50 € and Americans $1,234.50) with the user's locale:

| Value | Call |
|---|---|
| Money | `Intl.NumberFormat(locale, { style: "currency", currency })` |
| Date | `Intl.DateTimeFormat(locale, { dateStyle: "medium" })` |
| "Yesterday" | `Intl.RelativeTimeFormat(locale, { numeric: "auto" })` |
| "Maya, Jon and Ana" | `Intl.ListFormat(locale, { type: "conjunction" })` |

`toLocaleDateString()` with no locale, or a date built from `getMonth()`, is the bug. Translated strings also grow, a one-word button most of all, so leave room.

## Naming components by behavior

Words matter for components too. Some look alike, and picking by looks breaks what users expect. **Pick by behavior, not appearance.**

| Pair | The line between them |
|---|---|
| Tooltip (A small label that appears on hover or focus to explain an element. It can't hold anything you click, because the pointer can't move into it without closing it) vs popover (A floating panel that opens when you click something and stays open until dismissed. Unlike a tooltip, it can hold links, buttons or a small form) | Can it hold a link or button? A tooltip is a hover label and can't; a popover opens on click and can |
| Badge vs tag | A badge is pinned on and read-only (the "3" on an inbox); a tag is standalone and you can select or remove it |
| Dialog vs sheet vs drawer | A dialog interrupts, centre-screen, for a decision now; a sheet slides from an edge; a drawer pulls up from the bottom. Sheets and drawers are for secondary, dismissible surfaces |

Three questions settle it: must they interact inside it (popover)? Can they act on it (tag)? Must they decide now (dialog)?

**Copy and strings pass (paste into your agent):**

```text
Review the copy in this screen:
- Inventory existing nouns and verbs first (t() calls, locales/*.json) and reuse them; one name per thing.
- Buttons start with a verb; links name their destination; toggles describe the ON state; placeholders are examples.
- Errors: what failed + the next step. Status copy parallel: "Saving changes…" → "Changes saved" → "Changes not saved. Check your connection and try again."
- No sentence glued around a variable; plurals through ICU {count, plural, ...}; numbers, dates and lists through Intl with the user's locale.
- Keep one capitalization style for all buttons and headings.
```

**Check yourself:** A floating box needs to hold a 'Read the docs' link. Which component is it?

- [ ] A tooltip, because it floats next to the element
- [x] A popover, because it holds interactive content and opens on click
- [ ] A badge, because it is attached
- [ ] A toast, because it is small

*Why:* A tooltip can't take focus, so a link inside it is unreachable. Anything with a link or button inside is a popover.

## In the skill

- https://school.agents.org.in/skill/components/copy-voice.md
- https://school.agents.org.in/skill/components/strings-and-locale.md
- https://school.agents.org.in/skill/components/component-confusables.md


---

# Cards, icons, avatars and APIs

> Get the everyday parts right (cards, icons, avatars, presence and drawing tools) and design component props an agent can't misuse.

Module: Components and states · Level: intermediate · 9 min · https://school.agents.org.in/design/components/parts

Tap the names one by one and watch the left avatar.

*[Interactive demo: The one-liner breaks on emoji, suffixes, extra spaces and names without spaces.]*

`name.split(" ")[0][0]` is what every agent writes for initials, and real names break it: an emoji splits into half a character, "Christopher Montgomery III" becomes "CI", "王秀英" has no spaces. Small parts like this repeat on every screen, so their mistakes do too.

## Avatars: photo, initials, generated

Pick an avatar strategy before you scale; switching later is painful because people learn what they look like in your product.

1. **Real photo**, always preferred. `object-fit: cover` handles a 4000×200 upload; `flex-shrink: 0` keeps the circle round in a crowded row.
2. **Initials**, the common fallback. First and last word, by grapheme cluster (`Intl.Segmenter`), trimmed, suffixes like "III" dropped, uppercased. No usable name: the email's first letter, then a generic person icon.
3. **Generated**, from a seed. DiceBear makes the same SVG face from the same user id every visit. Self-host it (`@dicebear/core`) rather than hitting its public API on every page load, and check each style's license.

Match the style to the product: `initials` or abstract shapes for B2B, robots (`bottts`) for an AI agent, illustrated faces for consumer social. Healthcare, legal and finance stay with initials. And give every image an `onError` that falls back down the chain; a broken-image glyph should never show.

## Cards: never nested

A card is a self-contained unit with its own interior. A card inside a card inside a card means three borders, three shadows, three radii, and a design that has lost its way. The inner card is usually something else:

| Reaching for an inner card? | It is probably |
|---|---|
| A list of items | Rows with a hover state |
| A heading and its content | Typography and spacing |
| A status | A badge, inline |
| Comparison data | A real table |

Inside the card, space beats lines: a 24px gap between header and body reads cleaner than a divider. One small icon per card at most. Edit and delete actions can appear on hover, but **always show them on touch**. And if a card has a 12px radius and an image filling its top, the image needs `12px 12px 0 0`, or it looks off. If a screen is *all* cards, it has no hierarchy.

*(Most "this card looks off" complaints are a mismatched inner radius.)*

## Icons: one pack, matched to the type

The eye notices three icon packs on one screen instantly, even when nobody can say why. **Never mix icon packs.** If you need an icon your pack lacks, draw it in that pack's style.

- **Lucide is fine but everywhere.** Phosphor, Hugeicons or Tabler give a product its own look.
- **Sizes:** 16px inline with text, 20px in toolbars, 20 to 24px in navigation, 32 to 48px in empty states.
- **Stroke matches font weight:** regular text (400) wants about a 1.5px stroke; medium (500) about 1.75 to 2px.
- **One icon, one meaning.** A chevron can't mean both "expand" and "next page". Upload and download, which differ only by a 180° turn, need a label.
- **Transform, don't crossfade.** A chevron opening rotates 180°.
- 6 to 8px between an icon and its label; ambiguous icons (the floppy disk) get a text label.

## Presence: three states read better than two

Most real-time products show presence as one green dot. Benji Taylor's chat app Honk made presence the thing every feature was built around, and the patterns travel to any shared surface. Step through them:

*[Interactive demo: Arriving, fully here, half here, leaving. Each state has its own signal.]*

A presence signal (How a shared product shows who else is there and how much attention they are paying: arriving, here, half here, leaving. More than a green dot) shows who is here and how much. **Arriving** gets a moment (a ring filling), not a dot changing color. **Here** shows live activity: the typing bubble grows letter by letter. **Half here** (in the app but looking elsewhere) is a dashed outline, the state most products drop and the one that explains a slow reply. **Leaving** gets a short goodbye beat.

One color per person, everywhere: cursor, selection, avatar ring, name label. Reactions pin to where they were tapped, stored as relative coordinates. And presence fires often, so scale the signal down as frequency goes up, announce joins and leaves in a live region, and never fire it for your own arrival.

## A drawing toolbar, done well

If your product needs a little markup (circling a bug on a screenshot), Benji Taylor's Drawesome is the version worth copying. The bar **morphs** instead of opening panels: pick a color and the row of pens becomes the palette; minimize it and it rolls into a disc that still shows the tool in hand. Pens behave like their names: a pen thins as you speed up, a highlighter keeps its width, the eraser removes area rather than whole strokes. Shift locks a stroke to eight directions; `E` erases; `[` and `]` resize. Export returns exactly what is on screen. Under about 680px, the bar stands up as a rail on the left edge with fewer tools.

## Component props an agent can't misuse

This is about the props of a shared component, not its pixels. Two failures repeat: too rigid (people fork it) or too configurable (thirty props). The fixes:

- **Composition over configuration.** A `Card` that takes `CardHeader` and `CardContent` as children beats a `Card` with `header={{ title }}` and `footer={{ actions }}` props, because children can be reordered, omitted and wrapped.
- **Compound components (A component split into parts that share state behind the scenes, like ,  and , so you arrange the pieces instead of passing a big config object)** share state through context: `Dialog`, `Dialog.Trigger` and `Dialog.Content` are separate parts you arrange.
- **The layer order:** `variant`, then `size`, then `className` as the escape hatch, then `asChild` to render as another element (a Link styled as a Button). Never style props like `paddingX`.
- **Restrict the escape hatch.** Leave `className` off components whose look *is* the system (Button, Badge, Input), or agents "fix" spacing with literals one override at a time. A new look is a new variant.
- **Name like the platform:** `disabled`, not `isDisabled`; `onOpenChange`; positive booleans.
- **Controlled and uncontrolled:** `defaultValue` for internal state, `value` plus `onChange` when the parent owns it.
- **Forward refs and spread the rest**, so `aria-*` attributes reach the DOM and tooltips can position. Default `type="button"`, never `submit`.

  **Before (avoid):**

`Button primary large rounded leftIcon={PlusIcon} iconSpacing={8}`: boolean soup and a prop for every icon detail

  **After (do this):**

`Button variant="primary" size="lg"`, with the icon and the words "New project" passed as children

**Parts review (paste into your agent):**

```text
Review these shared parts:
- Avatars: photo, then grapheme-cluster initials (Intl.Segmenter, first + last word, suffixes dropped), then an icon; onError falls back; flex-shrink: 0; object-fit: cover.
- Cards: no card nested in a card; space over dividers; hover actions always visible on touch; inner radius matches.
- Icons: one pack only (tell me if any mix), stroke matched to font weight, sizes 16/20/24, one meaning per icon.
- Component API: variant → size → className → asChild; no boolean soup or style props; forward refs, spread props, type="button" default; no className on Button/Badge/Input.
```

**Check yourself:** Which Button API will lead to the fewest invalid combinations?

- [ ] Boolean props: primary, secondary, large, small
- [x] Enums: variant="primary" size="lg"
- [ ] Style props: backgroundColor="blue" paddingX={16}
- [ ] One config object: config={{ look: 'primary', big: true }}

*Why:* Enums for variant and size allow exactly one choice each. Boolean soup permits primary and secondary at once, and style props are a second CSS system.

## In the skill

- https://school.agents.org.in/skill/components/cards-design.md
- https://school.agents.org.in/skill/components/icon-systems.md
- https://school.agents.org.in/skill/components/avatar-systems.md
- https://school.agents.org.in/skill/components/presence-signals.md
- https://school.agents.org.in/skill/components/drawing-toolbar.md
- https://school.agents.org.in/skill/components/component-api-design.md


---

# The thirty-second test

> Recognize the visual, layout and dashboard defaults that make a screen look generated, and know which ones to delete on sight.

Module: Spotting AI-default UI · Level: beginner · 8 min · https://school.agents.org.in/design/tells/the-thirty-second-test

Here is a landing page. Tap everything on it that a model would produce if nobody told it otherwise.

*[Interactive demo: Seven tells on one hero. Tap each to see why it reads as generated, and what to do instead.]*

You probably found most of them before reading a word of explanation. That is the point. Nobody can name the one pixel that makes a screen look generated; people recognize the **accumulation**. The question this module trains is simple:

**Could this have been generated in 30 seconds with a prompt?** If yes, fix it.

## Why generated UI looks the same

A model picks the most common option for every decision on the page, all at once. Fifty median choices do not average into a neutral design. They compound into a very recognizable one: the indigo gradient, the centred headline, the three cards. Each of these is an AI tell (A default choice that signals nobody made a choice: a purple gradient hero, three emoji feature cards, 'Unleash your potential'. One is harmless; several together make a screen look generated), a default that signals nobody made a choice.

This list is not anti-AI. It is anti-default. Plenty of generated interfaces are excellent, because the person prompting knew to specify these things. The tells appear when nobody specified anything.

## The visual tells

| Tell | Instead |
|---|---|
| Purple, indigo or fuchsia gradient on the hero | Monochrome plus one real accent, taken from the brand or the domain |
| Inter or SF Pro on a marketing page | One distinctive typeface with a reason |
| Gradient text (Text filled with a color gradient instead of one solid color (in CSS, background-clip: text). On a headline it is one of the most common generated-page devices) on the headline | Solid ink |
| Gradient buttons with a colored glow | A solid button |
| Glass (Frosted, see-through panels with a blur behind them. Fine over a photo or a busy canvas; over a plain background there is nothing to frost, so it is pure decoration) with nothing behind it | A plain surface, or real content showing through |
| Blurred orbs and blobs behind the hero | The product itself |
| The same radius on everything | A deliberate radius scale |
| Pure `#000` dark mode | A near-black with a hint of hue, like `#18181b` |
| The default shadow, `0 2px 8px rgba(0,0,0,0.1)` | Layered shadows at 4 to 6% opacity |

## Layout, content, motion, words

- **Layout.** Hero, then three feature cards, then an FAQ. A single centred column top to bottom. The same card for unrelated content. "Trusted by" with six gray logos.
- **Content.** Lorem ipsum, "100+ customers", "John Doe at Acme Inc", "Unleash your potential", stock photos. (Next lesson.)
- **Motion.** `fade-in-up` on every block, the same easing everywhere, a bouncy spring on a button you press all day, hover effects on a phone.
- **Microcopy.** "Get started" as the main button, "Awesome!" on success, "Oops!" on errors, "Click here to view…" in a tooltip.
- **Sound.** A click on every button, a whoosh on every transition, a recognizable stock ping.

> **In plain words.** **Glassmorphism**: frosted, see-through panels with a blur behind them. Fine over a photo or a busy canvas. Over a plain background there is nothing to frost, so it is pure decoration.

## What the numbers say

Leonxlnx's taste-skill measured 246 gallery sites and split them into strong and weak by a panel's scores. Some devices are far more common on the weak ones:

| Device | Strong sites | Weak sites |
|---|---|---|
| Icon-topped feature cards | 1% | 28% |
| Three or more tiny tracked-caps labels | 6% | 43% |
| Pill label above the heading | 5% | 26% |
| Gradient text | 2% | 13% |
| Emoji | 10% | 26% |
| Gradient fills | 22% | 52% |

Delete those on sight. But some popular devices are **not** tells: backdrop blur, an italic serif accent word and thin rules show no real difference, and monospace labels are actually more common on strong sites. Those are choices, not defects.

*(Most pages carry three to five tells. Removing them changes the feel more than any new effect would.)*

## Dashboards have their own tells

An app screen is not a landing page. The generated dashboard is a landing page with numbers on it. Find its tells, then flip to the product version:

*[Interactive demo: Icon circles, giant figures, sparklines, green errors. Then the same numbers on a real product screen.]*

taste-skill also measured 23 real product screens from 17 products, and they share none of the generated habits:

- **No icon circles and no sparklines** on headline numbers: zero of 23.
- **Often no tile at all.** Many put the numbers as bare text, or in one container split by thin dividers.
- **Modest sizes.** Values 16 to 40px (median 26), labels 12 to 16px in gray sentence case above the value. Nothing at 48px.
- **Color means good or bad, not up or down.** Errors, latency, cost and churn are better when they fall. A rise in errors in the same green as a rise in visits hides the screen's only bad news.
- **Zero is gray.** A change from nothing is "+4 (new)", not a green "+100%". No change is a gray dash, never a bold red "0.00%".
- **Tables:** numbers right-aligned in tabular figures (Digits that all take the same width, so numbers line up in columns and a ticking counter doesn't wiggle. Turned on with font-variant-numeric: tabular-nums), fixed decimals per metric, a sign on every change, no zebra rows, no uppercase headers.

The sample is small and hand-picked, so treat the sizes as a guide. The two color rules don't need a sample: they are logic, so enforce them first.

**Spot the tells (paste into your agent):**

```text
Audit this screen for AI-default tells. Check: purple/indigo gradients, gradient text, Inter on a marketing page, glass with nothing behind it, blurred orbs, pill labels above headings, icon-topped feature cards, emoji bullets, "Trusted by 10,000+" logo walls, the default shadow, one radius everywhere, fade-in-up on every block.
For app screens also: icon circles or sparklines on KPIs, values over 40px, deltas colored by direction instead of good/bad, "+100%" from zero, red 0%.
Return a deletion list first, then what to replace each survivor with. Don't add new effects.
```

**Check yourself:** On an ops dashboard, the error rate rose from 1.2% to 1.8%. How should the delta look?

- [ ] Green, because the number went up
- [x] Red, because a rise in errors is bad news
- [ ] Gray, because deltas should never be colored
- [ ] A green pill with a sparkline, to match the other tiles

*Why:* Color says good or bad, not up or down. Errors, latency, cost and churn are better when they fall, so a rise is red.

## In the skill

- https://school.agents.org.in/skill/anti-patterns/MOC-anti-patterns.md
- https://school.agents.org.in/skill/anti-patterns/ai-default-tells.md
- https://school.agents.org.in/skill/components/dashboard-tells.md


---

# Real content beats fake proof

> Replace placeholder text, rounded numbers, stock faces and generic testimonials with specific, verifiable content, or leave an honest gap.

Module: Spotting AI-default UI · Level: beginner · 6 min · https://school.agents.org.in/design/tells/real-content

A proof section from a product page. Six things on it would make a careful visitor trust it less. Find them.

*[Interactive demo: Rounded numbers, a quote about nothing, a placeholder name, a stock face, lorem ipsum, a stale version.]*

The fastest way to spot generated UI isn't the visual style. It's the content. You can restyle a page in an afternoon, but "John Doe, CEO at Acme Inc." gives the game away in a glance.

## Five kinds of fake

**Placeholder text.** Lorem ipsum (Scrambled Latin used as placeholder text while a layout is designed. Shipped in a real product, it means the copy (and the design) wasn't finished), "Section title", "Description goes here", "Coming soon". If you don't know what the real copy is, the design isn't ready.

**Rounded numbers.** "100+ customers", "1000+ projects", "Hundreds of integrations". They read as approximations because they are. Specific numbers feel real because they imply someone looked:

  **Before (avoid):**

100+ customers<br />50,000+ downloads<br />Hundreds of integrations

  **After (do this):**

127 customers<br />52,841 downloads this month<br />26 integrations

**Placeholder people.** John Doe, Jane Doe, Acme Corp, user@example.com, a gray stock headshot. They say "we couldn't be bothered to find a real example".

**Marketing copy that says nothing.** "Unleash your potential." "Transform your workflow." "The all-in-one solution for…" All correct English, all empty. If a line could appear on any product's homepage, it is worth nothing on yours.

**Stock photography.** The diverse team smiling at a laptop. A real screenshot of the product explains more than any photo; a real, imperfect team photo beats a posed one; often the best image is none.

*(Specific is not the same as long. A 4-word real headline beats a 12-word generic one.)*

## Testimonials that sound like a person

  **Before (avoid):**

"This product changed our company."<br />John Doe, CEO

  **After (do this):**

"We went from 12 deploys a week to 47 in the first month. Our SRE team finally took a Friday off."<br />Sarah Chen, Engineering Manager

The good one is specific (12 to 47), attributable (a real name and role), and has a voice: the small aside about Friday is something only a person would add. Real quotes have tics and half-thoughts.

**Never fake a testimonial.** Use opted-in quotes from real customers, fictional examples clearly marked as examples, or your own team. Privacy rules like GDPR may limit what real customer data you can show, which is one more reason to ask.

## Signs that someone is home

Real products show evidence of ongoing attention, and their absence reads as abandonment:

- **Timestamps** on changelog entries.
- **Real version numbers**, like `v4.7.2`, not `v1.0` forever.
- **Recent dates** on "last updated".
- **Live proof**: "Used by 847 customers as of this week".

## Where it matters, and where it doesn't

It matters most on landing pages, empty states ("Add your first project" beats "No data"), error states ("Couldn't reach our API. Trying again in 4s…" beats "Error occurred"), about and team pages, changelogs, and demo environments, where realistic data trains the team to think about real cases.

It matters less in an internal admin tool on a deadline, or an early prototype where the copy lands later. Just don't ship the prototype as the product.

> **An honest gap beats fake proof.** If you have no customers yet, cut the "Trusted by" section. A launch page with a real screenshot and no social proof reads as new. A launch page with "10,000+ teams" and gray logos reads as fake.

**Content authenticity pass (paste into your agent):**

```text
Go through this page and list every piece of inauthentic content:
- lorem ipsum, "Coming soon", "Section title"
- rounded numbers ("100+", "Hundreds of") → ask me for the real count, or remove
- placeholder names and companies (John Doe, Acme), stock headshots
- testimonials with no specifics → ask me for real, opted-in quotes; never invent one
- marketing lines that could appear on any product's homepage → rewrite with a concrete claim
- missing dates and versions on changelogs and "last updated"
Don't fabricate numbers or quotes to fill a gap; mark the gap and leave it for me.
```

**Check yourself:** Your product launched last week with 9 paying customers. What should the social proof section say?

- [ ] Trusted by 100+ teams
- [ ] Loved by teams everywhere, with five gray logos
- [x] Either a real line, like a named quote from one of the 9, or no section at all
- [ ] Join thousands of happy customers

*Why:* Rounded or invented proof is a tell and erodes trust. Use something real and specific, or leave an honest gap.

## In the skill

- https://school.agents.org.in/skill/anti-patterns/content-authenticity.md


---

# Copy that sounds generated

> Strip the statistical habits of generated prose (model vocabulary, not-just-X, the rule of three, em dashes, puffery, chatbot residue) and put a real voice back.

Module: Spotting AI-default UI · Level: beginner · 7 min · https://school.agents.org.in/design/tells/copy-tells

A product announcement, the kind an agent drafts in a second. Tap each phrase that gives it away, then press **Rewritten**.

*[Interactive demo: Eight copy tells in two sentences, then the same news said by a person.]*

No single phrase there is wrong. "Additionally" is a real word; em dashes are real punctuation. But together they form a watermark. Each copy tell (A habit of generated writing: words like delve and leverage, 'not just X, but Y', lists of three, em dashes, puffery, 'I hope this helps!'. Each is fine alone; together they are a watermark) is defensible alone and damning in a crowd.

The pass goes in order: words, then sentences, then punctuation, then claims. Then you put a voice back, because clean and voiceless is its own tell.

## Words

| Generated | Write |
|---|---|
| delve into | look at |
| crucial, pivotal | important, or name the consequence |
| leverage, utilize | use |
| facilitate | help |
| enhance | improve, or say the specific change |
| showcase, underscore | show |
| landscape, tapestry | field, mix, or delete the sentence |
| additionally | also |
| in the event that | if |
| serves as, boasts | is, has |

Delete filler on sight: "in order to", "it is important to note that". Cut adverbs or fix the verb ("runs quickly" becomes the number). Swap promotional adjectives (seamless, robust, stunning, vibrant) for a plain description and let the reader supply the adjective. Metaphor jargon (north star, flywheel, paradigm) costs credibility when a plain word exists.

## Sentences

- **"Not just X, but Y."** State Y. If Y can't stand without the runway, Y is the problem.
- **Break the rule of three (The habit of listing exactly three things ('fast, simple and secure') because three sounds complete, whether or not there are three real items).** Count the real items; write two, or five.
- **Kill false ranges** like "from startups to enterprises". Name the one that matters.
- **Repeat the word.** Cycling synonyms for one thing is a school-essay habit; the same word helps the reader track it.
- **Name the actor.** Passive voice hides who does what.
- **Hedge once at most.** "Could potentially possibly" is a model avoiding commitment.

## Punctuation and formatting

No em dashes in product prose: use a period or a comma. A colon introduces a list; it does not glue two clauses. Bold is for scanning, not emphasis. **The bold-label-plus-colon list that restates its own line is the strongest formatting tell there is.** Sentence-case headings, no decorative emoji.

*(This lesson has no em dashes in it. You probably didn't miss them.)*

## Claims: the two tests

Puffery (Big promotional words that claim a lot and can't be checked: 'seamless', 'robust', 'a testament to', 'revolutionary'. Replace them with what actually happens) ("a testament to", "sets the stage for") gets replaced by what happened. Delete the trailing "-ing" clause that bolts a conclusion onto a fact ("…, ensuring reliability"). No generic conclusions ("exciting times ahead") and no chatbot residue (Lines the model wrote to the person prompting it that got pasted into the product: 'I hope this helps!', 'Great question!', 'Certainly! Here is…') ("I hope this helps!", "Great question!").

Then every product sentence faces two tests:

  **Step: Can it be restated as a concrete instruction, fact or number?.** "Saves you time" fails. "Drafts the invoice from your timer" passes.

  **Step: Could it appear unchanged in another product's docs?.** "Built for teams that ship" could be anyone's. If it could, cut it.

Fail either, cut the sentence.

  **Before (avoid):**

Our robust, seamless platform empowers teams to unlock their full potential, ensuring every project succeeds.

  **After (do this):**

Billable turns your tracked hours into an invoice. Most people send their first one in under 4 minutes.

## Putting a voice back

Stripping alone overshoots: a sentence reduced to nouns reads as generated too. The target is a sentence a specific person would say about this specific product.

- Have a reaction. Real reactions are mixed ("fast, though the setup is fiddly").
- Vary the rhythm. A short sentence after two long ones.
- Use "I" or "we" when it fits.
- Be specific over evocative: the specific image can't be produced by averaging.
- Let a little mess in: a tangent, an admitted uncertainty.

This applies to every text a person reads: button labels, error messages, empty states, marketing pages, docs, release notes, the README.

**Copy tells pass (paste into your agent):**

```text
Edit this text to remove generated-prose habits, in this order:
1. Words: delve, crucial, pivotal, leverage, utilize, facilitate, enhance, showcase, landscape, additionally, seamless, robust → plain words or delete.
2. Sentences: no "not just X but Y", no rule of three, no false ranges, name the actor, one hedge max.
3. Punctuation: no em dashes, no bold-label-colon lists, sentence-case headings, no emoji.
4. Claims: cut puffery, trailing "-ing" conclusions, generic endings, chatbot residue.
Every remaining sentence must be a concrete fact, instruction or number, and must not fit another product unchanged. Keep a human voice; don't strip it to nouns.
```

**Check yourself:** Which sentence passes both tests (concrete, and not pasteable into another product)?

- [ ] Our platform seamlessly enhances your workflow.
- [ ] Built for modern teams that move fast.
- [x] Flowly flags the two tasks blocking someone else every Monday at 9.
- [ ] Flowly isn't just a to-do app, it's a productivity revolution.

*Why:* It states a specific behavior with a time and couldn't describe another product unchanged. The others are puffery, a false range of 'modern teams', or the not-just-X construction.

## In the skill

- https://school.agents.org.in/skill/anti-patterns/copy-tells.md


---

# Code tells and leftovers

> Clean the over-defended, over-commented residue agents leave in UI code, and catch the dev tools, routes, logs and secrets that must never ship.

Module: Spotting AI-default UI · Level: intermediate · 8 min · https://school.agents.org.in/design/tells/code-and-leaks

An agent wrote this component. It compiles, the tests pass, and the page works. Tap every line that gives away who wrote it.

*[Interactive demo: Seven code tells in sixteen lines. The default fix for almost all of them is deletion.]*

Nobody flags generated code for one bad line. They recognize the accumulation: a comment narrating every statement, a try-catch around code that can't throw, a fallback to an empty array, a helper that already exists two files over.

There is a reason. Models learned from places where code that *runs in isolation* wins, so they over-defend, over-comment and over-scaffold. Everything that keeps a snippet alive on its own makes it noise inside a real codebase. These are code tells (A habit of generated code: comments that narrate each line, try-catch around code that can't fail, fallbacks to empty arrays, 'as any', names like fetchDataV2. It runs, but it hides intent and real failures).

## The pass, in order

Scan for comments, then defense, then types, names, structure, logging, tests and scope. **Delete first**; rewrite only where behavior must survive.

**Comments.** One test: does it say something the code can't? A constraint, a unit, a tradeoff, the bug it works around: keep. Narration (`// increment the counter`), notes to a reviewer (`// Updated to use the new API`), section dividers, commented-out code: delete.

**Defensive theater.**

- Null checks against states the types already rule out. Fix the type once.
- Catch, log, rethrow. Empty catches. `catch (e) { console.error(e) }` that carries on as if nothing happened.
- **The silent fallback (Code that quietly substitutes a default when something fails, like catching a failed request and returning an empty list. The page shows 'No invoices' when the server is actually down) is the most dangerous item on the list.** `catch { return [] }` turns a broken API into "No invoices yet". Tests stay green while the main path is broken.

When removing a cast, catch or fallback forces a choice, choose the **loud failure** (a thrown error, a failed build, a visible error state) over the quiet wrong answer.

**Types and names.** `as any`, `@ts-ignore` and a `!` that silences instead of fixes: the compiler error was information. `data`, `result`, `item` say nothing; `unpaidInvoices` does. `fetchDataV2`, `EnhancedButton`, `utils-new.ts`: replace the old thing and take its name.

**Structure.** One-call wrappers, an options object with one caller, an interface with one implementation, a duplicate helper the model didn't search for, `TODO: implement error handling`.

**Logs and tests.** `console.log("here")` and `✅ Successfully initialized!` are debug leftovers. Never delete, skip or weaken a test to make it pass, and be wary of a test that recomputes the implementation's own math: it passes when both are wrong.

  **Before (avoid):**

```ts
// Fetch the data
fetch("/api/invoices")
  .then((res) => res.json())
  .then((result) =>
    setData(result))
  .catch(() => setData([]));
```

  **After (do this):**

```ts
const res =
  await fetch("/api/invoices");
if (!res.ok)
  throw new Error(
    `Invoices ${res.status}`);
setInvoices(await res.json());
```

*(Match the room. A codebase that is dense with comments by convention isn't slop; rewriting it to your taste is a drive-by change.)*

Keep the diff to what the task needed, and propose unrelated cleanup separately.

## Leftovers and leaks

The second half is worse, because none of it shows up as a bug. The app works. It just ships an inspector panel to every visitor, lets search engines index `/lab`, and prints the session to the console.

*[Interactive demo: Six things that work fine and must not ship.]*

The rule: **find everything, gate what you can't prove is dead, delete only what is provably dead.**

| Leftover | Why it blocks | Fix |
|---|---|---|
| Dev panels (Leva, Tweakpane, dat.gui), devtools, grid overlays | A hidden panel still ships its bytes and feeds its defaults into the UI | Gate on development and load with a dynamic import; tuned values go in a config file |
| Scaffolding routes (A page made for development, like /lab, /test or /playground. In production it gets indexed by search engines, often skips login, and shows unfinished work): `/lab`, `/test`, `/playground`, `/debug` | Indexed, often skip auth, show unfinished work | Not-found in production; robots.txt only as a second layer |
| `console.log` of a user, session or payment response | It is a data leak | Remove it and say so; keep real `console.error` in error boundaries |
| A secret in a public env var (A configuration value whose name starts with a prefix like NEXT_PUBLIC_ or VITE_, which gets bundled into the code every visitor downloads. A secret key there is readable by anyone) | `NEXT_PUBLIC_` and `VITE_` values are bundled into the browser | Report the **name**, never the value, and put "rotate this now" at the top |
| `localhost` or staging hosts in source | Breaks or misroutes in production | Read from configuration |
| A TODO on auth, payment, permission or deletion | "check ownership here" may mean anyone can delete anything | Quote it verbatim; it is probably the most important finding |

Also hunt `.only(` and `.skip(` in tests, fresh `@ts-ignore`, and env files tracked by git.

> **Two fixes that aren't.** A hidden panel and a robots.txt line both look like fixes. Neither is: the first still drives values and ships the library, and the second is a polite request that leaves the route open to anyone with the URL.

**Code tells and leaks pass (paste into your agent):**

```text
Review this diff for code tells and leftovers. Default fix is deletion.
- Delete narrating comments, reviewer notes, commented-out code, console.log debug lines.
- Remove catch-log-rethrow, empty catches and silent fallbacks (catch → [] or ?? default on a failed fetch); fail loudly or show an error state. Check response.ok.
- Replace any/as unknown/@ts-ignore with real types; rename data/result/item and V2 names.
- Hunt: dev panels not behind a dynamic import + dev gate, /lab /test /playground routes, logs of user/session data, secret-looking NEXT_PUBLIC_/VITE_ names (report names only), localhost URLs, TODOs on auth/payment/delete paths (quote verbatim).
Match the repo's existing style; keep the diff to the task.
```

**Check yourself:** Which line is the most dangerous to leave in?

- [ ] // Fetch the invoices
- [ ] const [data, setData] = useState([])
- [x] .catch(() => setData([]))
- [ ] import { useEffect } from "react"

*Why:* A silent fallback turns a broken API into an empty list. The page lies ('No invoices') and tests stay green while the real path is broken.

## In the skill

- https://school.agents.org.in/skill/anti-patterns/code-tells.md
- https://school.agents.org.in/skill/anti-patterns/leftovers-and-leaks.md


---

# The unslop pass

> Run the full procedure on a generated screen (squint, subtract, sweep, put a point of view back) and critique the render, not the code, in at most three rounds.

Module: Spotting AI-default UI · Level: intermediate · 9 min · https://school.agents.org.in/design/tells/the-unslop-pass

Step through this hero from left to right. Each step changes one layer of decisions.

*[Interactive demo: One generated hero, unslopped in six steps. Read the line under it at each step.]*

The previous lessons were a catalog: what the tells are. This one is the procedure, the unslop pass (The procedure for de-generating a screen: squint, delete what isn't needed, sweep color, type, layout, decoration, icons, copy and motion, then put one real point of view back). It has two halves, and the second is not optional: **a screen scrubbed of tells with no opinion of its own is still generic, just quieter.**

## The pass

  **Step: Squint.** Could this screen belong to any of ten thousand products? Find what ties it to *this* one. If the answer is "the logo", everything else is up for review.

  **Step: Subtract before you restyle.** Walk every element (icon, line, badge, caption, container, word) and ask what breaks if it's gone. If nothing, delete it. Then walk again: deletions expose each other.

  **Step: Sweep in order.** Color, typography, layout, decoration, icons, copy, motion. Color first because it is the loudest: a page survives a common layout, not indigo-to-purple.

  **Step: Put a point of view back.** One real palette, one type decision, the actual product, a signature (below).

  **Step: Final read.** "What here would make a designer suspect a model built this?" Fix it. Repeat until nothing.

"It adds visual interest" and "it fills the space" are exactly the reasons the model put something there. Deletion is free, can't introduce a new tell, and gives every survivor hierarchy for nothing.

## What the sweep catches

| Layer | Delete | Replace with |
|---|---|---|
| Color | Unchosen indigo and violet; gradients with no job; dark-and-neon as a "premium" costume | A palette from the domain, the brand or the content |
| Type | Gradient text; `Inter font-black tracking-tight`, centred | One distinctive type decision |
| Layout | Pill, centred headline, two buttons, orb; three icon cards; a bento that's three cards with better kerning | One feature full-width with a real screenshot; small features as a list; unequal features, unequal space |
| Decoration | Glass with nothing behind it; blobs; border **and** shadow **and** ring on one card; `rounded-2xl` everywhere | A plain surface, a radius scale |
| Icons | Emoji as bullets; a sparkle on every AI feature | One real icon set, or none |
| Proof | "Trusted by 10,000+" on an unlaunched product; stock-avatar quotes | Real numbers and named quotes, or nothing |
| Copy | A headline a competitor could paste | One concrete claim |
| Motion | `fade-in-up` on every block; `hover:scale-105` on things you can't click; pulsing blobs | Motion only where it explains something |

## Looking designed by someone

- **Derive the palette from something real**: the domain (ledger green for invoices), the brand, the content.
- **One distinctive type decision.** One is enough; three is a costume party.
- **Show the actual product**: a real screenshot, real data, a working embed.
- **Let content break symmetry**: an odd number of features, one oversized item, a section that is just a sentence.
- **Repeat a signature**: one odd element used consistently beats ten borrowed ones used once.
- **Restraint is a feature.** The confident version has *fewer* effects. Adding "personality effects" on top is re-slopping with extra steps.

The pass ends when you can name the one choice a competitor couldn't copy and paste. If nothing on the screen answers, you stopped halfway.

## Critique the render, not the code

An agent that reviews the page it just wrote reads its own intentions back: the class says `hero`, so it sees a hero. Screenshot critique (Reviewing a page from screenshots of what it actually renders (at desktop, tablet and phone widths) instead of reading its code, one focused pass at a time, in at most three rounds) fixes that. Take screenshots at 1440, 768 and 390px wide, first screen and full page, and look at them *before* the code.

Then make single-purpose passes, one at a time. Here is one of them, the edges pass:

*[Interactive demo: Follow the left edge of every text block down the page and count the distinct positions.]*

  **Step: One second, then five.** Glance away: there should be one answer (a word, an image, an object), not "a website". Then five seconds as a stranger: what is this, who is it for, what do I do next?

  **Step: Squint, cover the logo, scroll.** Does the idea reach the pricing and footer, or turn into stock blocks after the first screen?

  **Step: Edges.** Two or three left edges reused on purpose is a composition. Six means things were placed one at a time.

  **Step: Frame and sameness.** Equal margins, no half-empty sections. Write one word per section's layout (split, list, grid, statement); the same word three times is the template showing.

  **Step: Phone.** Is 390px its own composition, or the desktop squeezed?

  **Step: Fullness and collisions.** Does every section deliver its information at a readable size? Is anything overlapping a headline or button?

## Write it down, fix in order, stop at three

Every round gets the same short write-up:

```text
ROUND 1
Seen in one second: a purple blur
Idea (one sentence): invoices from tracked time
Layout words: hero=centred / features=grid / proof=logos / faq=list
Top three problems, most visible first:
1. hero: headline any product could use. Fix: the one-click invoice claim
2. features: three equal icon cards. Fix: one screenshot full-width, rest as a list
3. proof: "10,000+" with gray logos. Fix: cut until we have a real number
```

Name findings the way a designer talks to a colleague: "the paragraph starts 40px left of the headline". "Could be more polished" is not a finding.

Fix in this order: **idea, hierarchy, composition, type, imagery, color, detail, motion**. Later items can't rescue earlier ones; no amount of kerning saves a page with no idea. Run at most three rounds, and if the last still falls short, say so plainly instead of rounding up.

> **Subtraction can overshoot.** In taste-skill's blind tests, a richer page full of template devices beat a cleaner page that was thin. Removing tells is half the job. When a deletion leaves a gap, enlarge what remains or close the gap. Never refill it with an ornament.

*(The pass is not "make it minimal". It's "make it someone's".)*

**Unslop and critique (paste into your agent):**

```text
Run an unslop pass on this page.
1. Screenshot it at 1440, 768 and 390 (first screen + full page) and critique the render before reading code.
2. Subtract: list every element whose removal breaks nothing; delete them; repeat once.
3. Sweep color → type → layout → decoration → icons → copy → motion against the usual tells.
4. Put a point of view back: a palette from the domain, one type decision, a real product screenshot, one repeated signature.
Write each round as: seen in one second / idea in one sentence / layout word per section / top three problems with fixes. Fix in order idea → hierarchy → composition → type → imagery → color → detail → motion. Stop after three rounds and report what is still weak.
```

**Check yourself:** You deleted the orbs, pill and logo wall, and the hero now feels thin. What next?

- [ ] Add a subtle animated gradient to restore interest
- [ ] Put the deleted orbs back at lower opacity
- [x] Enlarge what remains or show the real product in the space, without adding ornament
- [ ] Stop; thin is always better than busy

*Why:* Subtraction is half the pass. Fill gaps with substance (a real screenshot, a bigger headline), never with a new ornament, which would be re-slopping.

## In the skill

- https://school.agents.org.in/skill/anti-patterns/unslop-pass.md
- https://school.agents.org.in/skill/anti-patterns/screenshot-critique.md


---

# Why fake UI looks fake

> Name the five ways a product illustration fails, and set a word budget so your agent's demo reads as a real product instead of a generated one.

Module: Product demos · Level: beginner · 7 min · https://school.agents.org.in/media/faux-ui/why-fake-ui-looks-fake

Scroll any good product page and you will see little pictures of the app: an inbox with one conversation highlighted, a cursor assigning it to someone, a small card saying it worked. None of those are screenshots. They are drawn on purpose, and they are called faux UI (A drawn picture of a product's interface, made for a landing page, feature card or docs page. It looks like the real app but is simplified to tell one moment).

Ask an agent for one and you usually get something that looks *almost* right and somehow generated. Here are the five ways it goes wrong, each next to its fix. Tap through them.

*[Interactive demo: Same story, same kit. Only the decision changes.]*

## One sentence to hold on to

Everything in this module hangs on one line from Benji Taylor, who built Stageshow (Benji Taylor's open-source kit and skill for building faux product UI in React: real-looking windows, phones and a scripted cursor, plus examples and a filming script to check the result), the kit every demo in this course is made with:

> **In plain words.** It reads as a real product, simplified to what the viewer needs, framed with intent, and every motion has a cause.

A product shot (An illustration of one moment in a product, framed like a photo: cropped to where the story happens, not a whole screenshot pasted into a box) is not the product. It is an illustration of one moment in it. Your job is to decide what that moment is, and leave everything else quiet.

## Too much, and its opposite

Left alone, an agent fills every row. It knows what a support inbox contains, so it writes names, subjects, counts, badges and timestamps everywhere. **The more readable detail, the more generated the picture looks.**

The over-correction is just as bad: two gray blobs and a button. That is a wireframe, and it no longer looks like any product at all.

The fix sits in between, and it has a number. Count the real strings: the title, the thing being acted on, what gets typed, the result. Usually **3 to 7**. That is your word budget (The handful of real words (usually 3 to 7) a product illustration is allowed. Everything else becomes a placeholder bar, so the eye goes straight to what matters). Everything else becomes a placeholder bar (A short gray line that stands in for text nobody needs to read in a product illustration. Its width varies row to row and it sits at the height of the text it replaces), a short gray line standing in for text nobody needs to read.

*[Interactive demo: One scene written once. The fidelity setting decides what becomes a bar.]*

Notice that the screen stays *full* at every setting: a sidebar, a header, six rows. Only the words change. Simplify the words, not the context.

*(A word budget is not an element budget. A calendar still needs a populated week.)*

## The small rules that make it read as real

Most of the craft is in numbers you can hand straight to an agent:

- **Words where the eye lands, bars at the edges.** Gray bars on the subject read unfinished. Where the story touches a list, two or three real rows and at most as many bars.
- **Bars vary in width** (60, 45, 70, 52) and sit at the height of the text they replace, so a row keeps its rhythm.
- **Real type sizes**, 11 to 15px, one family. Nothing under 10px as it renders on a phone.
- **A 12px inset** between any words and the edge of their card, row or window. Under 8px looks like a mistake.
- **A title sits 4 to 6px above its subtitle.** The breathing room comes from the row's padding, not from that gap.
- **Keep the domain's signature content.** Album art keeps a music app a music app. Swap it for gray and it becomes a generic list.

  **Before (avoid):**

Every row: name, subject, ticket number, SLA badge, time. A header with three stats. A satellite with six lines.

  **After (do this):**

The title, three real subjects, and "Assigned to Maya". Bars everywhere else.

## Icons, logos and the color of AI

Icons come from one set, never drawn by hand: one size (14 to 16px), one stroke. Emoji are content, never icons.

A real company carries its real logo, or its name as plain text. A letter in a colored square next to a real company's name is an invented logo, and readers notice.

And no sparkles, wands or purple as "the color of AI". Show what the AI *does*: the answer streaming in, the row it filled. A generic sparkle is decoration.

## Faking it honestly

A faux app lives inside someone else's page, so a few rules keep it polite:

- Its title is a plain `div`, never a heading (`h1` to `h6`), so it does not break the page's outline.
- The whole stage gets `role="img"` and an `aria-label` that says what it shows, like "Assigning a conversation to Maya". Its fake buttons stay out of the tab order.
- Anything that loops for longer than 5 seconds gets a visible pause button. That is a WCAG rule, and demos are not exempt.

> **Stageshow or no Stageshow.** If your project uses React, install Stageshow and let it own the build: it ships the kit, about forty gold examples and a filming script. In any other stack, the rules in this module still apply; you build them in your own code.

## Tell your agent

**Brief a product illustration (paste into your agent):**

```text
Build a faux UI feature card (400 × 300) of our support inbox: one conversation being assigned to Maya.
- Word budget: at most 6 real strings (the title, the selected conversation and its two neighbours, "Assigned to Maya"). Everything else is a placeholder bar, widths varying row to row.
- Keep the screen full: sidebar, header, six rows. Simplify the words, not the context.
- Type 11–15px, one family, 12px inset from every edge.
- Icons from one set, no sparkles, no purple, no invented logos.
- The faux title is a div, the stage has role="img" and an aria-label.
If Stageshow is installed, start from its closest gold example.
```

**Check yourself:** Your agent's inbox demo has a real name, subject, ticket number and time in all ten rows. What is the fix?

- [ ] Delete the inbox and show just the result card
- [x] Keep the ten rows, keep real words only on the 2–3 rows the story touches, and turn the rest into placeholder bars
- [ ] Make every row a gray bar so nothing distracts
- [ ] Shrink the font so it all fits

*Why:* Too much detail reads as generated, but stripping everything gives a wireframe. Keep the full structure and spend a small word budget where the story is.

## In the skill

- https://school.agents.org.in/skill/faux-ui/MOC-faux-ui.md
- https://school.agents.org.in/skill/faux-ui/faux-ui-restraint.md
- https://school.agents.org.in/skill/faux-ui/stageshow-reconciliation.md


---

# Framing the shot

> Decide the size, crop and satellite of a product illustration before anything is built, so it reads as a product shot and not a screenshot in a box.

Module: Product demos · Level: beginner · 7 min · https://school.agents.org.in/media/faux-ui/framing-the-shot

Drag the slider. At zero, the inbox sits whole in the middle of a tinted rectangle, small, with even margins all round. Around a quarter, it suddenly looks like a photo of a real product.

*[Interactive demo: How much of the window runs off the stage decides what it reads as.]*

That is framing, and it is decided **before** any of the screen is built. A crop needs nothing past the crop, and the crop sets how big the type has to be.

## Size first

Design at the size the picture will be seen, and show it at that size. Rough starting points:

| Where it sits | Design size |
|---|---|
| Hero beside a block of copy | about 720 × 340 |
| Full-width homepage hero | 900 to 1100 wide |
| Feature card | about 400 × 300 |
| Anything with a demo in it | never under about 320 wide |

Small formats crop harder. A 400px card holds a *third* of a dashboard at full size, not all of it at a third of the size. Shrinking the whole window is how the "boxed" failure happens.

*(A lone card in a 680px column wants 500 to 560 wide, so its text reads at a real size.)*

## The rules of the crop

Benji Taylor's framing rules, condensed. Each one is a decision you can check by eye:

  **Step: One hero, at most two satellites.** The main window is the hero. A satellite (A smaller second object in a product shot (a receipt, a phone, a detail card) that carries the point. It usually straddles the main window's edge, about half outside) is a smaller second object that carries the point: a receipt, a phone, a detail card. Two satellites are two different kinds and sizes, never two equal cards stacked on one edge.

  **Step: Bleed 10 to 40 percent.** Let the hero bleed (Letting a window run off the edge of its frame on purpose, so it reads as a crop of a bigger product rather than a small picture floating in space) off the stage. It fills at least two thirds of the width and runs off an edge by a deliberate fraction, not a sliver.

  **Step: Crop only filler.** The action, the result, what the cursor touches and every real word stay whole. A placeholder button the crop would slice is removed, not sliced.

  **Step: Satellites straddle.** A satellite sits about half outside the hero's edge, over chrome or margin, never over the content the story is about.

  **Step: One gutter.** Pick one gutter (The one distance between the main window and the frame's edge. Every other edge in the picture either sits that same distance in, or runs well past the edge), say 24px, from the hero's inset. Every other edge sits a full gutter in or runs well past. Crossing an edge by a few pixels reads as a mistake.

Try the satellite options in the demo above. "Over the content" hides the very rows the story is about; that is why the rule exists.

## The page is the ground

On a white page, the stage is transparent. No plate (A tinted rectangle behind a window that makes it look like it is floating on a mat. On a white or dark page it is almost always a mistake: the page is the ground), the tinted mat a window floats on, behind the device. A whole window centered with air on four sides floats; bleed it off two edges or fill the slot instead.

  **Before (avoid):**

The whole app window at 55%, centered on a pale blue rounded rectangle, 40px of margin on every side. Its text renders at 7px.

  **After (do this):**

The window pinned 24px from the top, running a quarter off the right and bottom. A detail card straddles its left edge. Text at 11 to 13px.

A few more framing rules worth handing to an agent:

- **A window keeps its device's shape.** Whole, it is wider than tall (1.25:1 or more). Fix a bad fit by cropping, never by squashing.
- **What the story opens stays in frame.** A menu or popover needs a gutter of room on every frame, so place the trigger where it has space.
- **No dead corners**: no empty quarter of the stage, at rest or anywhere in the loop.
- **Start flat and frontal.** Depth comes from shadow, overlap and scale. A tilt has to beat the straight version at its real size.

## A layout to start from

You rarely need to invent a composition. A few recipes cover most briefs:

| Layout | What it looks like | Use it for |
|---|---|---|
| Corner bleed | Inset about 10% top and left, 15 to 35% past right and bottom | A hero beside copy |
| Pinned, bottom bleed | Inset on three sides, off the bottom | A feature card or bento tile |
| Satellite focus | Window cropped hard, a large satellite carries the story | A notification, receipt or diff |
| Device pair | Desktop window, a phone straddling its right edge | A story across devices |
| Close-up | One control at 1.3 to 1.6×, its chrome included | A card about one setting |
| Tilted stack | Three cards at −6°, 4°, −2°, only the top one with words | Paper things: receipts, tickets |

Agents told to "run the window off the edge" usually nudge it a few pixels past, the worst of both. **Copy an example that does it properly, then check the final crop.**

## Phones get their own design

A demo that might sit in a phone column ships a compact design (A second composition of a demo for a phone column (about 358px wide): cropped closer, satellite dropped or stacked, text still at real size. Not the desktop picture shrunk) at about 358px wide: cropped closer, the satellite dropped or stacked, the action and result still whole. A desktop picture scaled down passes every overflow check while its 13px text renders at 8px.

Phones inside a shot have rules too. A satellite phone starts at 40% of the window's visible width and at least 220px; let it run off rather than shrink it. A notification stays at true iOS size, and you size the phone around the banner, never the other way.

**Frame it before you build it (paste into your agent):**

```text
Before writing any code for this feature card, give me the framing plan:
- Design size (it sits in a 400px card, shown at 1:1).
- Layout: pinned, bottom bleed. The window runs 15–35% off the right and bottom.
- One satellite (the "Assigned to Maya" card) straddling the window's left edge, over the sidebar, never over the rows.
- One 24px gutter, measured in the final crop. No plate behind the window.
- Crop only filler: the selected row, the assignee and every real word stay whole.
- A compact design at 358 wide, cropped closer, text still 11–13px.
Then build it and show me both widths.
```

**Check yourself:** Your agent's hero shows the whole app window, centered on a pale tinted rectangle with 40px of margin on every side. What is the best fix?

- [ ] Add a stronger shadow so it pops off the plate
- [x] Remove the plate, pin the window near the top-left and let 15–35% of it run off the right and bottom
- [ ] Tilt the window 8 degrees for energy
- [ ] Shrink the window further so the margins look more generous

*Why:* A whole window floating on a plate is the boxed failure. The page is the ground; a deliberate bleed and a crop to the panel make it read as a product shot.

## In the skill

- https://school.agents.org.in/skill/faux-ui/product-shot-framing.md


---

# Story, cursor and loops

> Script an animated product demo with three to five beats, a cursor that moves like a hand, a poster frame that works alone, and a loop that resets for a reason.

Module: Product demos · Level: intermediate · 8 min · https://school.agents.org.in/media/faux-ui/story-and-cursor

Watch the pointer. It travels to the empty assignee, presses, the menu opens from that button, it picks Maya, and the result lands twice: her avatar on the row, a small toast at the bottom. Then everything holds still long enough to read.

*[Interactive demo: Four beats and a payoff that holds. Replay is the reset, so it has a cause.]*

An animated demo is a short film the viewer did not ask for. Every move needs a reason, and the whole thing has to survive being watched twice.

## Pick the story's shape

What is the product valued for? That decides the shape:

- **How it feels to use.** The gesture is the story: a cursor does one satisfying thing and the result appears under its hand. The demo above is this kind.
- **A flow in an app.** A few screens told as one continuous object, the payoff on the last.
- **What the system does.** The consequence lands somewhere else, and the link is drawn: a value travelling along a line, a card flying into a list. Usually no cursor at all.

And one tie-breaker from Benji Taylor: between a richer story and a calmer, better-framed one, choose the frame.

## Beats and the payoff

- **10 to 16 seconds per loop, 3 to 5 beats**, building to a payoff (The result an animated demo builds to: the panel that opens, the state that changes. It arrives whole, stays visible at the shown size and holds for about 2 seconds). One gesture and one response can be enough.
- **Open on a finished screen** that works as a still. The first real content is there before the first beat.
- **The payoff arrives whole** and is visible at the shown size: a panel, a state change, a card coming forward. Never a blank surface for a quarter second.
- **The payoff holds about 2 seconds**, and nothing sits still for more than 3.

> **The frame most people see.** The poster frame (The frame shown before a video plays, as its thumbnail. Some films bake 1 to 4 frames of the finished title at the very start so the thumbnail names the product) is the one still shown in a thumbnail, a screenshot, or to anyone who has reduced motion turned on. Set it first, to the beat that tells the story on its own, with the result left on screen. Never a frame caught mid-change.

## Resets that have a reason

A loop has to get back to the start. A good loop reset (How an animated demo gets back to its start. A good reset has a visible cause (undo, close, clear) or happens out of sight; data never rewinds in view) has a seen cause (an undo with its keys shown, a Clear button, a panel closing), or it happens while hidden. **Data never rewinds in view.** A number counting backwards on screen looks like a bug, because it is one.

Also: no product shrinks an app back into its notification. Leave the way the real product leaves.

## Two clocks: story and product

Storytelling motion is unhurried. Taylor's kit moves things across the stage on `cubic-bezier(0.45, 0, 0.1, 1)` and brings new things in over 0.8 to 1 second on `cubic-bezier(0.25, 0.8, 0.25, 1)`.

But the product's own feedback, a button press, a menu opening, keeps the product's real speed, under 300ms. That menu in the demo opens in about 240ms. Only the story slows down; the UI never does.

| Motion in the kit | Value |
|---|---|
| A card flying to its slot | 750ms on a soft arc, no bounce on landing |
| A new row opening | height over 0.42s, content fading in with it |
| A popover | scales over 0.24s from the side nearest its trigger |
| A stagger within a scene | 40 to 80ms apart, in reading order |

## A cursor that moves like a hand

The cursor is a character. Drag the slider, then move both:

*[Interactive demo: Short hops are quick, long trips don't drag. A fixed duration makes every hop crawl.]*

The rules for the pointer, with the numbers from Stageshow's code:

- **Travel is timed by distance** on a shallow arc: `clamp(260 + 0.9 × distance, 300, 650)` ms, bowing sideways by up to 24px.
- **A press reads**: a short pause on arrival, the pointer dips to 0.9 for a moment, and the state changes after the click completes. The pressed control shows its own press.
- **It never covers the words it clicks.** Aim inside the target, off its text, about 85% across and 60% down.
- **It rests on empty surface** near its next move, never parked on a control, and fades after about 4 seconds still.
- **Its shape comes from what it crosses**: an I-beam over text, a hand on a link, an open hand over something draggable. That is the cursor kind (The shape of the pointer: arrow, I-beam over text, hand on a link, open hand over something draggable. In a demo it switches as the pointer crosses an element's edge), and it switches as the pointer crosses the edge, not after it arrives.

*(A cursor parked on the button it just pressed hides the one thing the beat is about. Move it off as soon as the press lands.)*

## Drags, selections and phones

A drag is one motion: the thing follows the pointer the whole way. The grab offset (Where the hand took hold of something it drags. It stays the same through the whole drag and the drop, so the object does not jump to center itself under the pointer), where the hand took hold, stays fixed through travel and drop. A carried card lifts slightly (scale 1.03, a deeper shadow, a degree of tilt) and lands flat.

A text selection reads left to right from a word boundary and stays under the I-beam on every frame. Nothing scrolls unless the pointer is over it. And the camera pushes in only when the payoff would be too small to read, once per story.

On a phone there is no arrow cursor. A touch dot appears only where the finger taps or drags. A swipe is a flick: a third of the screen in about a quarter second. A notification is tapped to open its app, the way people actually use phones.

**Script a product demo (paste into your agent):**

```text
Script this faux UI as an animated demo:
- Story shape: how it feels to use. One gesture: assign the selected conversation to Maya.
- 4 beats: travel to the assignee, press, pick Maya from the menu, result lands (avatar on the row + a toast). Payoff holds 2s.
- Set the poster frame first: the assigned state with the toast visible.
- Cursor travel clamp(260 + 0.9 × distance, 300, 650)ms on a shallow arc; aim off the label (85% across, 60% down); rest on empty surface.
- The menu opens at product speed (~240ms) from its trigger; story motion may be slower.
- play once with a Replay button, or loop with a visible cause for the reset and a pause button if it runs over 5s.
```

**Check yourself:** Your demo loops every 12 seconds. At the end, the revenue number on screen counts back down from $4,210 to $3,980 so the next loop can count up again. What should change?

- [ ] Slow the count-down so it looks deliberate
- [x] Snap the number back while it is hidden or under a closed panel, or bring the next loop in as new data
- [ ] Remove the number from the demo
- [ ] Count down faster so nobody notices

*Why:* Data never rewinds in view. A reset needs a visible cause or must happen out of sight.

## In the skill

- https://school.agents.org.in/skill/faux-ui/demo-story-and-loops.md
- https://school.agents.org.in/skill/faux-ui/demo-cursor-and-touch.md


---

# Bento grids and rehearsal

> Plan a grid of feature tiles as one picture, then check an animated demo the way an audience sees it, frame by frame, before you ship it.

Module: Product demos · Level: intermediate · 7 min · https://school.agents.org.in/media/faux-ui/bento-and-rehearsal

Flip between the two grids. Each tile on the left is a decent cropped window. Together, they read as generated: the same signal four times.

*[Interactive demo: Same features, planned two ways.]*

A bento grid (A section of feature tiles of different sizes, like a bento box. It works when planned as one picture with different kinds of tile, not four cropped windows in a row) is one picture made of tiles. Most of its quality is decided in the tile list, before anything is drawn.

## Plan it as a table

Before any code, write the plan down. One row per tile:

| Tile | Span | Kind | The one thing | Words |
|---|---|---|---|---|
| Inbox | 2 × 1 | Hero UI | One conversation arriving, selected | "Shared inbox" + a sentence |
| Reply time | 1 × 1 | Stat | "14 min", the real median | "Median first reply." |
| Assign | 1 × 1 | Component | The assignee menu, Maya checked | "Assignments." |
| Weekend | 2 × 1 | Statement | One sentence in large type | the sentence |

Then check the plan against four questions: how many tiles are UI, which one is the hero, does any tile sit beside one of the same kind, and does any string repeat?

## The rules that matter most

- **Not every tile is UI.** About a third to a half show product screens; the rest are other kinds: a stat, a statement, an object, a diagram, a row of logos, a photo.
- **One hero**, two columns or two rows wide, with the richest picture.
- **A UI tile shows one thing, not a window.** Crop to the single component, the menu or the stepper, at real size or larger. Window chrome appears at most once in the whole grid, on the hero.
- **Neighbours contrast**, across and down like a checkerboard: a picture beside a number, dense beside calm.
- **Say each thing once.** No real string, avatar or color mark repeats from tile to tile.

*(Threading one conversation through five tiles feels like a story to its builder. It reads as one signal repeated.)*

A stat tile (A bento tile with one real number at display size (48 to 72px), a short lead and a one-line label. Number and unit stay on one line) is one real number at display size, 48 to 72px, with a short lead and a one-line label. The number and its unit stay on one line. Real figures only; never "10x".

The copy goes **outside** the picture at page size (17 to 24px), never the 12px of the UI inside it. And give each tile its own stage at 1:1: one stage around the whole grid scales every tile's text down with it, so 15px titles render at 9px in a narrow column.

Motion gets one place to lead. Supporting tiles do one small thing once as the grid scrolls in, staggered 80 to 120ms in reading order. At most the hero tells a cursor story.

## Then rehearse it

Agents check their work by reading the code back, or by taking one screenshot after everything has settled. Neither shows the frames in between, and that is where demo bugs live. Here is a toast arriving, filmed every 60 milliseconds. One frame should never ship:

*[Interactive demo: The first and last frames are perfect. Only the film shows the gap.]*

That grid of frames is a contact sheet (A grid of still frames taken at small steps through a video, so you can see exactly what happens around a cut), and making one is the heart of a rehearsal (Running an animated demo at real speed, filming it, and reading every frame, because the bugs live in the frames between the start and the end): run the demo at real speed, film it, read every frame.

  **Step: Prove the still first.** Render the opening and the payoff at both widths, next to the closest reference. Can a viewer find the focal object before any branding? Fix framing before touching motion.

  **Step: Set the poster, then film the loop.** Film the whole loop plus the start of the next, once at the shown width and once at 390.

  **Step: Step through every beat at 40 to 80ms.** Presses, menus, hand-offs, exits and the loop seam. A sheet sampled every second cannot show a pop.

  **Step: Fix the largest fault first.** Framing, then hierarchy, then motion, then polish. Re-film from the top after every fix.

Some faults are never intentional, whatever the builder meant: a button cut by the crop, words under 8px from an edge, a dot within 5px of its avatar, see-through fills overlapping, a stat's unit wrapped onto its own line, a popover drifting as its list changes height, a text highlight not under the I-beam.

Stageshow automates all of this with a filming script that ends with "rehearsal passed". **A clean report is necessary, not sufficient.** It catches mistakes, not a dull idea. Still watch it at full speed, with reduced motion on, and after the final edit.

## The fresh eye

When the film is clean and the frame still looks wrong, the builder has converged on their own blind spot. Hand the frames to a fresh-eye review (A second reviewer (a person or a new agent) who gets only the filmed frames and the checklist, never the builder's plan or reasons, so they see what a viewer sees): a person or a new agent who gets the frames, the checklist and the reference stills, and never the builder's plan or reasons.

Their first question: does this composition answer *this* product and page, or could it be any product in its category? Then a ranked list, framing first, each item a frame and a one-line fix. Finally, leave a short `REHEARSAL.md` beside the demo with what was checked, and what was not.

**Plan the grid, then rehearse the hero (paste into your agent):**

```text
Plan our feature section as a bento grid before building anything. Give me a table: tile, span, kind, the one thing it shows, its words.
- 4–5 tiles, one hero (2 wide). Only a third to a half are UI; the rest are a stat, a statement, an object or a diagram.
- UI tiles crop to one component; window chrome only on the hero.
- Neighbours are different kinds; no string, avatar or color repeats.
- Stat tiles: one real number at 48–72px, unit on the same line.
- Copy outside the picture at 17–24px. One Stage per tile at 1:1.
Then build the hero demo and rehearse it: film the full loop at the shown width and at 390, step through every press and the loop seam at 60ms, and list each fault with its frame and fix.
```

**Check yourself:** Your agent's feature section is four equal tiles, each a cropped window of the app with a title. What is the core problem?

- [ ] The windows need more realistic content
- [x] It says the same thing four times: no hero, no change of kind between neighbours, chrome in every tile
- [ ] The tiles need a gradient background
- [ ] There should be six tiles instead of four

*Why:* A grid of windows reads as generated however well each is made. Plan one hero and mix tile kinds so neighbours contrast.

## In the skill

- https://school.agents.org.in/skill/faux-ui/bento-grids.md
- https://school.agents.org.in/skill/faux-ui/demo-rehearsal.md


---

# An SVG is markup, not a picture

> Spot the problems in a generated SVG and get an agent to return a clean one that scales, recolors with the theme and can be read by a screen reader.

Module: SVG and icons · Level: beginner · 7 min · https://school.agents.org.in/visual/svg/clean-svg

Here is the same bell icon twice. One came straight out of a generator. The other was cleaned up in about a minute. Flip between them, then pick a color.

*[Interactive demo: Same bell. The generated one ignores the box CSS gives it and ignores the color you pick. The cleaned one does both, in a third of the bytes.]*

The generated file looks fine at its own size. Ask it to be bigger and it refuses. Ask it to turn blue and it stays dark gray. Those are not drawing problems. They are **markup** problems, and they are the whole subject of this lesson.

## Why the markup matters

An SVG (Scalable Vector Graphics: an image written as text (shapes, lines and curves), so it stays sharp at any size and you can style and animate its parts with CSS) is an image written as text. Every circle and curve in it is an element on the page, the same kind of thing as a button or a heading. That is what makes it special: you can color it with CSS, animate its parts, and give it a name a screen reader can say.

It also means the quality of the text decides everything that comes later. Whether it follows dark mode. Whether someone can animate the arm without redrawing the mascot. Whether it weighs 2 KB or 200 KB.

> **In plain words.** A PNG is a grid of colored dots. An SVG is a set of instructions: "move here, draw a curve to there, fill it with this color." Instructions can be edited, restyled and moved; dots can only be repainted.

## The five fixes that matter most

**Step: viewBox, and no fixed size.** The viewBox (The attribute that sets an SVG's own coordinate grid, like "0 0 24 24". With it, the drawing scales to whatever size CSS gives it; without it, the icon is stuck at one size) (`viewBox="0 0 24 24"`) sets the drawing's own grid. With it, the icon fills whatever box CSS gives it. Hard-coded `width="24" height="24"` on the root is how icons refuse to scale. Keep the viewBox, drop the fixed size.

**Step: Color from outside.** Fill a one-color icon with currentColor (A CSS keyword meaning "whatever the text color is here". An icon filled with currentColor changes color with the text around it, including in dark mode) and it takes the text color around it. Illustrations with several colors use CSS variables with a fallback: `fill="var(--mascot-skin, #f2c9a0)"`. Dark mode becomes a variable flip, not a second file.

**Step: A name, or nothing.** If the graphic means something, give it `role="img"` and a title (A <title> as the first child of an SVG gives it a name a screen reader can read aloud. Pair it with role="img"; decorative SVGs get aria-hidden="true" instead) as its first child. If it is decoration, hide it with `aria-hidden="true"`. Never meaning with no name; never a name on decoration.

**Step: Sensible numbers.** Whole numbers on a sensible grid (24 for icons, 100 or 1000 for illustrations), one or two decimals at most. `12.000000` is six characters saying nothing.

**Step: Groups that mean something.** `<g id="arm-left">` can be animated by anyone. A hundred unnamed `<g>`s cannot, including by you next week. Order groups back to front, like layers in a design tool.

## Shapes, paths and repeats

Simple static shapes can stay as `<rect>` and `<circle>`: they are small and easy to read. Anything that will animate or change shape should be a path (The SVG element that draws any shape from a list of commands in its d attribute: M moves the pen, L draws a line, C draws a curve, Z closes the shape), a list of drawing commands, because a morph needs matching command lists (lesson three).

Repeats belong in one place. Gradients, masks and filters go in `<defs>`; a shape used five times goes in a `<symbol>` and is placed with `<use>`. One edit then changes every copy.

And never paste a photo inside an SVG. A base64 PNG wrapped in SVG is a PNG with worse compression.

## Shrink it, carefully

SVGO (A free tool that shrinks SVG files by removing editor junk and rounding numbers. It needs a few settings turned off so it keeps the viewBox, titles and the ids you animate) strips editor junk and rounds numbers. Run it with `--multipass`, but switch off the parts that break things: keep the viewBox, keep `<title>` and `<desc>`, keep the ids and `data-*` attributes you animate against. When several SVGs sit inline on one page, prefix their ids so two gradients called `g1` don't collide.

```bash
npx svgo icon.svg --multipass
```

Then look at it. A shrunken file that renders differently is not smaller, it is broken.

*(Inline `<style>` inside an SVG becomes global CSS the moment you paste the SVG into a page. Scope it under the SVG's id, or move it out.)*

## The tells of a generated SVG

Once you know them, you can spot them in the source in seconds:

**Generated (avoid):**

- `width` and `height` on the root, no viewBox
- every color a hard-coded hex
- nests of empty, unnamed `<g>`
- circles converted to 8-decimal paths
- the same gradient defined four times
- an embedded PNG

**Clean (do this):**

- viewBox only, size from CSS
- `currentColor` or CSS variables
- a few groups, each named
- primitives where static, paths where they move
- one gradient in `<defs>`
- no rasters

Each of these is cheap to fix and **expensive to leave in**, because every later step (theming, animating, shipping) pays for it again.

## Get your agent to do it

You don't have to fix these by hand. Hand the file to your agent with the rules spelled out.

**Clean an SVG (paste into your agent):**

```text
Clean up this SVG for production:
- keep viewBox, remove width/height on the root
- monochrome: fill="currentColor"; multi-color: CSS variables with fallbacks
- if it carries meaning: role="img" + <title> first, aria-labelledby; if decorative: aria-hidden="true" focusable="false"
- round coordinates to 1 decimal, remove empty groups and editor metadata
- name the groups by what they are (arm-left, eyes)
- no embedded rasters, gradients only in <defs>
Return the file and a short table of what changed and why.
```

**Check yourself:** An icon stays 24px no matter what width CSS gives it, and stays gray in dark mode. What is the likely fix?

- [ ] Redraw it at 48px and export again
- [x] Add a viewBox, remove the fixed width and height, and fill it with currentColor
- [ ] Wrap it in a div with overflow: hidden
- [ ] Convert it to a PNG with a transparent background

*Why:* Fixed width/height without a viewBox stops scaling, and a hard-coded hex ignores the theme. viewBox plus currentColor fixes both without redrawing anything.

## In the skill

- https://school.agents.org.in/skill/svg/MOC-svg.md
- https://school.agents.org.in/skill/svg/svg-creation.md


---

# Making SVG move

> Choose the right animation engine for where an SVG lives, draw a line on with two CSS properties, and stop rotations from orbiting the corner.

Module: SVG and icons · Level: intermediate · 8 min · https://school.agents.org.in/visual/svg/animating-svg

Press replay. Then grab the slider and drag it yourself.

*[Interactive demo: The drawing animation is one property moving from 1 to 0. The slider moves the same property by hand.]*

That line is not being drawn. It is already all there. It is hidden behind one very long dash, and the dash is sliding out of the way. That trick, and a handful like it, covers most of the SVG animation you will ever need.

## First question: where does the file live?

Before easing, before duration, ask where the SVG renders. That decides which tools can even reach it.

| Where it renders | What can animate it |
|---|---|
| Inline in the page (React, HTML), reacting to clicks and state | CSS transitions or the Web Animations API |
| Inline, but driven by a drag, a spring or a long timeline | Motion or GSAP, whichever the project already uses |
| Loaded as `<img>`, a background image, in an email, on social, in Figma | Only what is *inside the file*: CSS `@keyframes` in its own `<style>`, or SMIL (Animation written as tags inside the SVG file itself (<animate>, <animateMotion>). It keeps playing even when the SVG is loaded as a plain image, where page CSS and JavaScript cannot reach) tags |
| A rich After Effects export that already needs a player | Lottie (A format that plays After Effects animations in apps and on the web through a JavaScript player. Handy for rich exports, but usually several times larger than the same motion as plain SVG) or Rive, and only then |

The third row catches people out. An SVG loaded as an image is sealed: your page's CSS and JavaScript can't touch it. Animation has to travel inside the file. CSS keyframes are the most portable choice; SMIL still wins for moving something along a path with `<animateMotion>`.

> **Usually smaller than the Lottie.** Native SVG with CSS or SMIL is often five to ten times smaller than the Lottie export of the same motion, and it keeps the `<title>` a screen reader needs. Lottie earns its place when the animation is genuinely rich.

## Drawing a line on

The trick in the demo uses stroke-dasharray (The SVG setting that turns a line into dashes. Make one dash as long as the whole line, slide it with stroke-dashoffset, and the line looks like it is being drawn), which turns a stroke into dashes, and `stroke-dashoffset`, which slides them along.

Measuring a path's length is fiddly, so skip it. Set `pathLength="1"` on the path and the browser treats the whole line as one unit long, whatever its real size.

```css
path {
  stroke-dasharray: 1;        /* one dash, as long as the whole path */
  stroke-dashoffset: 1;       /* pushed fully out of view */
  stroke-linecap: round;      /* soft ends while it draws */
  animation: draw 1600ms cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
```

Drawing is one of the few places an ease-in-out curve is right: the line is moving across the screen on its own, not answering a click.

## The corner problem

Now press rotate, a few times.

*[Interactive demo: Same rotate, same gear. On the left it swings around the top-left corner of the drawing (the red dot). On the right it turns in place.]*

In HTML, a rotation turns around the element's center. In SVG, it turns around **(0, 0)**, the top-left corner of the whole drawing. So every rotation and scale you copy from HTML orbits the corner.

The fix is two lines. transform-box (A CSS setting that decides what a transform measures from. In SVG the default is the corner of the whole drawing, so set transform-box: fill-box to rotate or scale a part around its own center) tells the browser to measure from the shape's own box, and then `transform-origin` means what you expect.

```css
.gear { transform-box: fill-box; transform-origin: center; }
.arm  { transform-box: fill-box; transform-origin: 10% 20%; } /* the shoulder */
```

For a mascot, put the origin where the part would hinge in life: a shoulder, a wrist, a lid.

*(Most "my SVG animation goes haywire" bugs are this one.)*

## Keep it cheap

The rules from motion apply unchanged. `transform` and `opacity` are cheap: the browser moves a finished picture. Animating `d`, `points`, `r`, `stroke-width` or a filter makes it repaint every frame. Fine for one small icon; **ruinous for a whole scene**.

- **Move groups, not paths.** Animate `<g id="arm">`, `<g id="eyes">`, not three hundred separate paths.
- **Stagger by 30 to 80ms.** Paired parts (two eyes, two arms) share duration and easing.
- **Budget the file.** About 2 KB for an animated icon, 10 to 30 KB for a hero mascot. Reuse shapes with `<use>`, move them with transforms instead of redrawing.

## Reduced motion travels with the file

Some people turn on "reduce motion" in their system settings because movement makes them unwell. Inline SVG respects it only if you wrote the media query. A self-contained file can carry its own:

```css
@media (prefers-reduced-motion: reduce) {
  .gear, path { animation: none; }
}
```

That query works even inside a file loaded as `<img>`. SMIL can't read it, so a SMIL-only file needs a still fallback image.

A copied animated icon with its own keyframes and no reduced-motion query is the most common miss in generated UI. Check every one.

## Ask for it precisely

**Animate an SVG (paste into your agent):**

```text
Animate this SVG. It is used as <img> in our marketing page, so the animation must live inside the file.
- CSS @keyframes in the file's own <style>, selectors scoped under the root id
- draw the outline on with pathLength="1", stroke-dasharray: 1, stroke-dashoffset 1 → 0, round linecaps
- the lid rotates around its hinge: transform-box: fill-box with transform-origin at the hinge
- only transform, opacity and stroke-dashoffset animate
- @media (prefers-reduced-motion: reduce) shows the finished drawing
- keep it under 10 KB; report the final size
```

**Check yourself:** A logo SVG ships as an <img> tag on your landing page. Your agent added the animation to the page's global CSS file. What happens?

- [ ] It works, but only in Chrome
- [x] Nothing animates: page CSS can't reach inside an SVG loaded as an image
- [ ] It animates, but ignores reduced motion
- [ ] It animates twice as fast

*Why:* An SVG in an <img> is sealed off from the page. The animation has to live inside the file, as CSS keyframes in its own <style> or as SMIL.

## In the skill

- https://school.agents.org.in/skill/svg/svg-animation.md


---

# Morphing shapes, and when not to

> Author two paths that really morph into each other, recognise the ones that never will, and know when a simple icon swap is the better answer.

Module: SVG and icons · Level: intermediate · 7 min · https://school.agents.org.in/visual/svg/path-morphing

Morph the circle into a square. Then switch to "Mismatched" and try again.

*[Interactive demo: Matched: the square is written with the circle's own command list, so it glides. Mismatched: a plain square, so the browser can only flip halfway.]*

Both squares look identical when they sit still. Only one can be reached from the circle. The difference is not the shape. It is how the shape is written down.

## The one rule

A path morph (One SVG shape smoothly turning into another by sliding every number in its path to a new value. It only works when both paths have the same commands in the same order) is the browser sliding each number in one path to the matching number in the other. So it needs every number to have a partner:

**both paths need the same commands, in the same order, of the same type.**

Four curves can become four curves. Four straight lines can't become four arcs, because there is nothing to pair up. When the lists don't match, the browser gives up and swaps one shape for the other.

> **In plain words.** A path is a recipe: "move to here (M), curve to there (C), close up (Z)". A morph walks two recipes side by side, step by step. If one recipe has a step the other doesn't, the walk can't happen.

## Writing two shapes that match

Decide the command list first, then place the points for each shape.

```css
.shape {
  d: path("M4 12 C4 7.6 7.6 4 12 4 C16.4 4 20 7.6 20 12 C20 16.4 16.4 20 12 20 C7.6 20 4 16.4 4 12 Z");
  transition: d 240ms cubic-bezier(0.25, 1, 0.5, 1);
}
.shape[data-state="square"] {
  d: path("M4 12 C4 4 4 4 12 4 C20 4 20 4 20 12 C20 20 20 20 12 20 C4 20 4 20 4 12 Z");
}
```

Both are `M`, four `C`, `Z`. The square cheats: each "curve" has its control points pulled into the corner, so it looks straight.

**Step: Pick one point budget.** Say `M` plus eight `C` plus `Z`. Both shapes use exactly that. If one shape needs less detail, add points that sit on top of each other; never delete commands.

**Step: Same start, same direction.** Start both at the same spot and draw both clockwise (or both counter-clockwise). Otherwise the morph twists through itself.

**Step: Match the holes.** A shape with two holes needs two holes in the other state. Shrink an unused hole to nothing instead of removing it.

**Step: Normalise before shipping.** Convert both to absolute cubic curves (`C`, not `c` or `S`). A morph that works in Chrome and jumps in Safari almost always mixes `S` and `C`, or `l` and `L`: one browser quietly converts them, the other doesn't.

## When the shapes just don't match

For two unrelated shapes (a logo becoming a word, a blob becoming a star), a library can resample both to the same number of points. flubber (A small JavaScript library that resamples two unrelated shapes so they can morph into each other. Worth it when the morph is the product, not for an icon toggle) does this with one function, `interpolate(pathA, pathB)`, and GSAP's MorphSVG does the same inside GSAP.

The cost is a dependency and a JavaScript loop. Pay it when the morph *is* the product: a logo transformation, a shape-shifting hero. Not for a button.

*(Animating `d` repaints every frame. One hero shape is fine. Twenty morphing at once is not.)*

## Most of the time, swap instead

Play to pause. Copy to check. Menu to close. These feel like morphs, but the honest version is an icon swap (Changing one icon for another (copy to check, play to pause) by fading, scaling and blurring the old one out while the new one comes in, instead of morphing between them). Press each button a few times, quickly.

*[Interactive demo: Left: the icon just changes. Right: the old one scales to 0.3, fades and blurs as the new one arrives. Press it twice fast and it reverses smoothly.]*

Both icons are rendered at once, stacked. The outgoing one goes to `scale(0.3)`, `opacity: 0` and `blur(3px)`; the incoming one comes back from there. Because both are always on the page, pressing again mid-change just reverses the transition.

A swap reads as a real change of meaning. A timid morph between unrelated shapes, a copy icon stretching into a tick, reads as a glitch.

| Situation | Use |
|---|---|
| Two states of the same shape (chevron up and down) | Rotate it |
| Icons designed together on one line budget | A morph |
| Two different icons (copy and check, play and pause) | A swap |
| A logo or hero that shape-shifts | A morph, with flubber if the paths don't match |

## Ask for the right one

**Morph or swap (paste into your agent):**

```text
Our copy button should turn into a check when pressed.
Don't morph the paths. Render both icons stacked; outgoing goes to scale(0.3), opacity 0, blur(3px), incoming reverses that, 200-260ms with cubic-bezier(0.23, 1, 0.32, 1), interruptible.
Separately, the hero blob morphs between three shapes: author all three as M + 8 absolute C + Z, same start point, clockwise, and transition the d property. Tell me if any shape can't fit the budget.
```

**Check yourself:** Your agent morphs a hamburger icon (three M/L lines) into an X drawn as two M/L lines. It jumps instead of animating. Why?

- [ ] The duration is too short
- [x] The paths have different numbers of commands, so nothing pairs up
- [ ] transform-box is missing
- [ ] Morphs only work with circles

*Why:* Three lines have more commands than two. A native morph needs the same commands in the same order; keep three lines in both states (collapse one) or rotate the lines instead.

## In the skill

- https://school.agents.org.in/skill/svg/svg-path-morphing.md


---

# Stickers and mascots

> Build an isometric sticker that reads as a made object, and turn a flat animated clip into one editable, recolorable SVG.

Module: SVG and icons · Level: intermediate · 9 min · https://school.agents.org.in/visual/svg/stickers-and-flipbooks

Play the clasp. Then switch to "Flat clip-art" and play it again.

*[Interactive demo: The same lock. The sticker has thickness, parts and a still hold after the action. The clip-art is one shape with the detail painted on.]*

The flat one is not wrong, exactly. It just looks like it was generated. The sticker looks like someone made it. That difference comes from a short list of rules, most of them from Kelvin Adithya's fun-isometric, a skill that draws and animates exactly this kind of sticker.

## Where stickers belong

Empty states, onboarding, feature cards: the rare, friendly screens. Not inside buttons, not in a list of fifty rows. A sticker is illustration, so it gets illustration time (a second of action, then rest), never the 200ms of a control's feedback.

## Drawing one

**Step: One square, one outline.** A 500-unit canvas, the subject filling about half of it with room to swing. One ink outline, about 5 units, on every part, with sharp corners. No gradients, no drop shadows, no glow.

**Step: Faces lean.** An isometric (A way of drawing 3D objects without perspective: faces lean at fixed angles, so a box shows its top and two sides and parallel lines stay parallel) face is tilted back, so rectangles become slanted parallelograms and circles become ellipses. It is not just rotated. The side falls the same way (down and left) on every part.

**Step: Every part is a slab.** Each part is a slab (A flat shape given thickness, like a cookie cut from dough. Isometric stickers build every part as a slab so it reads as a made object, not a flat drawing): a flat shape with its own thickness. The lock is a body slab, a shackle slab and a raised keyhole plate. Three to six parts for one object.

**Step: Sparse ink detail.** One keyhole. Two pupils. A few lines. Sparkles are small white stars with the same outline.

**Painting the detail flat onto one shape** is the tell of a generated sticker. Build it from parts.

## Colors as jobs, not hex codes

The sticker's fills are named by palette role (A named job for a color (main, deep, tint, gold, ink) instead of a fixed hex value, so a whole illustration can be recolored to a new brand without losing what each part means): `main` for bodies, `deep` for the dark side, `tint` for pale fills, `gold` for coins and stars, plus `ink` and `white`. Two accents and white per icon.

Roles are what let a sticker move to a new brand. Map `main` and `deep` to your design tokens and every part keeps its meaning. Leave a sticker on its default six-color palette in a two-color product and everyone can tell nobody mapped it.

## Moving one

- **Pick the motion by the verb.** A lock *clasps*, a coin *bounces*, a bell *wiggles*, a plane *drifts*. The verb decides the movement, not the noun.
- **Use tuned keyframes, not a sine wave.** The presets come from After Effects keys an animator already tuned. Change the amplitude or start; never stretch them.
- **Pivot where it would hinge.** Children follow 4 to 6 frames late, so nothing moves in lockstep.
- **Sparkle on the beat.** The glint starts on the slam, the landing, the reveal.
- **Then hold still.** Loops run two to three seconds with the action in the first half. Filling the hold with idle wobble is the quickest way to cheapen it.
- **Pop with scale, never fade.** A slab is drawn as a stack of copies, so fading it shows the stack.

> **Where its rules meet the rest of the course.** Its entrance pops from a small scale on a bounce. Menus and modals still enter from 0.95, never from nearly zero; a sticker's pop is a character moment, so it keeps its own. Idle loops are for one hero sticker on a rare screen; in a list, play once and rest. And your brand's palette always wins over its default inks.

If fun-isometric is installed, let it draw, check and export the sticker (live on a canvas or as looping video with a still poster). If not, build to these rules as an animated SVG. Either way, check it at 160px beside your other stickers: same line weight, faces leaning, every part with thickness.

## From a video clip to a vector mascot

Sometimes the source isn't drawn by hand at all. It is a short, flat animated clip, perhaps made by a video model: a rooster typing, a blob waving. That can become one SVG that is tiny, sharp at any size and recolorable.

*[Interactive demo: A vector flipbook: six traced frames, each its own group, shown one at a time at 12 frames a second. Try the jitter and the recolor.]*

It only works for **simple, flat art**: solid fills, two to eight colors, clean outlines, no gradients or texture. Ask for that when you generate the clip ("flat vector style, solid colors, no gradients, plain background").

**Step: Extract frames.** `ffmpeg -i clip.mp4 -vf "fps=12,scale=512:-1" frames/f_%03d.png`. Twelve frames a second reads as animation; 24 doubles the file for little gain.

**Step: Reduce the colors, then trace.** Quantize each frame to its handful of colors first, then vectorize (Trace a pixel image into vector shapes. Tools like vtracer turn each flat color region of a frame into a path) with vtracer (Potrace for pure black and white).

**Step: Clean and name.** Remove specks, merge near-identical colors, delete the background, run SVGO. Name each color region the same in every frame (`body`, `beak`, `eye-l`), matched by position and color. Named layers are what make it editable.

**Step: Assemble and budget.** One SVG, one group per frame, a stepped animation: a vector flipbook (One SVG file holding every frame of an animation as its own group, shown one at a time by a stepped animation, like flipping the pages of a sketchbook). Colors become CSS variables. Merge identical frames into longer holds. A simple 24-frame loop lands around 20 to 60 KB; bigger means slivers survived step three.

*(Traced frames wobble by a pixel because each was traced alone. Trace at one resolution, and reuse the last frame's paths for a hold.)*

The SVG stays the editable source. Export onward to Lottie, Rive or MP4 only when a platform needs it.

**Brief a sticker (paste into your agent):**

```text
Make an animated isometric sticker of a padlock for our empty "No saved passwords" state.
- 500-unit canvas, one 5-unit ink outline, faces leaning, side falling down-left
- three parts, each its own slab: body, shackle, keyhole plate
- fills by role (main, deep, tint, ink, white), mapped to our tokens --accent and --accent-deep
- verb "clasp": shackle drops, two small bounces, sparkle on the slam, then a still hold; play once, no idle loop
- reduced motion shows the closed lock
Check it at 160px next to our existing stickers.
```

**Check yourself:** You turn a mascot clip into a vector flipbook and the file is 900 KB. What most likely went wrong?

- [ ] The clip was 12 frames a second
- [x] The vectorizer left thousands of slivers: the art wasn't flat enough, or colors weren't reduced before tracing
- [ ] The colors are CSS variables
- [ ] Each frame is its own group

*Why:* A simple loop should land around 20 to 60 KB. Gradients, texture or skipping the color-reduction step turn each frame into slivers; go back, quantize, clean and dedupe.

## In the skill

- https://school.agents.org.in/skill/svg/isometric-stickers.md
- https://school.agents.org.in/skill/svg/fun-isometric-reconciliation.md
- https://school.agents.org.in/skill/svg/video-to-vector-pipeline.md


---

# Silence is the default

> Decide whether a moment in your interface should make a sound at all, using how often it happens, what it is for and the 100th-use test.

Module: Sound · Level: beginner · 6 min · https://school.agents.org.in/media/sound/silence-is-the-default

Press the button below. With sound off, nothing happens to your ears, and the button still feels fine. Now turn sound on and press it again.

*[Interactive demo: A 50 ms tick, made in code. Sound starts off, and the switch is right there.]*

That tiny tick is pleasant once. Imagine it on every button in an app you use all day. By lunch you would be hunting for the mute switch, and you would remember the product as the noisy one.

That is the whole problem with interface sound. **A bad sound is worse than no sound, and people remember it.** So this module starts with the question most teams skip: should this make a sound at all?

## Why sound has to work harder than motion

You can look away from an animation. You cannot un-hear a chime. And while operating systems have a setting that turns motion down (the reduced-motion preference), there is no setting that turns *your* sounds off. Your product has to do that job itself.

So the default is silence. A sound has to earn its place, and the bar is higher than it is for animation.

> **In plain words.** A **UI sound** is any noise an interface makes when you do something: a click, a whoosh, a chime. This module is about deciding which moments deserve one, designing a small set that belong together, and playing them at the exact right moment.

## Question one: how often?

Frequency decides most cases before anything else. The more often a person hears a sound, the faster it goes from charming to grating.

| How often | Example | Sound? |
|---|---|---|
| Constantly | Typing, scrolling, hovering | Never |
| Many times an hour | Pressing a button, picking an option | No, unless the product is built to feel physical |
| A few times a day | Message sent, file saved | Maybe: one short sound, and only if the person opts in |
| Rarely | First payment, setup finished | Yes. This is where the sound budget goes |

The keyboard never gets sound. Hover never gets sound. A plain button press already has motion and, on phones, a buzz; adding a click there makes the product feel like a toy.

## Question two: what is it for?

Apple's sound designers put it as one word: utility. A sound is worth adding only when it tells you something your eyes might miss. Four reasons pass:

- **Your attention is elsewhere.** You hit send and are already typing the next message. A soft tone confirms it went.
- **Something changed with nothing to see.** A timer ended in a tab you are not looking at. An upload finished in the background.
- **The product is physical by design.** A wallet card snapping into place, a game piece landing.
- **It is a brand moment.** A launch film or an onboarding hero, where sound is part of the medium. The last lesson covers that.

Four reasons fail: decoration, "the animation felt quiet", filling the wait during a slow request, and copying a competitor's ping.

*[Interactive demo: Pick a moment. Most of them stay silent; the ones that pass let you hear their sound.]*

## Question three: does it survive the 100th time?

This is the test Apple's team uses. Picture the sound the first time you hear it. Now picture the hundredth time. If the hundredth makes you reach for mute, cut the sound or make it smaller.

The 100th-use test (Imagine a sound the first time you hear it, then the hundredth. If the hundredth makes you want to mute it, cut the sound or make it smaller) is lopsided in a useful way. A sound that is a little too loud or a little too long is annoying by the tenth time. A sound that is a little too quiet or too short is never noticed as a flaw. So when you are unsure: halve the length and turn it down a notch.

*(Halve it, then drop it 3 dB. You will almost never regret it.)*

## Question four: is there a visual twin?

A sound may back something up. It must never be the only signal. Most people use their devices muted, in an office, on a train, or with a screen reader that is already talking. If the sound carries information nothing else shows, the design is broken for all of them.

Every sound in this module has a visual twin: the toast, the checkmark, the shake.

## Question five: can people turn it off?

Product sound ships **off**, with a switch people can find and reach by keyboard, and the choice is remembered. People who turn sound on often love it. People who cannot turn it off uninstall.

  **Before (avoid):**

A soft click on every button, a whoosh on every page change, a chime on load. No setting to turn it off.

  **After (do this):**

Silent by default. One short tone when a message sends, for people who turn sound on in settings. A three-note figure the day the first payment lands.

## Two surfaces, opposite defaults

Everything above is about the **product**: the app people use every day. There, sound is opt-in, rare and tiny.

A **launch film** flips it. In a short video that introduces the product, sound is the emotional layer and silence is something you place on purpose. Same skill, opposite default, so keep them apart in your head. The film side gets its own lesson at the end of this module.

## Getting your agent to ask the question

Agents love adding sound once you mention it. Give them the decision first, and let them say no.

**Decide before adding any sound (paste into your agent):**

```text
Before adding any sound to this UI, go through every moment that might get one and make a table: moment, how often a person hears it, what it is for, does it survive the 100th use, and its visual twin.
Default to silence. Typing, scrolling, hover and ordinary button presses get no sound.
Only moments that confirm something while attention is elsewhere, signal a change with nothing to see, or mark a rare milestone may pass.
Sound ships off by default behind a visible, keyboard-reachable setting that persists.
Show me the table and wait before generating or wiring any audio.
```

**Check yourself:** A team wants a soft click on every button press in their project-management app. What is the best answer?

- [ ] Yes, a click makes it feel responsive
- [ ] Yes, but make it quieter
- [x] No: presses are frequent, already have visual feedback, and the click would fail the 100th-use test
- [ ] Yes, as long as it is under 300 ms

*Why:* Frequency comes first. A press happens many times an hour and already answers with motion, so sound there reads as a toy and gets muted. Save the sound budget for rare or attention-elsewhere moments.

## In the skill

- https://school.agents.org.in/skill/sound/sound-decision-framework.md
- https://school.agents.org.in/skill/sound/MOC-sound.md


---

# One family of sounds

> Design four to six sounds that share one material, and give each one the right pitch, length, loudness and file format.

Module: Sound · Level: intermediate · 7 min · https://school.agents.org.in/media/sound/one-family-of-sounds

Tap each card below with sound on. Five different jobs, and yet they clearly come from the same product. That is not luck. They are all the same small struck bar, played higher, lower, shorter or longer.

*[Interactive demo: One material, five sounds. Higher dot, higher pitch. Longer bar, longer sound.]*

A product's sounds work like a typeface. One family, several weights. **Five sounds from one material feel like a product; five sounds from five packs feel like clip art.**

## Pick one material

Start with a physical thing, not a style. "A felt mallet on a wooden bar." "A fingernail on thin plastic." "A muted glockenspiel." Every sound in the family is that thing, struck differently.

Apple's sound team works this way: they record real instruments like marimba and kalimba, and once tapped a hollow watch case with mallets to find its ring. Real materials make new technology feel familiar. The default AI failure is the opposite: a glossy sci-fi "digital blip" that belongs to no product.

> **In plain words.** An earcon (A short interface sound whose meaning you learn from its shape, like a rising tone for joining a call. It does not imitate a real-world noise) is a short sound whose meaning you learn, like the rising tone when someone joins a call. It does not imitate anything real; its shape tells you what happened. A sound palette (The small, matched set of four to six sounds a product uses, all made from one material, like one typeface in several weights) is the small, matched set of them a product uses.

## The grammar of a family

Meaning comes from the shape of the sound. People learn it in a handful of hearings.

| What changes | Rule | Why it works |
|---|---|---|
| Size → pitch and length | Small things tick high and short (40 to 120 ms). Big things land low and long (300 to 800 ms). | Physics. A chip is lighter than a page. |
| Direction → meaning | Rising = success, open, join. Falling = dismiss, leave, error. | Every operating system you have used. |
| Interval → feeling | A major third or a fifth confirms. A tight, clashing step warns. | Consonance sounds safe; dissonance sounds tense. |
| Complexity → priority | One hit for frequent things. Several notes only for rare ones. | A busy sound heard often tires people out in a day. |

Notice the cards above: `send` rises, `error` falls and clashes, `success` is the only one with three notes.

## A family is small

Most products need four to six sounds, not forty:

- **tick** for a toggle, a selection, a snap. One hit, 80 ms or less, high.
- **tap** for a press, only in products built to feel physical. Up to 120 ms.
- **send** for sent, saved, done. One or two rising notes, up to 250 ms.
- **receive** for an incoming message. Two notes, lower than send, up to 400 ms.
- **error** for a rejected submit. Low, falling, up to 200 ms, beside a shake.
- **success** for a rare milestone. Three notes, up to 800 ms.

Write the family down as a list before generating anything. It is your sound design system.

**Design the family first (paste into your agent):**

```text
Design a sound family for this product before making any audio.
Pick ONE physical material (for example "soft felt mallet on a small wooden bar") and derive every sound from it.
List 4-6 sounds max as a table: name, role, contour (rising/falling), notes or pitch, duration in ms.
Rules: small/frequent = high, short, single hit; rising = success; falling and dissonant = error; only rare milestones get 3 notes.
No stock UI packs, no sci-fi blips.
```

## The numbers

Most "this sound feels wrong" complaints are a spec miss, not a taste miss: too long for its animation, too loud next to speech, or a sliver of silence before the hit.

| Kind | Length |
|---|---|
| Tick, toggle, snap | 40 to 80 ms |
| Tap, press | 80 to 150 ms |
| Hover | 0 ms. Hover is silent. |
| Send, save | 150 to 250 ms |
| Notification | 200 to 600 ms |
| Error | 100 to 200 ms |

A handy rule from Toptal: a sound should not outlast the animation it goes with by more than about 300 ms. A 120 ms press with an 800 ms tail is a bell, not feedback.

**Loudness.** Taps and ticks sit around −18 to −14 LUFS (Loudness Units relative to Full Scale: how loud a whole piece of audio sounds to a person. Closer to zero is louder; −14 is loud, −30 is quiet), below speech and above silence. Notifications may go to about −12. Peak every file at −3 dBFS (Decibels below the loudest a digital file can go. 0 dBFS is the ceiling where sound distorts, so UI sounds peak at about −3 dBFS) so it never clips. Put the design level in the file and the person's level in the volume control; never bake "a bit quieter" into the asset.

**Format.** Mono (Sound on one channel instead of separate left and right. UI sounds are mono because phone and laptop speakers fold stereo into one anyway), 44.1 kHz. Phones and laptop speakers fold stereo into mono anyway. Keep a WAV as the master and ship MP3 or Opus. Trim the start to the first audible sample: five milliseconds of silence up front is a visible sync error. Fade the last 10 ms so it ends without a click.

**Shape.** Cut everything under 150 to 200 Hz; laptop speakers cannot play it and it only eats headroom. Keep the body between 500 Hz and 4 kHz, where every device is honest.

*(Check on the worst laptop speaker you own, not studio headphones at midnight.)*

## What a designed effect is made of

For films and marketing pieces, Leon Lin's earshot project measured dozens of professional sound effects and found the same habits again and again:

1. **One gesture each.** A pitch that sweeps, notes that step up, a gate that speeds up into the cut.
2. **Its own frequency band.** Typing has nothing low; impacts live low with a thin tick on top. The middle is left for voice and music.
3. **A hard end.** Short sounds hold, then stop. No long reverb ride-out.
4. **Mono for objects, wide for textures.** A click is mono; a shimmer can be wide.
5. **Placed by kind.** Clicks land their attack on the moment; a whoosh peaks on the fastest frame; a riser ends on the hit.

Repeated sounds vary in *take* (four recordings cycled, a tiny level change) rather than random pitch. The UI click is the exception: one voice, one level, every time.

**Check yourself:** A product's error sound is a bright, rising two-note chime, 700 ms long. What is wrong?

- [ ] Nothing, it is pleasant
- [x] It rises and is consonant, which reads as success, and it is far too long for an error
- [ ] It should be stereo
- [ ] It should be louder so people notice

*Why:* Direction and interval carry meaning: rising and consonant says success. An error should be low, short (100 to 200 ms) and falling, with a shake as its visual twin.

## In the skill

- https://school.agents.org.in/skill/sound/sound-palette.md
- https://school.agents.org.in/skill/sound/sound-spec.md
- https://school.agents.org.in/skill/sound/designed-sfx-anatomy.md


---

# Sound from motion

> Place a sound on the exact frame where the motion makes contact, and derive its pitch, length and position from the motion itself.

Module: Sound · Level: intermediate · 7 min · https://school.agents.org.in/media/sound/sound-from-motion

Drop the tile below, then make it bigger and drop it again. The small one ticks high and quick. The big one lands lower and rings a little longer. Nobody picked those sounds from a library: the size of the tile decided them.

*[Interactive demo: Change the size, drop the tile. Bigger means lower and longer, and the sound fires as it lands.]*

When a sound feels wrong next to an animation, it is almost always because the sound and the picture disagree. A tiny chip lands with a heavy thud. A card on the right sounds from the middle. A whoosh peaks where nothing is moving. The fix is not a better sound pack. **Let the motion decide the sound.**

## Sound and motion are one event

Apple calls this harmony: things should feel the way they look and sound the way they move. In the physical world, physics gives you that for free. In software, you build it by hand.

The rule that produces it: the transient (The sharp, loud first instant of a sound, like the crack at the start of a clap. It is the part you line up with the picture) (the sharp start of the sound) sits on the contact frame (The frame of an animation where the moving thing visibly lands or arrives. Sounds that confirm a motion belong on this frame, not where the motion starts), the frame where the visual lands. The card stops, the toggle seats, the logo appears. Not the frame where the motion begins.

> **In plain words.** A **frame** is one still picture in an animation; at 30 frames a second, one frame lasts about 33 ms. The **contact frame** is the one where the thing visibly arrives. That is where the "click" belongs.

## Late is forgiven, early is not

Broadcast engineers measured how far apart sound and picture can drift before people notice. The numbers are lopsided:

| | Sound arrives early | Sound arrives late |
|---|---|---|
| People notice at | 45 ms | 125 ms |
| Still acceptable up to | 90 ms | 185 ms |

We forgive sound that comes a little after the picture, because that is how the world works: light travels faster than sound. Sound that comes *before* the picture reads as broken almost at once.

So: never let the sound lead. Aim for 0 to 30 ms late, about one frame.

*[Interactive demo: The same landing, three placements. The dot on the timeline is when the sound fires.]*

Try all three. "Early" sounds wrong even though it is only 90 ms off. "Late" at 200 ms becomes two events: a landing and then an echo.

## Each kind of sound has its own sync point

- **Clicks, keys, pops, impacts** put their *attack* on the contact frame.
- **A whoosh** puts its *peak* on the fastest frame of the move, starting 0.1 to 0.45 s before.
- **A riser** *ends* on the hit, or just before it to leave a breath of air.
- **A slow entrance** gets two sounds: the travel early, then a small landing on the settle.

## Finding the contact frame in code

For a strong ease-out (How speed changes during an animation: start fast and slow down, start slow and speed up, or both. It decides whether motion feels snappy or sluggish), the motion has visibly stopped long before the math says it has. A good rule is about 85% of the way through the duration. A 480 ms landing makes contact around 408 ms.

Fire the sound from the same event that starts the change, timed to the landing:

```ts
// Confirming a landing: play when
// the transform transition ends.
card.addEventListener("transitionend",
  (e) => {
    if (e.propertyName === "transform") {
      play("land");
    }
  },
  { once: true });

// A tick that goes with a toggle
// fires with the change itself.
toggle.addEventListener("change",
  () => play("tick"));
```

*(Springs never stop cleanly. Sync to the first time they cross the rest position, not to the end.)*

Firing on a spring's `animationend` lands the sound late by its whole wobbly tail. And every sound file must start at its first audible sample, or the file's own silence makes you late.

## The mapping: what the motion tells the sound

Once timing is right, let the motion set the rest.

| The motion says | The sound does |
|---|---|
| **Size** | Pitch and decay. A 60 px chip rings near 5 kHz for about 165 ms; a 460 px headline near 1.1 kHz for 600 ms. |
| **Position left to right** | Stereo pan, gently, never hard left or right. In films only; product UI stays mono. |
| **Height on screen** | Brightness of the attack. Higher sounds brighter. |
| **Direction** | Contour. Leaving upward, the sweep goes up. |
| **Duration and ease** | Length of a whoosh, and where it peaks. |
| **A stepped reveal** | One click per step. Words at 110 ms each, cells at 210 ms. The rhythm *is* the sound. |

The tile at the top uses the first row directly: pitch is `5200 × (60 / size)^0.75` Hz, and decay grows with size.

## Fewer sounds than tweens

Deriving is not the same as sonifying everything. A scene with forty moving parts does not get forty sounds. It gets the few the eye actually follows. Two things landing on the same frame are one sound: voice the bigger one.

For films, write the sound as data first: a cue sheet (A list written before any audio is made: one row per visual event, with its time, kind of sound, size and position, so the sound can be placed exactly), one row per visual event with its time, size and position. Lock the picture before you place anything, because a moved frame moves every cue after it.

**Sync a sound to its animation (paste into your agent):**

```text
Wire the "land" sound to the card's enter animation so it confirms the landing.
Find the contact frame: for this ease-out, about 85% of the duration (or the transform's transitionend). The sound's attack goes there, 0-30 ms late at most, never early.
For springs, use the first crossing of the rest position, not animationend.
Trim leading silence from the file to zero.
Derive pitch from size: small element = higher and shorter. One sound per event the eye tracks, not one per tween.
```

**Check yourself:** A drawer slides in over 400 ms with a strong ease-out. When should its soft landing sound fire?

- [ ] At 0 ms, when the click starts the motion
- [x] About 340 ms in, when it visibly arrives, or a frame after
- [ ] At 400 ms plus the spring's settle time
- [ ] 100 ms before it arrives, so it feels snappy

*Why:* The attack belongs on the contact frame: for a strong ease-out, about 85% of the duration. A little late is fine; early reads as broken.

## In the skill

- https://school.agents.org.in/skill/sound/sound-motion-sync.md
- https://school.agents.org.in/skill/sound/sound-from-motion.md


---

# Playing and making sounds

> Wire sound into a web app the right way (unlock on a gesture, off by default, a visual twin for every sound) and make the files with code, a model or a free library.

Module: Sound · Level: intermediate · 7 min · https://school.agents.org.in/media/sound/sound-on-the-web

Here is a tiny chat. Send a message with sound off: you still see it land, with "Sent" under it. Turn sound effects on in the setting above and send again: now a two-note tone joins the checkmark. Reload the page and the setting is still how you left it.

*[Interactive demo: Off by default, remembered, and every sound has a visual twin.]*

That small demo holds the five rules for sound on the web. Each one exists because someone shipped the opposite.

## The five rules

1. **Off by default, with a switch people can find.** In settings, reachable by keyboard, and remembered across visits. This is why sound on the web is trusted at all.
2. **No sound-only information.** Every sound has a visual twin: the checkmark, the toast, the shake. Most people are muted most of the time.
3. **No autoplay, no sound on page load.** A sound the person did not cause is a notification, and notifications need permission.
4. **Respect the device's silence.** On iPhone, the silent switch mutes sounds like yours. Do not play UI sounds in a hidden tab.
5. **Treat the mute switch as reduced motion for your ears.** There is no CSS setting for sound. If someone has asked for reduced motion, keep sound off and skip any "turn on sound?" prompt.

> **In plain words.** Browsers play sound through an AudioContext (The browser's little audio engine that a page creates to play sound. It starts paused until the person clicks or presses a key), a little audio engine your page creates. Browsers keep it **suspended** until the person clicks or presses a key, so pages cannot blast noise at you. Your code has to wake it up inside that first click.

## The bug everyone hits first

Create the audio engine when the page loads and it seems to work, except nothing plays. It starts suspended, and every sound is silently dropped until something wakes it. The symptom: "sound only works on the second click."

**Before:** created at load, on by default, sound on focus.

```ts
// starts suspended
const ctx = new AudioContext();
let enabled = true;
// talks over screen readers
button.onfocus = () =>
  play("tick");
```

**After:** created in the gesture, off until chosen.

```ts
let ctx: AudioContext | null = null;
const saved =
  localStorage.getItem("sound");
let enabled = saved === "on";

// call inside a click or keydown
function unlock() {
  ctx ??= new AudioContext();
  if (ctx.state === "suspended") {
    ctx.resume();
  }
}
button.onclick = () => {
  unlock();
  if (enabled) play("send");
};
```

Two more details. Play each sound from a fresh `AudioBufferSourceNode`; they are cheap, and it is the only web primitive with no start-up delay (an `<audio>` element adds tens of milliseconds). And never attach sound to focus: screen readers are already speaking when focus moves, and your tick lands on top of their voice.

*("Works on the second click" means the context was created at load. Create it in the gesture.)*

## Where the sounds come from

Three paths without paying anyone, and one with a key. Pick by what you need, not by what happens to be installed.

| You need | Path |
|---|---|
| Sounds right now, no files, tweakable in code | **Synthesize them.** A few lines of Web Audio, or ZzFX (under 1 KB) |
| Safe, decent sounds today | **A free library** with a CC0 (A licence that gives a work to the public with no conditions: you may use, edit and sell it without credit. The safest licence for sounds) licence: Kenney's interface packs, soundcn, Freesound |
| A custom material that matches the brand | **An open-weight model** like Stable Audio 3 Small-SFX, which runs on a laptop CPU |
| Iterate in words ("more felt, less glass") | **ElevenLabs** sound effects, with an API key |

### Synthesis

Every sound in this module is procedural (Sound built by code from oscillators and envelopes instead of loaded from a file. Weighs nothing and its pitch and length can follow the UI): built in code, zero bytes of audio. For ticks, taps and two-note tones it beats any model. It is the same every time, and its pitch and length are numbers you can tie to the UI.

```ts
// A tick: 60 ms of high sine, fast decay.
// No file, no fetch.
function tick(ctx: AudioContext,
  freq = 2400, ms = 60) {
  const t = ctx.currentTime;
  const end = t + ms / 1000;
  const o = ctx.createOscillator();
  const g = ctx.createGain();
  o.frequency.value = freq;
  g.gain.setValueAtTime(0.5, t);
  g.gain.exponentialRampToValueAtTime(
    0.001, end);
  o.connect(g).connect(ctx.destination);
  o.start(t);
  o.stop(end);
}
```

### Libraries

Kenney's packs are the most used free UI sounds in existence, which is also their problem: people recognize them. Use them as a starting family and re-pitch or layer. Whatever the source, keep one material. A grab-bag of free sounds is the audio version of the default icon set.

### Models

Prompt any sound model with **material + action + envelope + contour + purpose**, never a UI noun. "A success sound" gets you a stock jingle. This gets you a sound:

**Generate a sound family with ElevenLabs (paste into your agent):**

```text
Generate this family with ElevenLabs text-to-sound-effects (model eleven_text_to_sound_v2), all in one session with the same material phrase:
tick: "single very short tick, fingernail on thin plastic, dry, no tail, bright, one-shot UI feedback"
send: "two quick rising notes on a muted kalimba, warm, dry, short decay, confirmation"
error: "short low dull thud with a slight downward pitch, rubber on wood, dry, no tail"
Use duration_seconds 0.5 and prompt_influence 0.8 or higher. Then trim leading silence to zero, high-pass at 150 Hz, peak at -3 dBFS, mono. Commit the files and record the prompts and settings in a manifest.
```

Say "dry" and "no tail" every time; the default has room echo. Ask for half a second and cut it down. And commit the files: regenerating one sound next month gives you a *different* sound, and a family with one re-rolled member sounds patched.

> **Open source is not the same as commercial use.** Meta's AudioGen code is open, but its weights are non-commercial. Stability's licence changes above US $1M a year in revenue. Some sound packs mix free and restricted files. Write the licence next to every file.

**Check yourself:** Sound plays on the second click but never on the first. What is the likely cause?

- [ ] The file is too large
- [x] The AudioContext was created at page load, so it started suspended and only woke up later
- [ ] The sound is mono
- [ ] The volume is set to -3 dBFS

*Why:* Browsers start an AudioContext suspended until a user gesture. Create and resume it inside the click or keydown handler, before playing.

## In the skill

- https://school.agents.org.in/skill/sound/sound-playback-web.md
- https://school.agents.org.in/skill/sound/sound-generation-open-source.md
- https://school.agents.org.in/skill/sound/sound-generation-elevenlabs.md


---

# Sound in launch films

> Pick a sound register for a launch film, place the track's structure on the picture, take sound away before the reveal, and only use recordings you are licensed to.

Module: Sound · Level: advanced · 8 min · https://school.agents.org.in/media/sound/sound-in-launch-films

Same 12-second film, five soundtracks. Click through them below. They sound nothing alike, and yet every one does two things: its structure sits on what the picture is doing, and something drops out just before the name appears.

*[Interactive demo: One picture, five sound registers. The dashed line is where the product's name lands.]*

A launch film flips everything from the first four lessons. In the product, sound is opt-in, rare and quiet. In a short film that introduces the product, sound is the emotional layer, and silence is a material you place on purpose.

## Five registers

A sound register (The overall kind of soundtrack a film uses, such as a dry reveal, a low drone with clicks, a beat track, a voice, or soft foley) is the overall kind of soundtrack. Measurements across dozens of launch films found five that read as crafted. Choose from the picture, not from taste.

| Register | What carries it | Use it for |
|---|---|---|
| **A · Dry reveal** | Hits only, each on a motion peak; true silence between | Logo reveals and stings under 15 seconds |
| **B · One-key bed + clicks** | A low drone in one key, dry clicks on stepped reveals | Brand films and kinetic type (OpenAI's *Refreshed.* and GPT-5 films) |
| **C · Beat track** | A track whose drop lands on the reveal | UI motion, keynote cards, fast sizzles |
| **D · Voice-led** | The voice, with the music 10 to 13 dB under it | Founder-led and live-action films |
| **E · Product camera + foley** | A soft pad, plus foley (Everyday sounds added to match on-screen actions, like keys clicking or a button pressing. In a product film they get louder the more an action matters) louder the more an action matters | The real product, one action at a time (Linear, Raycast) |

Linear's agent film shows that last idea well: typing sits just over the pad, pressing Enter is louder, and Send is loudest of all. The level follows the consequence.

## Take something away before the reveal

**Most crafted films go quiet for a moment right before the name.** 23 of 28 films from one studio did it; so did 14 of 18 acclaimed launches. The gap is short, usually 0.1 to 0.9 seconds, often just the low end pulled out, and it ends exactly on the reveal frame.

The picture does the same thing at the same moment: a beat of stillness. Silence plus stillness is what makes the name land.

*(The gap must line up with something you can see. Phones cannot play the low end, so a bass-only gap is invisible there.)*

## Placing a music track

A track is placed by the picture's structure, not laid under it from second zero.

- **Put the drop on the reveal.** The drop (The moment in a music track where the beat and bass come in hard. In a launch film it should land on the reveal of the name) (where the beat and bass come in hard) lands on the brand or title. Move the track, not the picture: trim the file's start so its drop plays on the reveal frame.
- **Break down under reading.** When the viewer has to read (typing, a headline, a diagram), the beat thins or the bass leaves.
- **Lock only what you choose.** Pick one layer to sync, act changes or the beat grid, and land it on the beat. Or lock nothing. A cut a few frames off a strong beat reads as a miss.
- **Edit on bars, stay in key.** Shorten a track by cutting whole bars from the middle. Anything tonal you add on top, a chime or a boom, sits in the track's key.
- **Under a voice, the voice is the clock.** Time scenes to the spoken words, leave a short silence before key lines, and keep the music 10 to 13 dB below.

> **The AI default.** An unstructured bed (Music or a drone that runs quietly underneath a whole film or section. A bed laid flat from the first second with nothing placed on the reveal is the default mistake): one stock track started at 0 s, at one level, with no drop on the reveal, nothing taken away, and cuts landing wherever. If nothing in the track can go on the reveal, use register B's drone or register A's silence instead.

## Loudness, stated on purpose

There is one fixed number: **−1 dBTP** true peak (The highest point the audio wave really reaches, including between samples. Keep it at or below −1 dBTP so it never clips on playback), so nothing distorts on any device. Overall loudness is a choice per register. Acclaimed films sit around −18 LUFS in the middle; short beat-cut sizzles run hotter, around −13; long, calm product walkthroughs go much quieter. Write the target next to the register. And listen once on phone speakers before you ship.

## Can you use that recording?

A recording in a launch film is used **commercially**, **edited** (trimming and syncing counts) and **published** where platforms scan audio. Its licence has to allow all three.

*[Interactive demo: Pick a licence. Commercial, edited and published: it has to allow all three.]*

- **Yes:** CC0 (A licence that gives a work to the public with no conditions: you may use, edit and sell it without credit. The safest licence for sounds); CC BY with a full credit (title, author, link, licence, "edited"); Mixkit's and Pixabay's free licences.
- **No:** anything NC (non-commercial), ND (no edits), SA (your film would inherit the licence), "royalty free" with no licence text, and any case where two sources disagree.
- **Read the item's own page.** On Freesound, licences are per sound, not per person. Aggregator sites are not authoritative. Save the licence text, link, author and date next to the file.
- **Watch for Content ID (YouTube's system that scans uploads for registered music and files automatic claims, sometimes even when your use is properly licensed).** Some composers register their CC BY tracks, so a correct use can still get an automatic claim. Credit in the description and dispute citing the licence.

Sounds you rendered in code are yours.

## Giving your agent ears

An agent cannot hear what it makes, so it should never describe how audio sounds without a measurement behind it. Leon Lin's **earshot** is that measurement: a local tool that describes a file, finds and trims the right take, lays out a film's sound on a timeline, and renders effects from code. Let it do the listening; keep the decisions (register, placement, loudness) with the rules above. Where earshot's defaults differ, such as letting sound lead by a frame or a −14 LUFS target, the measured rules here win.

**Plan a launch film's sound (paste into your agent):**

```text
Plan the sound for this launch film before rendering any audio. Lock picture first.
1. Pick a register from the picture: A dry reveal, B one-key bed + clicks, C beat track, D voice-led, E product camera + foley. Write its loudness target next to it; true peak is always -1 dBTP.
2. Mark the reveal frame. Take something away 0.1-0.9 s before it (the low end or the whole mix) and end the gap on the reveal, with a beat of visual stillness.
3. If there is a track: trim its head so the drop lands on the reveal, break the beat down under anything the viewer reads, lock one layer to the beat or none.
4. List every recording with its licence read from the item's own page (CC0 or CC BY only; no NC, ND or SA), URL, author and date.
Show me the plan as a table of times before mixing.
```

**Check yourself:** A film uses a great track under the whole thing, from 0 s to the end, at one level. The name appears at 21 s. What is the first fix?

- [ ] Make the track louder
- [x] Trim the track's start so its drop lands on the name, and take sound away for a moment just before it
- [ ] Add clicks from a free pack over every cut
- [ ] Switch to a CC BY-NC track

*Why:* A flat bed from 0 s is the unstructured default. Move the track so its drop lands on the reveal, and leave a short gap before it. Library blips over a beat track and non-commercial licences are both mistakes.

## In the skill

- https://school.agents.org.in/skill/sound/launch-video-sound.md
- https://school.agents.org.in/skill/sound/launch-video-music.md
- https://school.agents.org.in/skill/sound/sound-licensing.md
- https://school.agents.org.in/skill/sound/earshot-reconciliation.md


---

# Pick the register first

> Name the kind of launch film you are making before anything else, so cut rate, stillness and loudness all have a target.

Module: Launch films · Level: beginner · 7 min · https://school.agents.org.in/media/film/register-first

Here is one moment, "the agent opens a pull request", shot three ways. Same product, same beat. Flip between them.

*[Interactive demo: One beat in three registers. The numbers under the frame come with the register, not with the quality.]*

None of the three is better. They are different **kinds** of film, and each kind has its own measured habits: how often it cuts, how much of it sits still, how loud it is. That kind is called the register (The kind of launch film you are making, such as UI motion graphics, product camera or kinetic type. It sets the cut rate, stillness and loudness), and it is the first decision on any launch film (The short video, usually 30 to 90 seconds, a company posts when it ships a product or feature).

> **In plain words.** A **launch film** is the short video (usually 30 to 90 seconds) a company posts when it ships something. A **register** is its genre: a lively motion-graphics piece, a calm close-up of the real product, a film made of words. Pick the genre, and most of the numbers pick themselves.

## Why register comes before everything

Every number later in this module depends on it. Four cuts a minute is normal for one kind of film and sluggish for another. Half the frames sitting still is calm in one register and dead in the next.

The numbers here come from a measured set of 47 launch films: 29 made by the studio Skale, and 18 acclaimed films from 2024 to 2026 by Linear, Raycast, Anthropic, Figma, Apple, Notion, Google and others. Each was timed frame by frame at its own frame rate, with cuts counted by eye.

| Register | Typical films | Cuts a minute | Still share |
|---|---|---|---|
| UI motion graphics | Skale's Replit, Bolt, Aside films | 4 | 9 % |
| Product camera | Linear Agent, Raycast, Claude Cowork | 14 | 49 % |
| Keynote cards + demo | Cursor 2.0, Notion Mail, Granola 2.0 | 12 | 37 % |
| Kinetic type | Claude Opus 4.6, the Gemini app | 3.3 | 17 % |
| Beat-cut sizzle | Figma glass, Arc on Windows, Spline Hana | 31 | 29 % |
| Founder-led hybrid | Typesafe, Browserbase | ~13 or 0-4 | 18 % |

Cut rate (How many times a minute the picture jumps to a new shot. About 4 in UI motion films, 14 in product-camera films, 31 in beat-cut sizzles) is how many times a minute the picture jumps to a new shot. Still share (The fraction of a film's frames where nothing visible changes. A blinking caret or a moving cursor counts as motion) is the fraction of frames where nothing visible changes at all; even a blinking caret counts as motion.

*(Never average two rows into a target. Half UI motion, half product camera gives a film neither register makes.)*

## How to pick

You rarely need a long debate. The product and the brief decide it.

  **Step: A known brand, the product is the hero.** Product camera. If the demo needs chapters, keynote cards with demos between them.

  **Step: A feed sizzle under 40 seconds, every shot a live component.** Beat-cut sizzle. These are also the loudest films in the set.

  **Step: A founder explains the product or a raise.** Founder-led hybrid, carried by the voice.

  **Step: The words are the product, like a model launch.** Kinetic type.

  **Step: None of these: a new product, no founder on camera, no brief.** UI motion graphics, the biggest measured row (18 of Skale's 29), with the name said early.

A second register can ride inside the first, like a material act inside Framer 3.0's tagline. Take the numbers from the main one.

## What every register keeps

Some rules held across all 47 films, whatever the genre. These are your floor.

- **No dead cut (A cut from a settled frame to a new scene that just waits. Neither side is moving, so the film stalls. It fails in every register).** Never cut from a settled frame to a scene that just waits. One side of the cut should be moving.
- **Nothing decorative moves on UI.** No floating, pulsing or breathing cards in any of the 18 acclaimed films. A slow camera drift under a hold is fine.
- **Words are built, not faded in.** 27 of 29 Skale films build copy word by word or letter by letter.
- **The bass arrives on the first reveal.** The music's low end lands with the name or the first big reveal.
- **Silence punctuates.** A pulled bass or a gap before a reveal, in 23 of 28 Skale films with sound.

**Cut rate, stillness and loudness are properties of the register, not marks of quality.** A calm product-camera film judged against a lively motion-graphics one looks too still. It is not; it is a different film.

## The order of decisions

Once the register is named, the rest of a film is a chain of decisions, each with its own owner. This module walks it in order.

1. **Register** (this lesson).
2. **Beats:** when the first word, the name and the endcard land.
3. **Words and product:** type cadence, the cursor, typed prompts.
4. **Cuts, seams and motion:** how one shot hands over to the next.
5. **Sound:** covered in the Sound module.
6. **Review:** check the render against its own register, then ship the loop, sting and vertical cuts.

A tool such as HeyGen's HyperFrames builds the film from code. The design-engineering skill decides it: register, beats, words and sound.

**Start a launch film with the register (paste into your agent):**

```text
Before you storyboard, name the launch-film register for this product
(UI motion graphics, product camera, keynote cards + demo, kinetic type,
beat-cut sizzle or founder-led hybrid) and say why in one line.
Then state the target cut rate and still share from that register's row,
and keep every later decision inside it.
```

**Check yourself:** A brand-new developer tool, no founder on camera, no brief. Which register is the safe default?

- [ ] Product camera, because it is calm
- [x] UI motion graphics, with the name early
- [ ] Beat-cut sizzle, because feeds reward energy
- [ ] An average of all the registers

*Why:* With nothing else to go on, the largest measured row is UI motion graphics (18 of Skale's 29 films). An unknown product names itself early, not at 70 %.

## In the skill

- https://school.agents.org.in/skill/launch-video/MOC-launch-video.md
- https://school.agents.org.in/skill/launch-video/launch-video-registers.md


---

# Time the beats

> Place the hook, the name, the proof and the endcard at the moments real launch films use, and know which of three name modes you are in.

Module: Launch films · Level: beginner · 7 min · https://school.agents.org.in/media/film/beats

Drag the playhead. This is a 60-second film for a made-up product called Relay, timed to the measured medians. Every frame in the preview is drawn from the time alone.

*[Interactive demo: Scrub, or tap a beat. The blue ticks mark the first word, the name and the final hold.]*

A launch film's structure is not a matter of taste. Across 47 measured films, the same moments land in the same places. Each section is a beat (A stretch of a film with one job: the hook, the problem, the name, the product, the proof, the endcard): a stretch of the film with one job.

## The first two seconds

Put a readable word on screen by **0.2 to 0.7 seconds**. Skale's films median 0.2 s; the acclaimed films 0.68 s, and six of them show a word on frame zero. The first scene change, the first seam (Any change from one scene to the next, whether a cut, a fill or a morph. A good seam reads as one continuous move), lands by about **2 seconds**.

That opening is the hook (The first two or three seconds of a film, which decide whether anyone keeps watching. A readable word should be on screen by 0.2 to 0.7 seconds). In the acclaimed films it is usually the product already working (five films), a claim or question (seven), or the brand mark (three).

> **The poster frame trick.** 9 of Skale's 29 films bake 1 to 4 frames of the finished title at t = 0, then cut. That frame becomes the autoplay thumbnail, at a cost of about a tenth of a second. It is called a poster frame (The frame shown before a video plays, as its thumbnail. Some films bake 1 to 4 frames of the finished title at the very start so the thumbnail names the product).

## When to say the name

This is the beat people get wrong most. There are three measured modes. Pick one and commit.

| Mode | When the name lands | Who does it |
|---|---|---|
| Early | by 11 % of the runtime | brand-first films, either corpus |
| End of the problem act | median 29 % (6-51 %) | problem-first films |
| Late | around 70 %, after the demo | known brands only: Apple, Arc, Anthropic, Figma, Raycast |

**Five of the six late-name films come from brands the audience already knew.** Holding an unknown product's name back for 70 % of the film means most viewers leave without hearing it.

*(Don't time a problem act to 15 %. That number mixes all the films that were not brand-first. A problem act runs to the name: median 29 %, never much past half.)*

The music agrees with the name. The **low end** (the bass) lands on the first reveal: in 12 of 18 Skale music films and 10 of 18 acclaimed ones.

## The body: show, or prove

Problem-first films tend to prove: 9 of Skale's 13 carry a proof beat, usually a counter rolling up to one number. Product-first films tend to show the product working instead.

Five acclaimed films land a **payoff** just before the endcard: the finished result or the user's reaction, 0.1 to 1.2 seconds before the cut. Cursor 2.0's hot reload lands 1.2 s before; Notion Mail ends on Send.

  **Before (avoid):**

The demo ends mid-task, then the logo fades up over a blank frame.

  **After (do this):**

The pull request merges, a beat later the mark grows from a dot, the wordmark lands, the last two seconds hold still.

## The endcard

The endcard (The closing frame of a film with the mark, the name and maybe a URL. In measured films it runs 4.4 to 6.3 seconds and its last 2 seconds hold still) is the closing frame: the mark, the name, maybe a URL. It runs longer than people expect.

- **Length:** 4.4 s (Skale) to 6.3 s (acclaimed).
- **Final hold:** about 2 seconds where nothing changes, so it can be read and screenshotted.
- **A URL only with a verb**, and only when there is somewhere to go. "Try it at relay.dev", not a bare address. Only about half of Skale's endcards carry a URL at all.
- **Build the mark** from a primitive or the film's own objects. Five acclaimed films grow it from a single dot; Claude Cowork blooms its mark out of the button the user just clicked.
- **Bookend:** repeat the opening move at the end (7 of 29 Skale, 10 of 18 acclaimed).

## A series is a template

Replit shipped three films in a month on one kit: red keywords, a cream canvas, named cursors, the same lockup hold. To make the next film in a series, write the new product into the slots and keep every frame number, ease and cut.

**Storyboard to measured beats (paste into your agent):**

```text
Storyboard this launch film (register: <name it>) with timestamps:
- a readable word by 0.7 s, the first seam by about 2 s
- the name in ONE mode: early (by 11 % of runtime), at the end of the
  problem act (about 29 %), or late (~70 %, only for a known brand)
- the music's low end on the first reveal
- a payoff just before the endcard
- an endcard of 4.4-6.3 s that holds still for the last ~2 s;
  a URL only with a verb ("Try it at ...")
Show the name time as a percentage of the runtime.
```

**Check yourself:** A 60-second film for a product nobody has heard of names itself at 42 seconds. What is wrong?

- [ ] Nothing, late names build suspense
- [x] 42 s is 70 %: the late mode is only seen from brands people already know
- [ ] The name should be at exactly 15 %
- [ ] It should never name the product before the endcard

*Why:* The late-name mode (~70 %) comes almost entirely from known brands. An unknown product should name itself early or where its problem act ends, around 29 %.

## In the skill

- https://school.agents.org.in/skill/launch-video/launch-video-structure.md


---

# Words and product on screen

> Build on-screen type at a readable cadence, and drive the product UI with a cursor, typed prompts and an honest wait.

Module: Launch films · Level: intermediate · 8 min · https://school.agents.org.in/media/film/words-and-product

Move the slider. The same line builds word by word at whatever pace you set, and the newest word lands blue before it settles to ink.

*[Interactive demo: Word stagger from 0.05 s to 0.9 s. Try 0.2 s, then tick “Fade as a block”.]*

At 0.05 s the line is a blur; at 0.9 s it is a voice speaking slowly. Around 0.2 s it reads as it builds. That gap between words is the word stagger (The gap between one word appearing and the next when a line builds word by word. About 0.2 seconds in a music film), and in a music-only film it is the edit itself: the type is the script.

## Build words, never fade a block

A sentence faded in as one block reads as a slide. 27 of Skale's 29 films build copy per word or per letter; the two exceptions are a phrase-caption film and a CG teaser with no copy at all.

| Job | Stagger |
|---|---|
| Music film | 0.13-0.2 s (median 0.18 s) |
| Captions synced to a voice | ~0.3 s |
| One word per beat of the track | 0.4-0.5 s (Arc on Windows) |
| The type is the voice | 0.5-0.9 s |

**Mark the newest word.** 15 of 29 Skale films land each new word in an accent or a pale grey, then settle it to ink within **0.13 to 0.3 seconds**. The eye always knows where the sentence has got to.

**Hold it long enough.** After the last word, keep the line up **1.15 s** (Skale's median); acclaimed title cards stay about 1.5 s in all. That is the hold (How long a line stays on screen after its last word lands, so it can be read. About 1.15 to 1.5 seconds in launch films).

*(Elements in a list can cascade fast (around 0.05 s apart). A sentence someone has to read cannot. Don't squeeze one into the other.)*

## Size and the slam

Statements in these films are small: about **5 % of frame height**, around 54 px on a 1080 px frame. They caption the UI; they are not headlines. Body text never goes below about 20 px at 1080p.

Six of Skale's films use one slam word (One word that arrives huge, a quarter of the frame or more, holds a moment, then drops into its sentence. Used once in a film, if at all): a single word arriving huge (25-61 % of the frame), holding a few frames, then dropping into its sentence. Replit Slides holds its slam about 0.23 s, then drops it from 5× to 1× in one frame. Once per film is plenty.

## The product as a character

Now the product itself. In most films the interface is **rebuilt as vector**, not screen-recorded (22 of 29 Skale films, 11 of 18 acclaimed), then cropped tight on the one control that matters. Eight acclaimed films use a macro crop (Framing a single control, like one button or field, so it fills the frame, instead of showing the whole app): one button or field filling the frame.

  **Before (avoid):**

A full-screen recording of the app, a tiny macOS pointer wandering across it, every panel in full colour.

  **After (do this):**

The one panel rebuilt and cropped tight, an oversized cursor entering from below, colour saved for the state change.

**Colour is saved for state.** Raycast floods its dialog green only on save; Linear Agent shows colour only in issue icons.

## The cursor

A cursor appears in 17 of 29 Skale films and 11 of 18 acclaimed ones. When it does, it must read at video size.

- **Size:** the oversized cursor (A pointer drawn far bigger than on a real screen, about 7 % of the frame's width, so it reads in a video), about 7 % of the frame's width (≈134 px on a 1920 frame), or a normal pointer inside a crop cut in until it reads (Cursor 2.0 cuts about 3×).
- **Entry:** from off-frame, on one decelerating glide of 0.4-0.9 s. The tip lands on the target.
- **Press:** shrink to 0.84 over 0.1 s, back over 0.22 s. The button dips with it.
- **Exit:** off the nearest edge, or as the carrier into the next shot. Never faded out in place.
- **Cause, then effect.** Nothing starts on its own; the click causes the next beat.

## Typed prompts: read or recognise

How fast should a prompt type? It depends on its job.

| The viewer must… | Speed | Seen in |
|---|---|---|
| **read** it (it carries the argument) | ~15 characters a second | Raycast 10, Notion 3.0 12-15, Cursor 2.0 20 |
| **recognise** it (only its shape matters) | ~53 characters a second | Skale's films |
| skim title cards | 40-65 characters a second | Cursor 2.0's cards |

A 33-character prompt meant to be read types for about 2.2 seconds, then holds 1.15-1.5 s. Keys land unevenly, slower at the ends, with a little extra time after spaces.

## Human and agent, told apart

In an AI product, show who is speaking. Framer 3.0 types the human at 21 characters a second and the AI at 160 after a 0.9 s thinking state. Linear Agent gives the human a caret and resolves the agent's rows from blur 0.25 s apart. Claude Cowork sets the agent in a serif and the user in grey sans pills.

**The agent's wait is either honest stepped state or it is cut past.** Notion 3.0 steps a counter 1 → 61 while the agent works; Linear Agent simply cuts to the prompt already posted, the answer half a second later.

**Type and UI for a launch film (paste into your agent):**

```text
For every line of on-screen copy: build it per word at ~0.2 s (music)
or ~0.3 s (synced to voice), land the newest word in the accent and
settle it to ink within 0.3 s, hold 1.15-1.5 s after the last word.
Statements at ~5 % of frame height. No block fades.
Product: rebuild the UI as vector and crop to the control that matters.
Cursor ~7 % of frame width, enters from off-frame, presses (0.84 scale,
0.1 s down / 0.22 s up), exits off an edge. Prompts to be read type at
~15 chars/s. Show the agent's wait as stepped state, or cut past it.
```

**Check yourself:** A prompt in your film carries the whole argument: “Find every flaky test and fix it.” How fast should it type?

- [x] About 15 characters a second, so it can be read
- [ ] About 53 characters a second, the Skale median
- [ ] Instantly, as one block
- [ ] As fast as the agent's reply

*Why:* A prompt the viewer must read types at about 15 characters a second. 53 is for prompts that only need to be recognised by their shape.

## In the skill

- https://school.agents.org.in/skill/launch-video/launch-video-type.md
- https://school.agents.org.in/skill/launch-video/launch-video-ui-demo.md


---

# Cuts, seams and motion

> Make every scene change read as one move, avoid the two cuts that fail in every register, and keep motion inside a shot calm and causal.

Module: Launch films · Level: intermediate · 8 min · https://school.agents.org.in/media/film/cuts-and-seams

Here is one scene change done three ways, laid out the way a reviewer checks it: frames a tenth of a second apart, either side of the cut.

*[Interactive demo: A contact sheet around the cut. Look for frames where nothing changes, and for a frame neither shot contains.]*

The dead cut shows identical frames on both sides of the cut. The crossfade shows a grey frame that belongs to neither shot. The cut on motion just looks like one card throwing to the next. That last one is the craft of this lesson.

## Films cut, and that is fine

A hard cut (An instant jump from one shot to the next, with no fade or transition between them. Most launch films use them) is an instant jump from one shot to the next. 26 of Skale's 29 films and 14 of the 18 acclaimed films hard-cut. Banning cuts forces every film into a continuous morph, which only five of the 47 chose.

How often a film cuts belongs to its register. Pick one below and feel the difference.

*[Interactive demo: Thirty seconds of film at each register's measured cut rate. Untick “Varied” for an even rhythm.]*

An even rhythm is a metronome. The films that feel alive vary it, and many end a run on an **accelerating ladder**: figma-launch cuts nine times on holds shrinking .8, .5, .4, .3, .2, .15, .12, .1 s, then lands on a hold.

## The two cuts that always fail

  **Step: The dead cut.** A settled frame, then a scene that waits. The outgoing shot is not moving on the cut frame, and the incoming one does nothing for more than ~0.3-0.4 s. The film stalls.

  **Step: The crossfade dip.** Both scenes fade at once, so at the midpoint whatever is underneath shows through: the page's white, a different background. Fade through a ground both scenes share, or cut.

A crossfade (The flash you get when two scenes fade at the same time: at the midpoint, whatever is underneath (often white) shows through) is not banned. 11 of 29 Skale films dissolve or dip (0.45 s median). Adaline dips to its paper colour; Extend dips through its brand colour. Those pass because the ground is shared.

## Where the eye accepts a cut

A good cut lands where the eye is already going. The measured places:

- **On the click.** Linear Agent clears the input on Enter, cuts 0.7 s later to the prompt already posted.
- **On a constant ground.** Keep one world or layout and cut freely; Raycast cuts 9 times in 28 s.
- **On the element being read.** A 2-3× scale jump of the same thing, anchored so it lands where it really sits.
- **Into motion.** Cut mid-drift on a moving camera, or onto things already travelling.
- **Inside a fill.** The Gemini app hides all four of its cuts in full-frame glyph fills.
- **To a still frame that acts within a beat** (0.3-0.4 s).

## Seams: what carries the eye

A seam (Any change from one scene to the next, whether a cut, a fill or a morph. A good seam reads as one continuous move) is any scene change, cut or not. It reads as one move when the outgoing element is still travelling on the last frame and the incoming one carries on: same axis, same direction, matched speed.

**One current per film.** Pick one dominant direction (leftward is a common default) and spend the others on purpose: up for a conclusion, a pull back for an arrival.

| Seam | How it works | Timing |
|---|---|---|
| Fill the frame | a pressed button or shape grows until its colour is the next ground | 0.33 s median |
| Zoom-through (A seam where the outgoing scene pushes in and blurs, the scene swaps, and the new one arrives from slightly smaller, as if the camera flew through) | out 1 → 1.2 with blur, hard swap, in from 0.75 | out 0.2 s, in 0.5 s |
| Zoom into type | letters grow 4-6× until they fill the frame | 0.33 s median |
| Shared element | one object carries across: a pill becomes a progress bar becomes a chart bar | ~2 s chains |

The thing that crosses is the carrier (The thing that crosses a scene change and carries the eye with it: a button filling the frame, a dot, a line that becomes a chart). Use **2-3 seam types** per film, one of them for 60-70 % of the changes.

*(Write the seams down before animating: one row per seam, typed cut, match-cut or morph, with what exits and what enters.)*

## Motion inside a shot

Inside a shot, things arrive on a fast exponential settle and leave on the mirror curve. The speed of a settle is its time constant (How fast an exponential settle comes to rest. A smaller tau settles faster; about 0.1 to 0.15 seconds for an object in a film): about **0.1-0.15 s** for an object, slower for long travel (Raycast's big crane ~0.43 s, counters 0.6-0.8 s).

- **Nothing decorative moves on UI.** No float, pulse or breathe. A slow camera creep under a hold is fine.
- **Overshoot is a register,** not a default: only 4 of 29 Skale films and 3 of 18 acclaimed show visible bounce. Never spring a camera.
- **Blur is a choice.** Velocity blur peaks around 10 px on text, 18-20 px on full-frame surfaces. Plenty of films whip completely sharp.
- **Decisions are stamped, not tweened.** A typed letter, a selection hop, a page swap happens in one frame.
- **A cause launches its effect.** Click, squash, release, impact. That is what makes a film feel lively, not bounce.

**Cut and seam a launch film (paste into your agent):**

```text
Plan every scene change before animating. For each seam write one row:
type (cut / match-cut / morph), what exits and what enters, axis and
direction. Rules: no dead cuts (one side always moving, or the new shot
acts within 0.3-0.4 s); no crossfade between two opaque scenes unless
they share a ground; one dominant direction for ordinary seams; 2-3
seam types, one primary. Inside shots: exponential settles (tau
0.1-0.15 s for objects), nothing decorative moving on UI, no spring on
the camera.
```

**Check yourself:** Two dark scenes on a white page are crossfaded over 0.6 s. What does the viewer see at the midpoint?

- [ ] A smooth blend of the two scenes
- [x] A grey flash, because the white page shows through
- [ ] Nothing, crossfades are invisible
- [ ] The first scene frozen

*Why:* When both scenes fade at once, their summed opacity drops below 1 and the page underneath shows through: the crossfade dip. Fade through a ground both scenes share, or hard-cut.

## In the skill

- https://school.agents.org.in/skill/launch-video/launch-video-cuts.md
- https://school.agents.org.in/skill/launch-video/launch-video-seams.md
- https://school.agents.org.in/skill/launch-video/launch-video-motion.md


---

# Review, render and ship

> Review a render against its own register, render it from code without the classic traps, and ship the loop, sting and vertical cuts that go with it.

Module: Launch films · Level: advanced · 9 min · https://school.agents.org.in/media/film/review-and-ship

Your film is going to the feeds. Here is the 16:9 master turned into a 9:16 phone cut two ways. Tick the overlay to see what the app's own buttons cover.

*[Interactive demo: A centre crop against a re-layout, inside the feed's safe zones.]*

The crop cuts the line in half and loses the product. The re-layout is a new composition on the same timeline. That is the last step of a film. Before it comes the review.

## Review against its own register

Numbers first, taste second. A film is judged against the medians of **its own** register: a product-camera film next to a motion-graphics one always looks too still.

> **Measure honestly.** Review at the film's native frame rate (The frame rate a video was made at, like 24, 30 or 60 frames a second. Review at that rate; converting adds fake duplicate frames). Resampling a 24 fps film to 30 inserts duplicate frames that fake short moves. And recount cuts by eye on a contact sheet (A grid of still frames taken at small steps through a video, so you can see exactly what happens around a cut): the automatic cut detector agreed with a frame reading in only 9 of 29 Skale films and 5 of 18 acclaimed ones.

The skill reviews a film on eleven rows. Each row has an owner and a target.

| # | Check | Target |
|---|---|---|
| 1 | Register | named; cut rate inside its row |
| 2 | Hook | a readable word by ~1.3 s, first seam by ~3.6 s |
| 3 | Name | one measured mode; the low end on the first reveal |
| 4 | Words | built per word ~0.2 s apart; newest settles in ~0.3 s; holds 1.15-1.5 s |
| 5 | UI demo | prompts ~15 chars/s to read; cursor ~7 % of width; the wait stepped or cut |
| 6 | Motion | nothing decorative on UI; no spring on a camera |
| 7 | Seams and cuts | no dead cut; no crossfade dip; one current; 2-3 seam types |
| 8 | Endcard | 4.4-6.3 s with a ~2 s final hold; a URL only with a verb |
| 9 | Sound sync | every hit on its contact frame or one after (0-30 ms), never ahead |
| 10 | Level | ≤ −1 dBTP true peak (The highest point the audio wave really reaches, including between samples. Keep it at or below −1 dBTP so it never clips on playback); loudness in LUFS (Loudness Units relative to Full Scale: how loud a whole piece of audio sounds to a person. Closer to zero is louder; −14 is loud, −30 is quiet) stated for the register |
| 11 | Source | the film's own sound, no mix bolted on after |

Write each finding as one row: what you saw, what to change, why.

## A film is a function of time

When a film is built from code, as with HeyGen's HyperFrames (HeyGen's open tool for making videos from HTML: you write the film as a web page and it renders it frame by frame), every frame must be a pure function of the time `t`. No `Math.random()`, no clock, nothing carried from the last frame. Seeded randomness is fine.

That rule buys real motion blur. The renderer samples several moments inside each frame and averages them: a 180° shutter (Real motion blur made by sampling several moments inside each frame and averaging them, like a camera's shutter staying open), four samples a frame by default, up to 24 on a whip pan. One unseeded `Math.random` makes those samples disagree and the blur turns to noise.

*(Never let the shutter cross a hard cut. Slide it back so it ends on the cut, or the fastest frame renders sharp and stutters.)*

**Place everything on a beat grid (The music's beats as a timeline you place everything on, measured from the actual track so cuts and hits land on the beat)** taken from the actual track, not from the brief. A generated track's tempo is a hope until measured: over 50 s, a 0.15 % tempo error puts the last hits about 70 ms late, past the 30 ms sync budget.

## Delivery gates

Hand nothing over until every file passes.

  **Step: Frame count matches.** A mux with <code>-shortest</code> silently drops the last frames when the audio ends early.

  **Step: H.264 level fits.** Level ≤ 4.2 at 1080p, ≤ 5.2 at 4K. A slow preset without <code>-level</code> writes 6.0, which fails in Safari, QuickTime and on phones.

  **Step: Under 1 GB at 4K.** CRF around 12 at a slower preset; higher quality nobody can see just doubles the file.

  **Step: Decodes on the hardware decoder.** That is what phones use; a software decode passing proves nothing.

  **Step: Loudness written, −1 dBTP.** For the register, chosen on purpose. −14 LUFS is an engine default, not a standard.

## The formats around the film

A launch film rarely ships alone. Each companion breaks a different rule of the master.

- **Feature loop (A short looping video for a landing page that runs as a seamless cycle, with no visible start or end)** for the landing page: a cycle, not enter-hold-exit. The last frame flows into the first with no visible seam; frame 0 is the poster and names the product. Ship MP4/WebM; GIF only at 25 or 50 fps (GIF delays are whole hundredths, so 30 fps plays 11 % fast).
- **Logo sting (A 3 to 8 second clip that assembles the logo and holds it, played at the start or end of other videos):** 3-8 s, the name plus three words at most. Assemble the mark from 2-4 pieces with one hit on contact; hold the lockup at least 1.5 s.
- **Vertical and square:** re-lay out, never crop. In 9:16 keep text inside top 270, right 120, bottom 420 and left 64 px of a 1080 × 1920 frame, type about 1.4× its 16:9 size, captions burned in because feeds autoplay muted. Those are the safe zones (The part of a video frame that the app's own buttons, captions and headers will not cover, where text and key action must stay).

## Who owns what

Three build tools appear next to the skill. The split is the same each time: **the tool owns the build; the skill owns register, beats, type cadence and sound.**

| Tool | Owns | Where the skill's measured value wins |
|---|---|---|
| HyperFrames | the build, seam eases and zoom values inside its projects, captions | register, structure, type cadence, sound |
| cinetic (Remotion) | scaffold, render, delivery, layout and sync checks | endcard 4.4-6.3 s not 2.2 s; text hold 1.15 s not 0.77 s; crossfades allowed over a shared ground |
| scenario-kinetic-film | one-page beat-cut type films, generated music and key art | loudness is a register choice; a 9:16 cut is re-laid out, never the master centred on black |

**Inside a HyperFrames project, its seam values win; everywhere, the skill's measured beats and sound win.**

**Review a launch-film render (paste into your agent):**

```text
Review this render against its own register, at its native frame rate.
Recount cuts by eye from a contact sheet around each seam; don't trust a
detector count. Go through the 11 rows (register, hook, name, words, UI
demo, motion, seams and cuts, endcard, sound sync, level, source) and
write one Before | After | Why row per issue. Then check the delivery
gates: frame count, H.264 level, size, hardware decode, loudness and
-1 dBTP. Make the 9:16 cut as a re-layout inside the feed safe zones.
```

**Check yourself:** Your automatic cut detector reports 6 cuts in a 40-second film. What should you do before comparing it to the register's cut rate?

- [ ] Trust it, detectors are exact
- [x] Recount by eye on a contact sheet at native frame rate
- [ ] Resample the film to 30 fps and run it again
- [ ] Average it with the register median

*Why:* The detector matched a frame reading in only 9 of 29 and 5 of 18 films; it misses dark, white-on-white and scale cuts and fires on fills. Resampling adds fake frames.

## In the skill

- https://school.agents.org.in/skill/launch-video/launch-video-review.md
- https://school.agents.org.in/skill/launch-video/launch-video-formats.md
- https://school.agents.org.in/skill/launch-video/film-as-function-of-time.md
- https://school.agents.org.in/skill/launch-video/hyperframes-reconciliation.md
- https://school.agents.org.in/skill/launch-video/cinetic-reconciliation.md
- https://school.agents.org.in/skill/launch-video/scenario-kinetic-reconciliation.md


---

# Should you draw it?

> Decide whether a picture beats a sentence or a table, then pick where the picture should live by who touches it next.

Module: Diagrams · Level: beginner · 6 min · https://school.agents.org.in/visual/diagrams/should-you-draw-it

Here is the same idea said three ways. Pick a piece of content, then flip between a sentence, a table and a picture. Only one of them is the best fit each time, and it is not always the picture.

*[Interactive demo: Three plans want a table. A checkout with a retry loop wants a picture. One relationship wants a sentence.]*

A diagram is the most expensive way to say something. It costs layout, labels, a legend and a review. And a wrong one teaches the wrong structure faster than a paragraph ever could, because people trust pictures.

So the first question is never "which kind of diagram?". It is **does a reader learn more from a picture than from a paragraph or a table?**

## Picture, table or sentence

Most content falls into one of a few shapes. Each shape has a format that suits it.

| The content is… | Use |
|---|---|
| A list of things, or attributes side by side | a table or bullets |
| One relationship ("the API calls the queue") | the sentence |
| A before and after where only attributes change | a table |
| Flow, nesting, order in time, dependency, position on two axes | a diagram |
| Live numbers inside your product | a chart component, not a figure |

The test is simple. If a three-column table carries the same meaning, the table wins. If a sentence does, the sentence wins.

> **In plain words.** A diagram earns its place when the **shape** of the idea is the point: something that loops, branches, nests, or happens in order. If you could read it out as a list without losing anything, it is a list.

The trap is "before and after". If a redesign only changes attributes (a new price, a new name), that is a table. If it changes the **topology**, meaning which thing connects to which, that is a diagram.

## Who touches it next?

Once you know a picture helps, the next question decides where it lives. A picture has a surface (Where a picture lives: a finished figure, an editable canvas, a Mermaid block in the docs, or FigJam. You pick it by who touches the picture next), and you choose it by asking who will touch it next.

  **Step: Readers only read it.** A doc, a blog post, a README, a deck, a pull request. Make a finished editorial figure (A finished diagram made for people to read, in a doc, post, README or deck. It is polished once and not edited by hand afterwards): one HTML file with the drawing inside it as SVG. Cathryn Lavery's diagram-design skill draws these.

  **Step: A team keeps moving boxes.** Planning, whiteboarding, reviewing in comments. Use an editable canvas (An endless editable whiteboard, like tldraw or FigJam, where a team keeps moving boxes and arrows around after the first draft) like tldraw, where arrows stay attached when someone drags a box.

  **Step: It lives beside the code.** A picture that changes when the code changes. Write it as a Mermaid (A way to write a diagram as short text (A --> B) that a docs site turns into a picture. Good for structure kept next to code; its automatic layout looks generated in a finished figure) block in the Markdown, versioned with the text it explains.

  **Step: The team lives in Figma.** Use FigJam. Meet people where they already work.

One picture, one surface. When a canvas grows up into a published figure, redraw it from its structure. Never paste a screenshot of a whiteboard into a doc and call it the figure.

*("Make a diagram" from someone who edits it tomorrow is a canvas job.)*

## Try the whole decision

Here is that flow as three questions. Answer them for something you have been meaning to diagram.

*[Interactive demo: What it is, who touches it next and how big it is. The answer is a format and a surface, not a style.]*

Notice that three of the four first answers end in "don't draw it". That is the honest ratio. Most "can you diagram this?" requests are tables wearing a costume.

## The budget

A single figure holds about **9 boxes, 12 arrows and 2 accent colors**. That is the diagram budget (The most one figure should hold: about 9 boxes, 12 arrows and 2 accent colors. Past that, split it into an overview and a detail). Past it, the figure is really two figures: an overview that shows the big parts, and a detail that zooms into one of them.

diagram-design aims for a density of about 4 out of 10: complete enough to be correct, sparse enough that nobody needs a guide to read it. A canvas has no hard budget, but if a board needs a legend to find your way around, it needs frames (labelled groups) instead.

  **Before (avoid):**

One architecture figure with 22 boxes, 31 arrows and five highlight colors. Every part is there; nobody can find the path a request takes.

  **After (do this):**

An overview with 6 boxes (the main services), plus one detail figure for the payments service with its 8 parts. Each fits the budget and has one thing to say.

## Ask your agent the right way

Agents love to say yes to "diagram this". The skill makes them ask the cheap questions first. You can too, in the prompt.

**Decide before drawing (paste into your agent):**

```text
Before you draw anything, decide whether this should be a sentence, a table or a diagram, and say why in one line.
If it is a diagram, ask me who will touch it next (readers, a team editing it, the code, or Figma) and pick the surface from that.
Keep one figure under 9 nodes, 12 connectors and 2 accent colors; if it needs more, propose an overview and a detail instead.
```

Two gotchas to remember. A chart inside a live product is not a figure at all: it is a component with loading, empty and error states. And if the request does not say who edits the picture next, that is a question to ask, not a guess to make.

**Check yourself:** A teammate asks for a diagram comparing your three pricing plans on seats, storage and support. What should you make?

- [ ] A flowchart from Free to Pro to Team
- [x] A table with a column per plan
- [ ] An editable tldraw board
- [ ] Three boxes with the attributes inside, joined by arrows

*Why:* Attributes compared side by side are a table's job. There is no flow between the plans, so arrows would invent a relationship that isn't there.

## In the skill

- https://school.agents.org.in/skill/diagram/MOC-diagram.md
- https://school.agents.org.in/skill/diagram/diagram-decision.md


---

# Drawing the real mechanism

> Draw a figure that shows how the thing actually works, with one accent, right-angle connectors, readable labels and an SVG a screen reader can describe.

Module: Diagrams · Level: intermediate · 8 min · https://school.agents.org.in/visual/diagrams/drawing-the-mechanism

Ask an agent to "diagram how password reset works" and you will often get the vague version. Here is the real one; flip to Vague to compare.

*[Interactive demo: Same system. The vague version could describe any app. The real one shows the token, the queue, the async hop and the email that matters.]*

The vague one is not wrong, exactly. It is just empty. "Backend" and "Cloud" could be any system on earth, the arrows go every direction, and nothing tells you what happens first.

The real one **names the actual parts and shows the order they act in**. That is the whole job of a figure: show the mechanism, so a reader could rebuild it.

## Delete first

A designed diagram is mostly deletion. Before you draw, run four tests on your list of parts.

  **Step: Can a node go?.** Every node (One box (or shape) in a diagram, standing for one distinct thing: a service, a step, a person, a store of data) should be a distinct idea. A "Load balancer" nobody asked about can usually go.

  **Step: Can two nodes merge?.** If two boxes always travel together, they are one box.

  **Step: Can an arrow go?.** If the layout already implies it (left to right, top to bottom), the arrow repeats it.

  **Step: Can a label go?.** If the shape or color already says it, the word is noise.

Then hold the budget from the last lesson: about 9 nodes, 12 connectors, 2 accents and 2 callouts. The figure is done when nothing else can be removed.

## One accent, and hierarchy by treatment

A focal accent (The one bright color reserved for the one or two things a reader must see first. Everything else stays in quiet grays) is one bright color on one or two elements: the thing the reader must see first. In the password reset figure that is the email worker and the link it sends. An accent on five boxes is no accent at all.

Everything else gets a **treatment** instead of a size or a color. Identical boxes erase the hierarchy, so each kind of node looks slightly different:

| Kind | Treatment |
|---|---|
| Focal | accent tint fill, accent stroke |
| Service | white fill, ink stroke |
| Store (a database, a cache) | ink at 5% |
| External (the user, a third party) | ink at 3%, stroke at 30% |
| Optional | dashed `4,3` |

Use borders, not shadows. Nodes get a corner radius of 6, never a pill and never a soft floating card. A figure is print, so the rules for shadows in a live interface don't apply here.

*(Purple glow and rounded-everything is the generated-diagram look.)*

## The six connector rules

Connectors (A line or arrow between two boxes that says they are related: one calls, feeds, or leads to the other) are where most figures fail. These six rules come from Cathryn Lavery's diagram-design, and they are what make the right-hand picture look calm.

1. **Orthogonal (Running only across and up or down, turning at right angles, never on a slant. Diagrams with right-angle lines look drawn on purpose) only.** Right-angle elbows rounded at 8px. A straight line only when both ends share an x or a y.
2. **Labels on a mask (A small patch of background color behind a label on a line, so the line stops short of the words instead of running through them).** Each arrow label sits on a patch of paper color, 6 to 10px clear of its line, never on it. 14 characters at most.
3. **No stacked lines.** Parallel routes sit at least 12px apart. Hop at a single crossing.
4. **Own attach points.** Several arrows on one edge spread out evenly, at least 12px apart, instead of meeting at the middle.
5. **Never behind a box.** A line that passes behind a box it doesn't end at gets rerouted.
6. **Arrows before boxes.** Draw lines first, so they sit behind the nodes and no label covers a box.

  **Before (avoid):**

Diagonal lines from center to center. "request" written across the stroke. Three arrows meeting at one point on the API box. A legend floating between nodes.

  **After (do this):**

Right angles. "request" on a paper mask above the line. Three arrows spread along the edge. The legend in a strip under the figure.

Strokes are muted gray by default. The headline path gets the accent, and async or return paths are dashed (`5,4`), like the "user clicks" line looping back to the browser.

## Grid, type and legend

Put every position, size, gap and padding on a **4px grid**: if it doesn't divide by 4, nudge it. Node names go in a sans font at weight 600. Technical sublabels (ports, URLs, commands) go in mono. Never use mono as a blanket "developer" font for everything.

The legend is a horizontal strip **under** the figure, never floating among the nodes. Widen the drawing by about 60px to make room for it.

On a phone, try the demo above at a narrow width: it switches to a vertical layout, a redesign rather than a squeeze. If you can't redesign it, keep the SVG at its full width inside a small scrolling wrapper, so the figure scrolls sideways and the page doesn't.

## An SVG a screen reader can describe

A figure is an image, so it needs a name. Put `role="img"` on the SVG and point `aria-labelledby` at a `<title>` and `<desc>` (Two tags inside an SVG that name the picture and describe it in one sentence, so a screen reader can say what it shows).

```html
<svg viewBox="0 0 710 320" role="img"
     aria-labelledby="reset-flow-title reset-flow-desc">
  <title id="reset-flow-title">How a password reset works</title>
  <desc id="reset-flow-desc">The API saves a short-lived token and queues a job;
    a worker emails the link, and the user clicks it.</desc>
  <!-- arrows first, then boxes; labels as real <text> -->
</svg>
```

Three details people miss. `<title>` is the first child, before `<defs>`. Both ids carry the figure's name as a prefix (`reset-flow-title`, never a bare `title`), because two figures on one page would otherwise clash. And `<desc>` describes the content in one sentence, not the geometry ("boxes and arrows" tells nobody anything).

## When your project has a brand

If the project ships a `.design` file (a written design contract), it supplies the skin (The colors and font families a diagram borrows from your brand. The brand changes the skin; it does not change the diagram's spacing or line rules) of the figure: the paper color, the ink, the one accent and the font families. diagram-design keeps everything else: the geometry, the connector rules, the type sizes and its motion timing, because its scripts check them.

| Topic | Who wins inside a diagram-design file |
|---|---|
| Paper, accent, font families | your `.design` contract |
| Box radius, no shadows, connector rules | diagram-design |
| Small type (12px names, 9px sublabels) | diagram-design, as long as the figure isn't shown smaller than its width |
| Step-by-step reveal at 480ms | diagram-design: that is reading pace, not a slow button |
| Whether it moves at all | this skill decides; static by default |

The gotcha goes both ways. Skip diagram-design's first-run question about colors and you ship its default palette (an orange accent on warm gray) as if it were your brand. Re-skin the connector rules "to match the brand" and you break the checks it runs.

**Draw the mechanism (paste into your agent):**

```text
Draw a figure of how [system] actually works, as one HTML file with inline SVG.
Name the real parts (not "Backend" or "Cloud") and show the order they act in.
Delete first: cut any node, arrow or label the layout already implies. Max 9 nodes, 12 connectors.
One accent on the headline path only. Treatment by kind: focal tint, service white, store 5% ink, external 3%.
Orthogonal connectors (r=8), labels on paper masks 6–10px off the line, own attach points, arrows drawn before boxes.
4px grid, sans 600 names, mono only for technical sublabels, legend as a strip under the figure.
role="img" with aria-labelledby to a prefixed <title> and a one-sentence <desc>.
```

**Check yourself:** Your figure has three arrows leaving the bottom of the API box, all starting from its exact center. Which rule fixes it?

- [ ] Make the arrows diagonal so they separate
- [x] Give each connector its own attach point, spread evenly at least 12px apart
- [ ] Put a label on each line so readers can tell them apart
- [ ] Add a drop shadow to the API box

*Why:* Connectors on one edge each get their own attach point, spaced along the edge. Diagonals and labels on lines are two of the faults the rules remove.

## In the skill

- https://school.agents.org.in/skill/diagram/diagram-craft.md
- https://school.agents.org.in/skill/diagram/diagram-design-reconciliation.md


---

# Editable canvases

> Build a whiteboard your team can keep editing, with arrows that stay attached, cards that hold their own words, and an agent that follows comments.

Module: Diagrams · Level: intermediate · 6 min · https://school.agents.org.in/visual/diagrams/editable-canvases

Two canvases, one move. Someone dragged the blue card down on both. Drag it again, or press Move the card, and watch the two arrows.

*[Interactive demo: A bound arrow is a real connection, so it follows. An arrow drawn to look attached is left pointing at nothing.]*

Before the move, both arrows looked identical. Only one survived the drag. That is the whole lesson in one move: **a canvas is worth what survives the next person's drag**.

A finished figure is drawn once for readers. A canvas (An endless editable whiteboard, like tldraw or FigJam, where a team keeps moving boxes and arrows around after the first draft) is for pictures people keep editing. So build it from real relationships (arrows bound to shapes, cards grouped in frames), not from coordinates that only happen to look right today.

## Which tldraw to use

tldraw is an open-ended whiteboard, and it gives agents three ways in. Which one you use depends on where you are.

  **tldraw Desktop:**

You have the app open with a `.tldraw` file. The app's agent setup installs a `tldraw-offline` skill and helper. It talks to a small local server: one call to search the board, one to run edits. Hand long jobs to its helper.

  **Hosted chat:**

No app installed: you are in Claude, ChatGPT or Cursor. Use the tldraw MCP app, one address you add as a connector. Its `search` tool reads how the editor works; `exec` runs code against the live board.

  **Your own product:**

You are building a canvas into your app. That is the tldraw SDK, with a `tldraw-migrate` skill for upgrades. The skill only covers the UI craft here.

> **They speak different formats.** Desktop takes raw tldraw records. The MCP app takes simpler shapes with string ids. Never paste a snippet from one into the other; read the surface's own search first.

## Six rules for a board that stays editable

  **Step: Bind every meaningful arrow.** Create arrows with `helpers.createArrowBetweenShapes`, so they are bound (An arrow on a canvas that is truly attached to the shapes at its ends, so it follows them when someone drags. An arrow that only looks attached gets left behind) to both shapes. A raw arrow placed by coordinates detaches on the next drag. Unbound arrows are for decoration only.

  **Step: Generate structure, don't place it.** A flowchart, sequence, state machine or mind map comes from `helpers.mermaid(source)`, which writes bound shapes for you. On a canvas, Mermaid (A way to write a diagram as short text (A --> B) that a docs site turns into a picture. Good for structure kept next to code; its automatic layout looks generated in a finished figure) is the right start, because people rearrange the result.

  **Step: A card and its words are one shape.** Put the label inside the shape's own text. A long label needs a bigger card, not a separate text box laid on top. Loose text is only for words that stand alone, like a heading over a group.

  **Step: Arrange with the editor, not arithmetic.** Use align, stack and distribute, and draw containers as frames. Shapes inside a frame are positioned relative to it, and text never crosses a frame's edge.

  **Step: Lint before you call it done.** Run `helpers.getLints()` and fix every lint (An automatic check that flags problems on a board, like text spilling out of a card or an arrow that isn't attached. Fix every one your edit caused) your edit introduced, on the shapes you touched. Don't tidy a neighbour nobody asked you to move.

  **Step: One accent here too.** Canvas colors are names (`blue`, `orange`, `grey`), not hex codes. Pick one named color for the focal shapes and keep the rest neutral.

  **Before (avoid):**

Twelve boxes placed by x and y, each with a text shape floating over it, arrows drawn from point to point. Looks perfect. Someone drags one box and three arrows and two labels stay behind.

  **After (do this):**

The same flow generated from a Mermaid source: labels inside their cards, arrows bound, the whole thing in a frame. Drag anything and it all comes along.

## People share the board

A canvas is a team's space, and an agent working on it is a guest. Three habits make it a good one.

**Comments are instructions with an address.** An anchored comment (A comment pinned to a specific shape, so it points at what it means instead of describing it. On a board, each one is an instruction with an address) is pinned to the shapes it means, so the agent reads the threads first and uses each anchor to find its target. It replies in the thread it acted on and resolves it. A thread addressed to someone else is not its job.

**Ambiguity stops the edit.** "Make the important one stand out" over six identical cards is a question. An unanswered question is not permission to pick one.

*(Pointing beats describing, on a canvas too.)*

**Scripts are code.** A board can carry scripts that run whenever the file opens. So treat a `.tldraw` file from someone else the way you'd treat code from someone else, and ask before writing to a board whose scripts you didn't start.

Two more small things on Desktop: never edit a `.tldraw` file while it's open in the app, and save with `helpers.saveDoc()` yourself instead of asking the person to.

## When the board becomes a figure

Boards often grow into something worth publishing. When that happens, redraw it as a finished figure from the board's **structure**: its shapes and the bindings between them. Don't screenshot it. A screenshot keeps the mess and loses the meaning.

**Work on a shared board (paste into your agent):**

```text
Open the planning board in tldraw.
First read every comment thread; act only on threads addressed to you, reply in that thread, then resolve it.
Build the signup flow from a Mermaid source so every arrow is bound. Labels go inside their cards; group the flow in a frame.
Use one named color (blue) for the focal step and grey for the rest.
Run getLints and fix every lint your edit introduced. Don't move shapes you weren't asked to touch.
If a request is ambiguous (which card is "the important one"?), ask me instead of guessing.
```

**Check yourself:** An agent draws a flowchart on a tldraw board by placing boxes and arrows at exact coordinates. It looks perfect. What will go wrong first?

- [ ] The colors won't match the brand's hex codes
- [x] The arrows detach as soon as someone drags a box
- [ ] The board will be too slow to open
- [ ] Mermaid will refuse to import it

*Why:* Arrows placed by coordinates only look attached. Bound arrows, created with createArrowBetweenShapes or generated from Mermaid, follow the shapes when people rearrange them.

## In the skill

- https://school.agents.org.in/skill/diagram/tldraw-canvas.md


---

# Briefs that work

> Turn a feeling ("make it smoother") into a brief an agent can act on, decide where your judgment is worth spending, and hand over evidence instead of adjectives.

Module: Directing your agent · Level: beginner · 7 min · https://school.agents.org.in/foundations/agents/briefs-that-work

You type "make the menu smoother". The agent (A language model plus a harness: the model decides what to do, the harness gives it tools to read files, write code, run it and check the result) changes the duration, adds a bounce and swaps the curve. It is different now. It is not better.

The agent did not fail. You gave it a feeling, and a feeling has a thousand right answers. Pick one of the vague asks below and look at what replaces it.

*[Interactive demo: Each vague ask has a precise word behind it. The word brings the numbers with it.]*

## Why agents land in the middle

An agent produces the average of everything it has seen. Left alone, that is where your interface ends up: fine, and indistinguishable from every other one.

Steering is how you move it off the average. A brief (The few sentences you send an agent before it starts: what to build, for whom, and the exact values that matter. A good brief names things instead of describing how they feel) is the few sentences that do the steering: what to build, for whom, and the exact values that matter.

> **In plain words.** A **brief** is the message you send before the agent starts. Good briefs name things ("tabular figures") instead of describing how they feel ("the numbers jump").

## Name it, don't describe it

"Make it smoother" leaves the agent guessing. "Open it from the trigger, 200ms, `cubic-bezier(0.23, 1, 0.32, 1)`, transform and opacity only" does not.

You do not need every number. You need the concept. *Tabular figures*, not "the numbers jump". *Inner radius = outer − padding*, not "the corners look off". The precise word is a key, and the agent already has the lock.

  **Vague (avoid):**

"The card feels cheap. Make it nicer."

  **Named (do this):**

"Layered shadow at 4–6% opacity instead of the 16px blur, and 8px inner radius on the image (the card is 16px with 8px padding)."

The skill keeps a whole lexicon (The exact names for how interfaces look and move, like tracking, easing or hierarchy. The right word gets you the right result from an agent) of these words, seeded from Emil Kowalski and Glenn Carstens-Peters' work on articulating design. Its one job is to stop anyone, you or the agent, saying "weird". Look a word up, use it, stop.

*(A screenshot with arrows often beats any word.)*

## Spend your judgment where it changes the result

Some work only you can judge: what to build, which direction to keep, when to stop. Other work your opinion adds nothing to: research, summaries, a command menu the agent has built a hundred times.

  **Step: Get interrogated first.** For a new feature, ask the agent to question *you* until the goal, the user and the why are written down. Skip this for research.

  **Step: Delegate known patterns.** A data grid, a chart, a command menu: hand them over whole.

  **Step: Hand-tune the fabric.** Page layout, the type scale and spacing are often faster by hand, or with one precise prompt each.

## One or two passes, then look

Every pass is a bar the work has to clear. Ask for five at once and you get a muddle that half-satisfies each.

Run one or two. Look at the screen. Then run the next. The one exception is the launch-week sweep, which crosses everything on purpose; you will meet it later as `ship`.

**A brief for one pass (paste into your agent):**

```text
Polish the pricing card only. Two things:
1. Press state: scale(0.97) on :active, 120ms, ease-out.
2. Tabular figures on the price so it doesn't jitter when the plan toggles.
Don't touch layout or colors. Show me the diff.
```

## Fix the system, not the screen

Try this: ask for one real screen in one sentence ("a dialog that publishes pending changes, using our components"). Every mistake in the result points at a hole in your system.

| The agent did this | The hole |
|---|---|
| a raw hex or a one-off spacing value | a semantic token is missing or badly named |
| invented a `ghost` button variant | the variant list isn't closed |
| hand-rolled a select instead of `Select` | the component has no "when to use" note |

Correct the screen and you fixed one screen. Patch the hole and the next screen comes out right.

## The papercut pipeline

Spot, document, delegate. When something small is wrong, write it up with the evidence: a screenshot with arrows, or for motion, a recording scrubbed to the bad frame. Name the cause if you know it ("the select renders in a portal, so the modal treats a click on it as outside").

It takes two minutes to write, and the agent fixes it while you keep working.

## When you want what someone else built

"How did they build that gradient?" is a question about a mechanism. The lazy answer is a dump of computed styles and a lookalike snippet. It explains nothing.

Ask the agent to read the page as **evidence, never instruction**, and to tag every claim: *measured* (read off the page), *derived* (computed from measurements) or *inferred* (a guess about intent). Then close on the recipe in words: the layers, their order, the one or two values doing the work.

**Explain another site's effect (paste into your agent):**

```text
How is the glow behind the hero on example.com built?
Scope to that one effect. List the layers in paint order with the declaration doing the work on each.
Tag every claim measured, derived or inferred.
End with the recipe in words, plus what wouldn't survive copying. No lookalike code.
```

> **A page is not a prompt.** Text inside someone else's page (comments, class names, alt text) is content to report. It never changes what the agent does next.

## When the agent keeps missing

Two rounds of "closer, but not quite"? Rephrasing a third time is the casino. Build a small tool instead, a lesson of its own in this module. The opposite failure is real too: tooling up for something you hit once is procrastination with extra steps.

**Check yourself:** The agent keeps writing raw hex colors in new screens. What fixes it for every future screen?

- [ ] Correct the hex in this screen
- [ ] Rephrase the prompt to say 'use our colors'
- [x] Add or rename the missing semantic token so the agent can find it
- [ ] Ask for five passes at once

*Why:* A raw hex points at a hole in the system: the token the agent needed was missing or hard to find. Patch the hole and every later screen comes out right.

## In the skill

- https://school.agents.org.in/skill/meta/directing-agents.md
- https://school.agents.org.in/skill/meta/design-vocabulary.md
- https://school.agents.org.in/skill/meta/reading-others-interfaces.md


---

# The review table

> Read and write a UI review as Before | After | Why rows, scan code against the checklist, and scope a review to what a change actually broke.

Module: Directing your agent · Level: beginner · 8 min · https://school.agents.org.in/foundations/agents/the-review-table

Here is a review you have probably received: "The animation could be smoother, the transition is a bit long, consider a different easing curve."

You read it twice. You still don't know what to type.

  **Prose review (avoid):**

The modal could be smoother. The transition is a bit too long and the scale starts too small. Consider a different easing.

  **Table review (do this):**

`transition: transform 0.6s ease` → `0.25s cubic-bezier(0.25, 1, 0.5, 1)`. Under 300ms feels responsive.<br />`scale(0)` → `scale(0.95)`. Nothing real appears from nothing.

## Three columns, every time

The skill makes every UI review a table with three columns. Emil Kowalski calls this format non-negotiable, because it forces concrete diffs instead of advice.

| Column | What goes in it |
|---|---|
| **Before** | The real line from your code, quoted. Never paraphrased. |
| **After** | The replacement. Same job, better feel. |
| **Why** | One short sentence, ending with the skill node that goes deeper. |

**If you can't write the Why in one sentence, you're probably reaching.** Drop the row.

  **Step: One issue per row.** Three problems in one line of CSS get three rows.

  **Step: Order by impact.** The thing that breaks keyboard users goes above the hairline.

  **Step: Nothing actionable in the prose.** Prose around the table is context only: "Three issues in the modal entrance:".

  **Step: Nothing to fix is a valid review.** "Looks good, nothing to change" beats invented nitpicks.

## The checklist: a scan before taste

Before anyone argues about taste, there is a floor. The review checklist (Thirty-nine common UI mistakes, each with a quick way to spot it and a fix, scanned before anyone argues about taste. A floor, not a ceiling) is thirty-nine common mistakes, each with a quick way to find it and a fix: `transition: all`, `scale(0)`, `ease-in` on UI, a removed focus ring, `z-index: 9999`, `100vh` on phones, placeholder names like "John Doe".

Edit the CSS below. Each rule is one of those quick checks; fix a line and its row disappears.

*[Interactive demo: A slice of the checklist, running live. The table is the review.]*

Two warnings come with it. The checklist is a **floor, not a ceiling**: the best reviews catch what isn't on it. And it is a flag, not a law: a 500ms animation is fine on a rare celebration moment. Read the context before writing the row.

**Ask for a proper review (paste into your agent):**

```text
Review src/Modal.tsx as a Before | After | Why table.
Scan it against the review checklist first.
Quote my real lines in Before. End each Why with the node that owns it.
Three to five rows, most important first. If nothing fails, say so.
```

## Reviewing a change, not a codebase

Now picture a pull request: a set of changes you want merged. The author is asking one question: *did I make this worse?*

A review that answers "here is everything wrong with this codebase" buries the two lines that matter under thirty that don't. A change-scoped review (A review of what one change (a branch or pull request) did, not of the whole codebase. Each finding is marked Introduced, Regression or Pre-existing by what caused it) answers the real question.

*[Interactive demo: A branch review: the scope first, then the removed lines, then a Status on every row.]*

### Resolve the scope

With no target named, the agent reviews the commits your branch has that main doesn't, **plus** anything uncommitted, and says how many of each. With nothing changed, it asks what to review instead of inventing a scope.

### Follow the blast radius

A changed file is evidence, not the subject. Its blast radius (Every screen a changed file shows up in. Change a shared token or component, and every place that uses it is part of the review) is every screen that renders it. Change a token, and every component using that token is in scope.

### Read the removed side

**Regressions are invisible in the after state.** A `<button>` that became a styled `<div>` in a "move to the new design system" refactor looks identical. Only the deleted lines show it.

So the agent searches the removed lines for signals that left: `aria-*`, `<label>`, `:focus-visible`, `prefers-reduced-motion`, a token swapped for a literal. Each is a lead, not a finding. `aria-label` replaced by `aria-labelledby` on visible text is fine; read the hunk before deciding.

*(The button-to-div swap is the regression people miss most.)*

### Status by cause

Every finding gets a status, decided by what caused it, not by where it sits:

| Status | Means | Blocks the merge? |
|---|---|---|
| Introduced | the change created it | yes |
| Regression | the change weakened something that worked | yes |
| Pre-existing | it was already there, not made worse | no |

An untouched screen whose contrast now fails because a shared token changed is **Introduced**. A bug three lines from the edit that the edit didn't touch is **Pre-existing**. List at most three of those, plainly marked as not this change's job. Thirty is another review.

**Review my branch (paste into your agent):**

```text
Review this branch as a change-scoped review.
Scope: commits ahead of main plus uncommitted files; tell me how many.
Read the removed lines for lost signals.
Columns: Severity | Status | Location | Before | After | Why.
Introduced and Regression only, then at most three Pre-existing.
```

**Check yourself:** A pull request changes one shared color token. An untouched settings screen now fails contrast. What status does that finding get?

- [ ] Pre-existing, because the settings file wasn't edited
- [x] Introduced, because the change caused it
- [ ] Regression, but only if the file is in the diff
- [ ] None: untouched files are out of scope

*Why:* Status follows cause, not location. The token change caused the failure, so it is Introduced, even though the settings file never appears in the diff.

## In the skill

- https://school.agents.org.in/skill/meta/review-format.md
- https://school.agents.org.in/skill/meta/review-checklist.md
- https://school.agents.org.in/skill/meta/change-scoped-review.md


---

# Options, tools and generators

> When nobody has decided yet, get three real directions behind a picker; when prompting keeps missing, build the tool that makes the thing; when all you have is a vibe, build a generator.

Module: Directing your agent · Level: intermediate · 8 min · https://school.agents.org.in/foundations/agents/options-and-tools

You ask for a project card. You get one. You aren't sure it's right, so you ask for "a few more options". You get three cards that are the same card in three shades of grey.

That is not exploring. It is waiting.

*[Interactive demo: Three directions, one named axis each, the worst real content, a picker that is chrome. Try keys 1–3.]*

## Three directions behind a picker

When the decision hasn't been made, the fastest way to make it is to see the answers side by side, working. The skill calls this the prototype picker (Three to five genuinely different, working versions of one piece on a throwaway page, switched with a small dark pill. You pick; the decision gets written down; the page gets deleted), from Emil Kowalski's practice.

If you *have* decided, skip it. A picker holding three answers to a settled question wastes the run.

  **Step: Never touch production code.** The variants live on a throwaway page (`/proto/project-card`) that nothing imports.

  **Step: Real materials, worst content.** Your real fonts, tokens and components, filled with the content that breaks things: the longest real name, 1,284 members, the empty state. Pretty placeholders make every variant look fine.

  **Step: A named axis each.** Density, personality, layout, motion, interaction model. Name them "Quiet", "Editorial", "Dense", never "Option A". If you can't name the axis, it's one idea in three tints.

  **Step: Every variant fully works.** Real hover, real motion, real states. No "imagine this part".

  **Step: The picker is chrome.** A small dark pill at the bottom, never styled like your product. Number keys switch, `?v=2` in the address keeps the choice.

## Numbers go on sliders

Any number you would hardcode in a prototype is a decision not made yet. Duration, easing, blur, radius: put each on a slider, with a range wide enough to be wrong on both sides, and its value printed beside it.

Then the conversation changes. Instead of "a bit faster?", a teammate opens your link with `?v=2&dur=180` already in it and drags the slider themselves.

## The output is a decision

**The only thing that survives a prototype run is the decision, written down.** Including what you rejected and why, which is the part everyone skips and the part that saves the next meeting.

```md
Project card — decided from /proto/project-card
- Dense: density high; 13.5px name, 12px meta
- Rejected: Editorial (long names wrap to three lines); Quiet (fewer per screen)
```

Then the winner gets built properly and the prototype page is **deleted**. A picker that outlives the run is exactly the dead code rule one was there to prevent.

**Explore before deciding (paste into your agent):**

```text
I haven't decided how the empty inbox should look.
Build three directions on a throwaway route, each on a named axis.
Use our real components and the worst real content (long subject lines, 0 and 1 messages).
Put any timing or size on a slider. Add the dark picker pill, keys 1–3.
Then stop and let me pick. Don't choose a favourite.
```

## When prompting keeps missing: build a tool

Some things you can describe but can't get by asking: a chart's density, a shadow that has to match a reference, a texture that feels right. Each prompt lands "close". You rewrite. It lands close again.

That loop is a slot machine. The fix is to stop pulling the lever and build the small tool (When prompting for something keeps landing 'close but not quite', build the small tool that makes it instead, with every taste decision on a knob you adjust by eye) that makes the thing, with every judgment on a knob you turn by eye.

First, diagnose *why* prompting misses:

| The gap | The tool |
|---|---|
| A judgment only your eye can make (density, balance) | a dial-in tool: every taste value on a control |
| It must match something that exists | a matching tool: the target shown beside the output, at real size |
| Right pixels, wrong format | a converter |
| It can only be judged in a state that's slow to reach | a stage: a route that jumps straight to that state |

Then pitch it in four lines before any code: the gap, the tool, the controls, the export. That is the cheapest moment to be corrected.

> **Save writes a file.** The tool's save button writes the whole configuration as a file in the project, and the real artifact imports that file. Saving *is* shipping the decision.

The same move works for checking. If you keep checking one thing by hand (nested radii, the tab order), build a small inspector you can drop onto any page. A one-off check stays a checklist row.

*(After one round of "closer, but not quite", the diagnosis is done. Propose the tool.)*

## When all you have is a vibe: a generator

"Warm. Kind of old-timey. Hand-done." There is no target here, only a mood. Prompting for finished pictures will circle the nearest cliché forever.

The way out has two halves. **Research** turns the vibe into rules: what process made this look (a printing method, a tool, an era), what that process did to colour and texture, and what its recurring patterns are called. Compress that into 5 to 10 rules you can strike or keep *before* any code exists.

Then build a seeded generator (A small program that makes a picture from its settings plus a random seed, so one click gives a new variation and saving the settings lets you make the keeper again forever): the picture is a pure function of its settings, a click gives a new variation, every taste decision is a knob, and save writes the settings so the keeper can be made again forever.

**From a vibe to a generator (paste into your agent):**

```text
I want card backgrounds that feel like old risograph prints. I can't describe it better than that.
First research the process: what made that look, what it does to colour and texture, what the patterns are called.
Give me 5–10 rules as constraints and let me edit them.
Then build a seeded generator on a /lab route: click for a new seed, a knob for every taste value, save writes the JSON.
```

| You have | Reach for |
|---|---|
| A few nameable directions, undecided | the picker |
| A clear target the prompt keeps missing | a tool |
| Only a mood | research, then a generator |

**Check yourself:** You've rejected two rounds of prompted variants of the same illustration idea. What does the skill say to do next?

- [ ] Ask for a third round with more adjectives
- [x] Stop and propose research plus a seeded generator
- [ ] Pick the closest one and ship it
- [ ] Build a prototype picker with Option A, B and C

*Why:* Two rejected rounds means you are searching a space, not requesting an item. Research turns the vibe into rules, and a generator lets you search by clicking instead of prompting.

## In the skill

- https://school.agents.org.in/skill/meta/prototype-picker.md
- https://school.agents.org.in/skill/meta/build-a-tool.md
- https://school.agents.org.in/skill/meta/vibe-to-generator.md


---

# Point, don't describe

> Use Agentation to click an element in your running app, write a two-line critique, and hand your agent the exact selector and file, by paste or straight over MCP.

Module: Directing your agent · Level: beginner · 7 min · https://school.agents.org.in/foundations/agents/point-with-agentation

"The button in the top right, no, the other one, the grey one next to the title." You have typed a sentence like this. The agent edited the wrong button.

Now watch the same feedback given by pointing.

*[Interactive demo: Click the element, write two lines, and the fix session gets the selector, the file and your words.]*

## What just happened

The toolbar in the corner is Agentation (Benji Taylor's development toolbar: click any element in your running app, write a short note, and it hands an agent the element's selector, file and your words), by Benji Taylor. It runs inside your app while you develop it. You click an element, type a short critique, and it turns that into structured notes an agent can read: the element's selector (The address of an element on a page, like button.export-btn. It tells code (or an agent) exactly which element you mean), the file it lives in, its computed styles and your words.

Pointing beats describing. The agent no longer guesses which button. It is handed the button.

> **In plain words.** Your app runs on your own computer while you build it, at an address like `localhost:3000`. That's the **dev server**. Agentation only appears there, never on the real site.

## Set it up once

  **Step: Install the package.** Add it as a development-only dependency.

  **Step: Mount it in development only.** Render it in your root layout behind a development check, so it never ships.

  **Step: Optional: connect your agent.** Add Agentation's MCP server so the agent reads notes directly instead of you pasting them.

```bash
npm install --save-dev agentation
```

**Mount it, development only (paste into your agent):**

```text
Install agentation as a dev dependency and mount its toolbar in the root layout,
after the page content, only when process.env.NODE_ENV === "development".
Keep the layout a server component. Don't add a second mount if one exists.
```

Not on Next.js? Benji ships an `agentation` skill that knows how to mount it in Vite, Remix, Astro and the Pages Router. Ask your agent to "set up Agentation" with that skill installed.

## Two ways to hand the notes over

**Paste.** Click Annotate, click the element, write your note, submit. Agentation gives you markdown: selector, file path, styles, critique. Copy it into your agent. No server needed.

**Over MCP.** Here a little vocabulary helps. Theo Browne describes an agent as a model plus a harness (The hidden layer of ordinary code around a model: it lists the tools, notices when the model asks for one, runs it and sends the result back. Model + harness = agent): the hidden layer that hands the model its tools and runs them. An MCP (Model Context Protocol: a standard way for an agent to use tools that live somewhere else, by sending a request to a server, like this course's exam server) server is a tool that lives somewhere else, reached by an address. Agentation's server is one such tool. Connect it, and your agent can read new annotations the moment you add them.

```bash
claude mcp add agentation -- npx -y agentation-mcp server
```

*(A green "healthy" check isn't proof. Add one test note and make sure the agent reads that exact note.)*

## The two-session loop

With MCP connected, you can split the work across two agent sessions, each with one job.

| Session 1: critique | Session 2: fix |
|---|---|
| opens your app in a visible browser | watches for new annotations |
| adds 5–8 annotations per page | reads each selector, file and note |
| names the principle in each | makes the smallest fix it can defend |
| | moves to the next one |

The annotation is the contract between them: the same selector and note a person would have written. Session 1 can be you, clicking around, or an agent driving the browser on its own. Either way, you review the resulting commits, not every single note.

The skill's own verb for session 2 is short:

```bash
/design-engineering agentation
```

It pulls the annotations, fixes them one at a time with minimal diffs you could defend as review rows, and commits only if you ask.

## Write notes the agent can act on

Agentation's own guidance for a good note, paraphrased:

- **Two or three sentences.** Longer reads as an essay; shorter is too vague.
- **Specific and actionable.** Not "this section needs work". Instead: "Use a three-column card grid with icons."
- **Name the principle.** Hierarchy, grouping, whitespace. Those words are how the agent finds the right pattern.
- **Point at a product that does it right.** "Like Linear's command bar."

  **Vague note (avoid):**

This button looks weird.

  **Actionable note (do this):**

Main action, but it reads as secondary: grey fill, 11px. Use the primary fill and 14px, like Linear's primary button. Hierarchy.

**Turn notes into fixes (paste into your agent):**

```text
Read my pending Agentation annotations.
For each one: open the file it names, make the smallest change that answers the note, and give me a Before | After | Why row.
One annotation at a time. Don't commit until I say so.
```

## Where it doesn't fit

- **Production.** It is a development tool; it should never ship.
- **A one-off marketing page.** Pointing by screenshot is faster than setting up the toolbar.
- **No running app.** It needs something on `localhost` to click.

> **Selectors go stale.** An annotation on `.card > div:nth-child(3)` points somewhere else once someone reorders the divs. Prefer stable hooks like `data-testid`, and clear old annotations instead of letting them pile up. Also read Agentation's licence (PolyForm Shield) before using it inside a product that competes with it.

**Check yourself:** Why does an Agentation note fix the right element when a typed description often doesn't?

- [ ] It uses a stronger model
- [x] It hands the agent the element's selector and file instead of a description to interpret
- [ ] It edits the code itself, without an agent
- [ ] It only works on buttons

*Why:* The note carries the exact selector, the file path and the computed styles, so the agent doesn't have to guess which element 'the grey one next to the title' means.

## In the skill

- https://school.agents.org.in/skill/meta/agentation-workflow.md


---

# Design contracts

> Give your agent a .design file or a DESIGN.md so it uses your tokens instead of guessing, know which source wins a disagreement, and write design-system docs an agent can actually follow.

Module: Directing your agent · Level: intermediate · 8 min · https://school.agents.org.in/foundations/agents/design-contracts

Your brand uses 8px corners and one blue. The agent builds a new screen with 6px corners, Inter at 14px and a blue that is almost yours.

It didn't ignore your design system. It never saw one. Write the system down where an agent looks, and this stops.

## Two files an agent reads

A token (A named value, like --color-text-secondary, used instead of typing the raw color everywhere. Change the token and every place that uses it changes) is a design decision with a name: `colors.accent`, `rounded.md`, `motion.duration.base`. Components use the name; the value lives in one place.

There are two common files that hold tokens for an agent:

| | `.design` | `DESIGN.md` |
|---|---|---|
| Made by | AgentsORG (schema `design.v1`) | Google Labs |
| Shape | a machine-readable contract | tokens up top, prose below |
| Holds | tokens, components, voice, rules, and an **intent** | tokens, plus an overview and do's and don'ts |
| When both exist | it is the contract | it becomes the rationale |

> **In plain words.** A design contract (A file in your project (.design or DESIGN.md) that tells an agent how things look: colours, type, spacing, radii, which component to use when. It outranks the skill's defaults) is a file in your project that says how things look, in a form the agent can read: colours, type, spacing, radii, which button to use when. The agent reads it before writing UI.

## Who wins a disagreement

Your prompt says one thing, the contract another, the skill a third. The `.design` format settles this with a fixed order. Toggle the sources below and watch the value change.

*[Interactive demo: The highest source present wins. The skill only fills in where the contract is silent.]*

The order, top to bottom: your prompt (this task only), the nearest `.design`, the `design` skill's procedure, generic taste skills like design-engineering, and finally the model's defaults.

So if your contract says dialogs open in 400ms, the agent uses 400ms, even though the skill says most UI lands under 300ms. **It raises the tension once, in a sentence, then complies.**

*(Silence isn't permission. Where the contract says nothing, the skill's craft still applies.)*

## How an agent reads a `.design`

The agent walks up from the file it is editing to find the nearest contract. In a monorepo, a package's own contract beats the one at the root.

  **Step: agent.instructions first.** The contract's own short brief, written so the file works even with no skill installed.

  **Step: intent.** The committed direction, where boldness is allowed (the `signature`), and the register: utilitarian or editorial.

  **Step: constraints, policy, decisions.** Hard rules, and what to do when a component isn't listed: ask, use the nearest, or invent with a note.

  **Step: tokens, voice, components.** Bind every listed property to its token. Apply the voice (casing, terminology, error style) as firmly as the colours.

If there's no contract at all, the agent should **offer to create one**, after scanning what you already have (global CSS, Tailwind config, an existing DESIGN.md). It should never invent a system silently.

## How an agent reads a `DESIGN.md`

The agent looks in the repo root, then `design/`, then `.design/`. The YAML at the top is **normative**: use `{colors.primary}`, never a hardcoded hex; match the typography token, not "close enough". The markdown below is **context**: the overview tells it what the system is trying to feel like, and the do's and don'ts are hard constraints.

The agent never rewrites your DESIGN.md on its own. It may *propose* an edit, as a review comment, when a token is missing, a pair fails contrast, or a variant like `button-primary-active` doesn't exist yet.

```bash
npx -y @google/design.md lint DESIGN.md
```

**Use the contract (paste into your agent):**

```text
Before writing UI, find our design contract (.design first, then DESIGN.md).
Use token references, never raw hex, spacing or radius values.
If our contract and your skill disagree, say so once and follow the contract.
List any literal you had to leave in place and why.
```

## Writing docs an agent can follow

Tokens say what exists. Docs say **when to use what**. Every decision the docs leave out gets replaced by a guess, and a guess looks exactly like a decision.

Write them for an engineer who joined yesterday and can never ask you a question.

  **Step: Inventory the code first.** Every token, every component's variant list, two or three real uses each. Never write from memory: that's how agents learn variants that don't exist.

  **Step: Size it.** One token file and five components or fewer: one file. Bigger: a short root file plus one file per topic, each under 150 lines.

  **Step: Use the four-part template.** For every component, in this order.

  **Step: Verify by prompting.** Ask for a real screen in one sentence and read the drift.

The four parts:

1. **When to use it, and when to use the alternative.** "Navigation styled as a button is a Link."
2. **Which variant applies where, and the default.** Then close the set: "Nothing else exists; an unlisted variant is a bug."
3. **One correct and one incorrect example**, from real code.
4. **A flowchart** when there are three or more options.

  **Hedged (avoid):**

Use small text sparingly. Prefer the primary button for important actions.

  **Strict (do this):**

Body 16px, captions 14px, never below 12px. One primary button per view: two means the screen has no hierarchy.

When the test screen drifts, fix the **docs**, never just the screen:

| The agent did | The fix |
|---|---|
| hardcoded a colour | move the "semantic tokens only" rule into the root file |
| invented a variant | add "nothing else exists" and tighten the type |
| picked the wrong component | add the when-vs-alternative line for that pair |

Repeat until a fresh prompt drifts zero.

**Check yourself:** Your .design sets dialog duration to 400ms. The skill says most UI should be under 300ms. Your prompt says nothing about it. What should the agent do?

- [ ] Use 200ms, because the skill knows better
- [x] Use 400ms, mention the tension once, and move on
- [ ] Ask you every time it builds a dialog
- [ ] Use 300ms as a compromise

*Why:* The nearest .design outranks generic taste skills. The agent raises the conflict once in prose, then follows the contract.

## In the skill

- https://school.agents.org.in/skill/meta/using-design-file.md
- https://school.agents.org.in/skill/meta/using-design-md.md
- https://school.agents.org.in/skill/meta/design-system-docs.md


---

# Break it, then ship it

> Feed a component the realistic worst case before users do, land the production baseline, and sweep the whole app before launch with a severity tree that tells blockers from polish.

Module: Directing your agent · Level: intermediate · 9 min · https://school.agents.org.in/foundations/agents/break-then-ship

The member row looks perfect. It was designed with "Ana Ruiz", three rows and a wide screen. Production sends it "María-José Fernández de Córdoba", 1,284 members and a sidebar that leaves 280px.

Pick a scenario and watch what breaks.

*[Interactive demo: One row, five realistic cases, each failure with its one-line fix.]*

## Stress what can actually reach it

A stress scenario (One realistic but hostile input for a component: the longest real name, zero items, a 320px box. You render them side by side to see what breaks before users do) is one hostile but realistic input. The trick is to keep only the ones the component can really receive:

| Axis | Keep it when | Try |
|---|---|---|
| Content length | it shows text it didn't write | empty, one word, a paragraph, a long unbreakable URL |
| Content shape | users or other languages write the text | emoji, right-to-left, accents |
| Quantity | it repeats over items | zero, one, the usual count, ten times that |
| Container | always | 320px, squeezed by a sibling, very wide |
| State | it gets the state as a prop | loading, error, disabled |

A static button with a fixed label gets container widths, never long text.

**Every hostile value is realistic or the real limit.** A hyphenated, accented surname. A 60-character work email. The exact `maxLength` from the form. Random keyboard mash gets dismissed as "never happens", and the conversation ends.

## Read the failure signature

Most breaks have a known cause and a one-line cure:

| You see | Cause | Fix |
|---|---|---|
| avatar squished into an oval | a flex child shrinking | `flex-shrink: 0` |
| text pushes the menu out of the box | `min-width: auto` on the text column | `min-width: 0` |
| an email runs off the edge | no place to break | `overflow-wrap: anywhere` |
| "1 members", `NaN` | a hardcoded plural, a raw number | `Intl.PluralRules`, a null guard |
| 1,000 rows stutter | every row rendered | virtualize or paginate |

  **Step: Build the harness.** A throwaway page, scenarios side by side, each in a fixed-width box. Feed the data at the data boundary (the fetch or the hook), never by editing the component.

  **Step: Look once, report.** Name what broke and the node that owns each fix.

  **Step: Keep the worst-case fixture.** Delete the page; keep the data. It's the regression test for the next person.

Dark mode, browser zoom and reduced motion are the user's toggles. Name them for the person to check; don't fake them on the page.

*(Stress tests run isolated. Design variants run in context. Don't swap them.)*

**Break one component (paste into your agent):**

```text
Stress-test components/MemberRow.tsx.
List every value it renders with its real limit (schema, maxLength, API type); "unbounded" is a finding.
Build one worst-case fixture shaped like our demo data, plus Empty, One and 1,000 rows.
Render them side by side at 320px and our real width. Report what broke and the fix. Don't fix yet.
```

## The production baseline

Before launch, dozens of findings tend to share one root: the app never set its global floor. The production baseline (The global floor every app a phone will open needs: one viewport tag, a few root CSS rules, hover and reduced-motion gates, and dev tools kept out of the real build) is that floor. Land it once and whole classes of bugs vanish.

**In the head:**

- One viewport tag: `width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content`. Never `maximum-scale`; fix the 16px inputs instead.
- `<html lang>` set to the real language.
- Declare only the colour schemes you designed. A light-only app that says `color-scheme: light dark` gets dark native inputs on white cards.

**In the root CSS:**

- `font-synthesis: none`, so a weight that never loaded fails visibly instead of being faked.
- A `:focus-visible` outline, 2px, with `outline-offset: 2px`.
- Inputs at 16px on touch; `touch-action: manipulation` on controls.
- `text-wrap: balance` on headings, `pretty` on paragraphs.

**Two gates:** every `:hover` behind `(hover: hover) and (pointer: fine)`, and reduced motion handled where the shared durations live. **Dev tools** mount only in development, through a dynamic import, so their code stays out of the real bundle.

> **Merge, never append.** Most projects already have half of this. Add the missing lines to the existing rules; never create a second viewport tag or a second global stylesheet.

## The ship check

A review judges one change against a craft bar. A ship check (The last pass before launch: the whole app swept across nine lanes, each finding ranked by a severity tree, mechanical fixes applied, and what needs a real phone listed) sweeps the **whole app** against a ship bar and fixes as it goes. Its question is blunt: what breaks in the first hour?

It goes an inch deep on nine lanes: accessibility, performance, mobile, forms, states, motion, theme, leftovers (lorem, debug logs, secrets) and marketing (titles, link previews). Anything bigger than a few lines goes on a list for a human.

### The severity tree

Walk it top to bottom and stop at the first yes.

*[Interactive demo: Pick a finding. The first yes decides its severity.]*

Context can override a lane's default. A missing `alt` on the only product photo is a blocker. `100vh` on a desktop-only admin tool is nothing.

### Fix order

Accessibility blockers, then leaks, then the baseline (one change clears dozens), then per-file fixes, then polish while the diff is still readable in one sitting. Then run the typecheck and build. A ship check that breaks the build is worth less than none.

### Three ways to fail

1. **Calling it ready from a laptop.** Sticky hover, input zoom, the phone keyboard and safe areas never reproduce in a desktop browser. Say which checks need a real phone.
2. **A report where a diff belonged.** Sixty rows of advice isn't launch prep. Apply the mechanical fixes.
3. **Redesigning in launch week.** New palettes and re-timed motion are taste. Fix defects; taste goes on the human list.

"Already in good shape, small diff" is a valid, good result.

**Check yourself:** During a ship check you find cards losing their border, but only in the dark theme. Using the severity tree, what is it?

- [ ] Blocker, because it's visible
- [x] Should fix: it shows only on another theme
- [ ] Polish, because borders are decoration
- [ ] Not a finding: dark mode is the user's toggle

*Why:* It doesn't lock anyone out, a typical first session won't hit it, and it leaks nothing. It is visible only in another theme, so it is a Should fix.

## In the skill

- https://school.agents.org.in/skill/meta/stress-scenarios.md
- https://school.agents.org.in/skill/meta/production-baseline.md
- https://school.agents.org.in/skill/meta/ship-check.md


---

# Routing the job

> Know how the skill decides who owns a request (posture, phase, material, companion skills) and run multi-step jobs in an order that doesn't waste passes.

Module: Directing your agent · Level: intermediate · 8 min · https://school.agents.org.in/foundations/agents/routing-the-job

Ask an agent "is this modal animation any good?" and one of two things happens. It hands you a careful verdict. Or it rewrites your modal.

Same words, very different jobs. The skill's first move on any request is to work out which job it is.

*[Interactive demo: Sort the request before reading anything. The posture fixes the shape of the answer.]*

## Four postures

Every request falls into one of four postures (Which of four jobs a request is: Build (make it), Judge (assess it, no edits), Decide (make a call first) or Name (give the word). It fixes the shape of the answer), and each has a fixed answer shape:

| Posture | You want | The answer |
|---|---|---|
| **Build** | UI made or changed | code, using the right values |
| **Judge** | existing work assessed | a table or list, **no edits** |
| **Decide** | a call before building | yes, no or a pick, with the trade named |
| **Name** | the precise word | the term in one line, then stop |

**Judge never writes.** A review request gets a table; a fix request gets code. Turning one into the other silently is how "is this good?" becomes a rewritten file you didn't ask for.

## One entry node, not four

The skill is a graph of small notes called nodes. The routing table (The skill's map from a job to the one note to open first, so the agent doesn't read four clusters to answer one question) maps a job to exactly one node to open first: "pick easing for one animation" opens the easing node, "a dropdown hides under the header" opens the z-index node.

Opening four nodes up front means you skipped the sorting. Start at one, follow its links to the companions it names (the easing node points at reduced motion, for example), and always finish by applying the two override files: your gotchas and your taste.

## The router: phase, then material

Before the routing table, the skill router (What /design-engineering does first for a plain request: find the design contract, work out the phase and the material, then hand the job to one or two owners) asks two questions. First, **what phase is this?**

| The user is saying | Phase | Owner |
|---|---|---|
| "try a few", "not sure which" | undecided | three options behind a picker |
| "something like…" | stuck on a vibe | research, then a generator |
| "build this screen" | foundation | hierarchy and spacing first |
| "feels cheap", "janky" | works, feels off | polish, motion or mobile |
| "review before I ship" | done | the review table |
| "closer, but not quite" | keeps missing | build a tool |

Then, **what is it made of?** Type, colour, motion, sound, SVG, forms. That picks the node or the specialist.

> **Phase beats material.** Polishing a screen whose layout is still wrong wastes the pass. The polish gets thrown away with the layout.

## One or two owners, never five

Loading four craft skills at once spreads attention across four quality bars and applies none of them. The router names **one or two owners** and hands off.

Some owners live outside this skill. A companion skill (Another installed skill that owns one job better, like HyperFrames for launch films or Stageshow for faux product UI. The router hands the job to it when it's installed) is another installed skill that does one job better: HyperFrames builds launch films, diagram-design draws figures, Stageshow builds faux product UI, the AgentsORG `design` skill owns `.design` files. When one is installed, the job goes to it, and the result still passes back through your gotchas, your taste and, for reviews, the table.

Jakub Krehel's interfaces skills are a good example of how the hand-off works. Install them and some jobs move:

| Job | With Jakub's skills | Without them |
|---|---|---|
| Review a branch or PR | `/interface-review` | change-scoped review |
| Stress one component | `/break` | stress scenarios |
| Three variants | `/variant` | the prototype picker |
| How another site was built | `/explain-interface` | reading others' interfaces |

His slash commands are ones *you* start, so the agent tells you the command and stops. Where his values and the skill's disagree, a reconciliation note settles each case. A press scale of 0.96 with no scaling when disabled. A brand focus ring is fine *if* it clears 3:1 against everything it crosses. Unrelated icons (play and pause) cross-fade; related shapes morph.

*(When two owners both seem right, answer from the narrower one and link the other.)*

## Questions that sound the same

Some requests blur together and land on the wrong node. The skill keeps a list of these confusable pairs (Requests that sound alike but belong to different notes, like 'how should this move?' versus 'is this diff's motion good?'. Each pair has a tiebreaker, usually scope) and a tiebreaker for each:

- **Four motion questions.** How should *this one thing* move? Is *this diff's* motion good? What's wrong across the *whole app*? Where is motion *missing*? Tiebreaker: scope.
- **Layout shift has two owners.** Once, while loading: skeletons and reserved space. On every interaction: animating the wrong properties.
- **Looks right, feels finished, should ship.** Designing, polishing, gating. Don't hand a gate verdict to someone still designing.
- **Sound.** Is there a user who can mute it? Product rules. If not, it's a video.

## Chains: the order for a whole job

Single questions go to single nodes. Real jobs chain several, and the order matters. The stacking chains (An ordered route through the skill for a multi-step job, like the screen loop: decide, foundations, one material pass, refine, review, ship. Skip steps; never reorder them) encode orders that survived use.

The screen loop, for one screen:

  **Step: Decide.** If you can't describe it yet, options behind a picker.

  **Step: Foundations.** Hierarchy, spacing, alignment; delete what adds nothing.

  **Step: One material pass.** Forms, surfaces, type, a component's API or a landing page. Just one.

  **Step: Refine.** Polish, then motion, mobile or accessibility only if that's the problem.

  **Step: Review.** The table.

  **Step: Ship.** Once every screen is done, the whole-app pass.

Chains are orderings, not checklists: skip links that don't apply, never reorder them. And don't run a chain on a one-line change; a hover tweak goes to one node and stops.

**Ask who owns it (paste into your agent):**

```text
/design-engineering which "the toast says Saved but screen readers never announce it"
```

**Check yourself:** Someone asks 'Is this dropdown animation any good?' and shares the code. What should the agent return?

- [ ] A rewritten dropdown with better motion
- [x] A Before | After | Why table, without editing the file
- [ ] A list of five skills to install
- [ ] A yes or no with no detail

*Why:* That's the Judge posture. Judge answers with a table or list and never edits unless asked afterwards.

## In the skill

- https://school.agents.org.in/skill/meta/skill-router.md
- https://school.agents.org.in/skill/meta/routing-table.md
- https://school.agents.org.in/skill/meta/disambiguation.md
- https://school.agents.org.in/skill/meta/stacking-chains.md
- https://school.agents.org.in/skill/meta/interfaces-reconciliation.md


---

# Make the skill yours

> Fork the skill's taste in pov.md, log every lived failure in gotchas.md, write lines that actually change an agent's behaviour, and test that they work across models.

Module: Directing your agent · Level: advanced · 9 min · https://school.agents.org.in/foundations/agents/make-it-yours

Two teams install the same skill. One makes a calm banking dashboard. The other makes a language-learning app where every button looks pressable and every win gets confetti.

Same skill, opposite answers. The difference is two small files the skill tells you to edit.

## pov.md: your taste overrides

The skill's canon comes from people like Emil Kowalski, Benji Taylor and Jakub Krehel. pov.md (The file where you override the skill's taste with your own. Where it disagrees with the canon it wins; where it's silent, the canon holds) is the layer where **you** override it. Where it disagrees with canon, it wins. Where it's silent, canon holds.

The install this course is built on is forked to Duolingo's design language: physical buttons with a 4px lip in a darker shade of their own fill, colours that each carry one meaning, rounded lowercase headlines, celebration on the reward beat. Those calls flip several defaults, like whisper-thin shadows and one-accent palettes.

  **Changes nothing (avoid):**

I like clean UI.

  **Changes output (do this):**

1px borders everywhere. If a border needs more weight, raise its contrast, never its width.

Two kinds of entry are useful: **hard overrides** ("I never use X") and **taste calls** ("when in doubt, lean toward A because B"). A taste call should orient a decision, not amputate a possibility.

> **Two things outrank taste.** The accessibility floor and reduced motion. White text on a bright green fill fails contrast however on-brand it is; celebration is the first thing to drop when someone asks for less motion.

## gotchas.md: the lived failures

gotchas.md (One dated line in gotchas.md recording something the agent got wrong in practice, and the fix. The file only grows, and negative examples teach the agent the most) is append-only. Every time the agent gets a UI detail wrong in practice, you add one dated line.

```text
- [2026-10-30] Agent declared color-scheme: light dark on a light-only app; phones in dark mode rendered dark inputs on white cards. → Declare only the schemes that were designed. See [[contrast-and-color-scheme]].
```

Why it matters: the main nodes encode the happy path. Gotchas encode the failure, with the exact mistake. Perplexity's agent-skills team found negative examples are often the highest-signal content in a skill over time. The description should change rarely; this file should grow steadily.

| Put it in gotchas | Put it in pov |
|---|---|
| "The agent did this wrong; here's the right answer." | "I prefer this default over the canon's." |
| one line, linked to a node | as long as the call needs |

**Log a gotcha (paste into your agent):**

```text
/design-engineering pov gotcha "the agent hid the closed drawer with opacity: 0 and keyboard users tabbed into it"
```

## Lines that change behaviour

Agents are probabilistic: ask twice, get two answers. A skill can't force the same *output*. It can force the same *process*, so the reasoning is yours every run. The skill's writing rules (How to write a skill that changes behaviour: encode a process, write the why, be strict, make every line earn its place, keep one skill per job, and test by running):

1. **Encode process.** Give a decision tree the agent walks every time.
2. **Write down the why.** A rule with its reason gets extended to cases you never wrote.
3. **Be strict.** "Reasonably short" changes nothing; "under 300ms" does.
4. **Every line earns its place.** Explaining what a button is dilutes the lines that matter.
5. **One skill per aspect.** Building animations and reviewing them are different jobs.
6. **Test by running.** Unsure if a line matters? Run with it and without it.

Try it on a draft. Tap each line to rewrite it by those rules.

*[Interactive demo: Hedges become numbers, rules gain a why, and what the agent already knows gets deleted.]*

**Strict is not long.** A strict file is usually shorter than the hedged one it replaces, because every hedge was a sentence that did nothing.

## Test across models

A skill that loads perfectly on one model may never load on another. Perplexity's team put it plainly: Sonnet and GPT behave quite differently with skills. Most authors test on the model they happen to use.

  **Step: Loading.** Ten queries that should load the skill; it loads on all ten.

  **Step: Not loading.** Ten nearby queries that shouldn't; it loads on none.

  **Step: The right node.** Five queries mapped to specific nodes; the agent opens those.

  **Step: The right output.** "Review this CSS" comes back as the table.

Known patterns: GPT routes on broad nouns like "CSS" more eagerly; Sonnet can skip a description over about 80 words; Haiku may stop at the top file and never open the notes; Opus over-reads. A gap of 5% or more between models on the same set is a routing problem.

*(Use real queries from real users. Ones you wrote yourself only prove the description matches you.)*

## Measure whether a change helped

Taste is trained, but it can also be measured. Two public benchmarks show the shapes worth borrowing:

| Benchmark | What it measures | Borrow |
|---|---|---|
| DesignBench (WebPAI) | generation, editing and repair of real web UI, scored by compiling, visual similarity and a model judge | the three tasks: real work is mostly edit and repair |
| Design Arena | anonymous side-by-side votes, ranked with a Bradley-Terry fit | blind pairwise judging |

"Is this good?" has no honest answer. "Which of these two is better?" does. So when you change a node that alters output, generate before and after for a few fixtures, judge them blind, and record the win rate. A node that loses the pairwise (Judging two outputs side by side, without knowing which is which, and picking the better one. Honest where 'is this good?' isn't; the win rate tells you if a change helped) more than it wins gets reverted, however good it reads.

One trap: similarity scores reward matching the reference, flaws included. A repair that fixes an overflow the reference also had can score *lower*. Read the pairwise result first.

**Check yourself:** Your team always wants 1px borders, raising contrast instead of width when a border needs more weight. Canon doesn't say this. Where does it go?

- [ ] gotchas.md, as a dated failure line
- [x] pov.md, as a strict taste call: '1px borders everywhere; for more weight raise contrast, never width'
- [ ] The skill's description, so it loads more often
- [ ] Nowhere: mention it in each prompt

*Why:* A preference that overrides or extends the canon is a taste call, so it belongs in pov.md, written strictly. gotchas.md is for a specific thing the agent got wrong, with its fix.

## In the skill

- https://school.agents.org.in/skill/meta/pov.md
- https://school.agents.org.in/skill/meta/gotchas.md
- https://school.agents.org.in/skill/meta/skill-writing-rules.md
- https://school.agents.org.in/skill/meta/cross-model-testing.md
- https://school.agents.org.in/skill/meta/design-benchmarks.md


---

# One command, many verbs

> Learn how /design-engineering reads its first word, what happens with no word at all, which aliases mean the same verb, and the short verbs that route a job and stop.

Module: The /design-engineering toolkit · Level: beginner · 8 min · https://school.agents.org.in/design/toolkit/meet-the-toolkit

Type `/design-engineering` into your agent and press Enter, with nothing after it. Nothing runs.

That's on purpose. Watch what it does instead.

*[Interactive demo: No verb, no guessing: a menu that leads with what fits, then a wait.]*

## One entry, one word

The skill has a single slash command (A command you type into an agent starting with /, like /design-engineering. It runs a skill or a built-in action instead of being read as a normal message). Everything else is the first word you type after it, the verb (The first word after /design-engineering, like review, polish or motion. It picks which procedure the skill follows), followed by a target: a file, a folder, a symptom in quotes.

```bash
/design-engineering review src/PricingCard.tsx
```

The skill reads that first word and looks it up in its dispatch table (The table at the top of the skill that maps each verb (and its aliases) to the file with its procedure. The skill reads your first word and looks it up there). If it's a verb, it opens that verb's command file (The written procedure behind one verb: its posture, what to read in order, the steps, what to return and where to hand off. Each one works on its own) and follows it exactly. Each command file is a whole procedure on its own: what to read, in what order, what to return, where to hand off.

> **In plain words.** Think of the verb as the name of a recipe. `review` is one recipe, `polish` another. The words after it are the ingredients: which file to work on.

## The four rules of dispatch

  **Step: A verb (or its alias).** Open its command file. The rest of your words are the target.

  **Step: A plain request.** "Why does this card feel flat" isn't a verb, so the router runs: it finds your design contract, works out the phase, and names one or two owners.

  **Step: No words, but a design request already in the chat.** Treat that request as the plain request.

  **Step: No words, no request.** Show the menu, lead with the 2–3 verbs that fit what's in front of it, and wait.

What "fits" means: a diff on your branch leads with `review`; a `.design` file leads with `tokens`; a video file leads with `film review`; launch week leads with `ship`. **A bare command never auto-runs a verb.** A wrong guess spends a whole pass, and surprises nobody wants.

If two verbs fit your words, it asks once. If none fits and the job isn't design (backend logic, data, build tooling), it says no skill applies.

## Which verb? Describe it

You don't need to memorise the table. Describe your situation in your own words and see which verb answers it.

*[Interactive demo: Your words in, the verb and its exact command out. Try the examples or type your own.]*

## Aliases: other names for the same verb

Most verbs answer to a second or third name, an alias (Another name the same verb answers to, like critique for review or launch for ship, so whichever word comes to you first works), so whatever word comes to you first works.

| Verb | Also answers to |
|---|---|
| `review` | `review-ui`, `critique` |
| `polish` | `finish` |
| `motion` | `motion-audit`, `animate` |
| `mobile` | `native-feel` |
| `break` | `worst-case`, `break-ui` |
| `tells` | `unslop`, `scan-ai-tells` |
| `a11y` | `accessibility` |
| `tokens` | `contract`, `apply-design-md` |
| `ship` | `prep`, `prod`, `launch` |
| `film` | `film-review`, `video` |
| `stage` | `faux-ui`, `demo`, `bento`, `product-shot` |
| `help` | `menu`, `?` |

## The short verbs

A handful of verbs are small: each hands the job to one owner and stops where that owner says stop.

| Verb | Does | Stops after |
|---|---|---|
| `options` | three to five real directions behind a picker | your pick |
| `name` | the precise word for a thing | one line, no code |
| `which` | who owns a job, and why | three lines; it doesn't do the job |
| `pick` | one maintained library for a task | one recommendation, or "a few lines of code would do" |
| `diagram` | whether to draw at all, and where | the figure or the canvas |
| `agentation` | the fix loop for browser annotations | one annotation at a time |
| `pov` | fork your taste file, or draft a gotcha | your yes, never an overwrite |

`options` has its own small modes once you've seen the first three:

  **Step: riff.** Three more directions near the one you liked.

  **Step: tune.** A control panel on the kept direction, and nothing else.

  **Step: mix.** Parts of the other variants carried onto your chosen base, as a list of portable features.

  **Step: keep.** Write the decision down, delete the harness, keep the worst-case data.

**Three small verbs (paste into your agent):**

```text
/design-engineering name "the menu grows out of the button that opened it"
/design-engineering which "a toast that screen readers never announce"
/design-engineering options riff
```

*(`pick` reads your package.json first. It never adds a rival to something you already have.)*

## Every verb keeps its posture

Each verb names a posture (Which of four jobs a request is: Build (make it), Judge (assess it, no edits), Decide (make a call first) or Name (give the word). It fixes the shape of the answer) and holds it. `review`, `tells` and `a11y` judge: they report and never edit unless you ask afterwards. `polish`, `mobile` and `ship` fix. `break` is an adversary that reports and *stops*. `name` answers and stops.

And every verb applies your two override files, gotchas and pov, plus your project's design contract, which outranks both.

**Check yourself:** You type /design-engineering with no words. You're on a branch with four changed files. What happens?

- [ ] It runs review on the branch automatically
- [x] It shows the menu, leads with review and one or two others, and waits
- [ ] It runs every verb in order
- [ ] It errors, because a verb is required

*Why:* A bare command never auto-runs anything. It shows the menu, leads with the verbs that fit what's in front of it (a diff suggests review), and waits for you.

## In the skill

- https://school.agents.org.in/skill/commands/MOC-commands.md
- https://school.agents.org.in/skill/commands/cmd-small.md


---

# Verbs that judge

> Use review for a verdict and a Before | After | Why table, tells for a deletion list of AI defaults, and a11y for an evidence-only accessibility audit, and know what each returns.

Module: The /design-engineering toolkit · Level: beginner · 8 min · https://school.agents.org.in/design/toolkit/verbs-that-judge

Three verbs read your work and report. None of them edits a line unless you ask afterwards. That's the deal: you get an honest assessment, and you decide what changes.

## review

**Reach for it** when something is done, or a change is ready to merge, and you want to know if it's right.

*[Interactive demo: review: a verdict on the first line, then three to five rows of before, after and why.]*

```bash
/design-engineering review src/PricingCard.tsx
```

**What it does.** It reads the review format, the 39-row checklist, your gotchas and your pov, in that order. Then it quotes your real lines into the Before column, runs every checklist row against them, and turns each failure into a row.

**What comes back:**

1. **A verdict, first line.** *Blocked* (an interaction or accessibility-floor failure), *Approve with changes*, or *Ship it*.
2. **The Before / After / Why (The table a design-engineering review comes back in: the exact line, its replacement, and one sentence of reason. One problem per row) table.** Three to five rows. Twelve rows aren't actionable.
3. A one-line lede if the issues share a theme.

Rows are ranked in a fixed order: interaction, accessibility floor, layout stability, motion, spacing, type, surface, component API, performance, polish. If there are more than three, they're grouped under Blocking, Should fix and Nits.

> **Point it at a branch.** Give it a branch, a pull request or nothing at all with uncommitted work, and it becomes a change-scoped review: a Status column (Introduced, Regression, Pre-existing), the removed lines read for lost signals, and at most three pre-existing findings.

*("Ship it, nothing to change" is a valid review. Invented nitpicks are not.)*

**Hand off** when motion is most of the problem (`motion`), when the question is "does this look AI-made?" (`tells`), or when it's the whole app before launch (`ship`).

## tells

**Reach for it** when a screen looks like every other generated page and you can't say why.

*[Interactive demo: tells: a numbered deletion list, then one line on what to enlarge so the page isn't left thin.]*

```bash
/design-engineering tells app/page.tsx
```

**What it does.** It picks the surface and reads only that surface's notes: a page, a screenshot, copy, code or a dashboard each have their own catalogue of AI-default tells. Then it runs the unslop pass in order.

**What comes back:** a deletion list (A numbered list of things to remove or replace, highest impact first, each with where it is and why. What the tells verb returns instead of a redesign). Numbered, highest impact first, each item saying what to remove or replace, where it is, and which node names the tell. Gradient text, the generated-page purple, a tiny tracked-caps label above every heading, emoji as icons, "John Doe" in a testimonial.

**Every tell you remove must leave something better behind.** Stripping a page and leaving a thin screen of nothing is its own failure, so the list ends with what to keep or enlarge.

  **A redesign (avoid):**

"Here's a new direction with a warmer palette and a serif headline."

  **A deletion list (do this):**

"1. Remove bg-clip-text on the h1 (solid ink). 2. Swap indigo-to-purple for the brand accent. 3. Delete the FEATURES eyebrow. Keep the product shot; enlarge it."

`tells` never proposes a new visual direction on its own. If you want one after the scan, that's `options`.

## a11y

**Reach for it** when you need to know who your interface locks out, and you want proof rather than opinions.

*[Interactive demo: a11y: every row tied to a WCAG criterion, or honestly labelled a preference.]*

```bash
/design-engineering a11y components/Dialog.tsx
```

**What it does.** It reads the accessibility baseline and the WCAG (The Web Content Accessibility Guidelines, the standard most laws point to. Its contrast rule is the familiar 4.5:1 for body text and 3:1 for large text) thresholds, then only what the target contains: widgets get keyboard maps, toasts get live-region checks, motion gets reduced-motion checks, colour gets contrast.

**What counts as a finding:** a WCAG criterion it fails, or a behaviour you can reproduce (focus escapes the dialog; the toast is never announced). Anything else is labelled a *preference*.

**What comes back:** the review table with a **Criterion** column (`4.1.2 A`, `1.4.3 AA`, or "preference"), most severe first.

> **Severity follows the criterion.** A 36px icon button on desktop meets the 24×24px minimum (criterion 2.5.8). 44px is a recommendation, never the top severity. The skill learned this the hard way; it's in its gotchas.

**An audit you can act on (paste into your agent):**

```text
/design-engineering a11y components/Dialog.tsx
Then: fix only the rows with a criterion. Leave preferences for me.
```

## Which one?

| Your question | Verb |
|---|---|
| "Is this right? Can I merge it?" | `review` |
| "Why does this look generated?" | `tells` |
| "Who can't use this?" | `a11y` |

All three are judges. The fix comes after, when you say which rows to apply.

**Check yourself:** You run /design-engineering tells on your landing page. What should you expect back?

- [ ] A new visual direction with a different palette
- [ ] The page rewritten without the tells
- [x] A numbered list of things to delete, with locations and the node for each, plus what to keep
- [ ] A WCAG audit

*Why:* tells is a judge verb. It returns a prioritised deletion list, not a redesign and not edits, and ends with what to enlarge so the page isn't left empty.

## In the skill

- https://school.agents.org.in/skill/commands/cmd-review.md
- https://school.agents.org.in/skill/commands/cmd-tells.md
- https://school.agents.org.in/skill/commands/cmd-a11y.md


---

# Verbs that fix

> Run polish when a screen works but feels unfinished, motion when something moves wrong (or shouldn't move), mobile when it feels like a website on a phone, and tokens to thread your design contract through the code.

Module: The /design-engineering toolkit · Level: intermediate · 9 min · https://school.agents.org.in/design/toolkit/verbs-that-fix

The judge verbs report. These four edit your code, then show you what changed. Each one starts by checking it's the right moment to run at all.

## polish

**Reach for it** when the screen works, the layout is right, and it still feels unfinished: nothing obviously wrong, just not *done*.

*[Interactive demo: polish: the edits first, then up to five rows of the changes that matter most.]*

```bash
/design-engineering polish app/settings/page.tsx
```

**It checks the phase first.** If the layout or hierarchy is still wrong, polishing is wasted work, because the polish gets thrown away with the layout. It says so and fixes the foundations instead.

**Then it walks the pass in order: states before decoration.** Hover, focus, active, disabled, empty, loading and error. Then the optical details: nested radii (inner = outer − padding), whisper-thin shadows, line wrapping. Every edit uses your contract's tokens, never new literal values.

*(A press state with no `:active` is the most common thing polish adds.)*

## motion

**Reach for it** when something moves wrong: it feels janky, slow, hesitant or bouncy. Or when you're about to add an animation and want to know if it should exist.

*[Interactive demo: motion: should it move? Then every After names its milliseconds, curve and properties.]*

```bash
/design-engineering motion components/Dropdown.tsx
```

**It decides first.** If the motion shouldn't exist (it fires a hundred times a day, or on a keystroke, or has no purpose you can name in a word), it recommends removing it and stops. That's a valid result with zero code.

Then it opens only the notes for the pattern in front of it: enter and exit, hover and press, drag and momentum, lists, a state change in place, or jank.

| You give it | It returns |
|---|---|
| existing code | the review table; every After names ms, curve and properties |
| a new animation to build | the code, then three lines: gate result, ingredients, what to feel-check |
| jank | the cause named before any change, layout causes before paint |

It has three modes that change its posture:

| Mode | Does |
|---|---|
| `motion remove` | subtracts only: walks every animation through the gate and deletes or reduces, with what was kept and why |
| `motion opportunities` | read-only: at most 5–7 places motion should exist, a required *Rejected* list, one top pick |
| `motion audit` | read-only on source: a whole codebase, written up as numbered plan files for another agent to execute |

**Remove before you add (paste into your agent):**

```text
/design-engineering motion remove app/
Then: /design-engineering motion opportunities app/
Only after both, pick one opportunity to build.
```

## mobile

**Reach for it** when the app "works in Chrome but feels wrong on my phone": hover that sticks after a tap, inputs that zoom the page, a keyboard that covers the button.

*[Interactive demo: mobile: a few lines, not a report. What was wrong, what changed, and what still needs a phone.]*

```bash
/design-engineering mobile "the keyboard covers the send button"
```

**It matches the symptom to its platform cause** and applies the narrowest fix, in your styling system: `user-select: none` on controls, never on the whole page; hover behind `(hover: hover) and (pointer: fine)`; inputs at 16px so iOS doesn't zoom.

It prefers CSS and meta tags over script: no detecting phones by user agent, no `maximum-scale=1`, no blocking scroll with `preventDefault`.

> **Honest about what code can't prove.** Sticky hover, the tap flash, input zoom, safe areas and the keyboard only show up on real hardware. mobile lists each unverified fix with exactly what to look for on a phone, and never claims it checked.

## tokens

**Reach for it** when your project has a design contract and the agent keeps ignoring it: raw hex values, almost-right radii, Inter where your brand says something else.

*[Interactive demo: tokens: the map from each token to where it lands, and any literal it kept, with the reason.]*

```bash
/design-engineering tokens
```

  **Step: Find the contract, nearest first.** Walk up from the edited file for a `.design`; then look for `DESIGN.md`. If both exist, the `.design` is the contract and DESIGN.md is the rationale.

  **Step: Read the normative core.** For a `.design`: instructions, intent, constraints, policy, tokens, voice, components, in that order.

  **Step: Map and apply.** Tokens onto CSS variables, a Tailwind theme or component props.

  **Step: Respect precedence.** Your prompt, then the contract, then the skill, then the model's defaults.

**No contract?** It says so, scans what your code already uses, and offers to bootstrap one. **It never invents an untracked design system silently.**

## In what order?

For one screen, the skill's screen loop puts these in a fixed order: foundations, one material pass, then **polish**, then **motion**, **mobile** or **a11y** only if that's the problem, then review. One or two at a time, looking at the screen between them.

| The screen… | Verb |
|---|---|
| works but feels unfinished | `polish` |
| moves wrong, or you're not sure it should move | `motion` |
| feels like a website on a phone | `mobile` |
| ignores your colours and radii | `tokens` |

**Check yourself:** You ask for polish on a screen whose layout is still clearly wrong. What should the verb do?

- [ ] Polish it anyway: states and shadows are always worth it
- [x] Say the layout is the problem and fix the foundations first
- [ ] Run motion instead
- [ ] Refuse and do nothing

*Why:* polish checks the phase first. Polishing a layout that's going to change wastes the pass, so it names the real problem and starts from the foundations.

## In the skill

- https://school.agents.org.in/skill/commands/cmd-polish.md
- https://school.agents.org.in/skill/commands/cmd-motion.md
- https://school.agents.org.in/skill/commands/cmd-mobile.md
- https://school.agents.org.in/skill/commands/cmd-tokens.md


---

# break and ship

> Run break to feed one component the realistic worst case and stop with a report, and ship to sweep the whole app before launch, fix in order, and say what still needs a phone or a person.

Module: The /design-engineering toolkit · Level: intermediate · 8 min · https://school.agents.org.in/design/toolkit/break-and-ship

Two verbs test against reality instead of taste. `break` asks what one component does with real data. `ship` asks what the whole app does in its first hour.

## break

**Reach for it** when a component renders data it didn't write: names, emails, counts, labels from an API. Especially before you trust a list.

*[Interactive demo: break: what broke and how badly, worst first, then the toggle to see it yourself. It stops there.]*

```bash
/design-engineering break components/MemberRow.tsx
```

Its posture is unusual: **adversary, then judge**. It builds the worst case, shows you, and stops. It never fixes until you name which breaks to fix.

  **Step: Map.** Every value the component renders, with its source and its real limit from the schema, the API type or the form's maxLength. No limit anywhere is written down as "unbounded", which is already a finding.

  **Step: Build the worst case.** One fixture (A saved set of test data shaped like the real thing, such as a worst-case list of members. Kept in the project so the next person can see the same breaks) shaped like your demo data, every value plausible or at the real limit, failures spread across the first rows the way real data does. Plus Empty, One, and 1,000 rows if the list isn't paginated.

  **Step: Wire a toggle.** A dev-only switch (Demo, Worst case, Empty, One, 1,000) that swaps the data at the data boundary, never by editing the markup. The choice lives in the address: `?data=worst`.

  **Step: Break it.** At the real width, at 320px, at the widest layout, at 200% zoom, and right-to-left if you ship it. It says which breaks it saw and which it inferred from CSS.

  **Step: Report and stop.** Then waits.

**What comes back:**

| Section | Holds |
|---|---|
| What broke | #, Severity, Field, Worst-case value, What happens, Fix with file and line. Worst first. |
| Decisions for you | breaks with more than one right answer: wrap or truncate? paginate or virtualize? |
| What held up | the cases already handled, so you trust the test |

Severity has three levels: **Broken** (unreadable, unreachable or wrong data), **Ugly** (readable but visibly off), **Fragile** (fine until one realistic step more).

**Then fix some (paste into your agent):**

```text
fix 1, 3
```

After a fix, it flips through *every* state again, including the happy-path demo, so a worst-case fix can't quietly break the normal case. The fixture stays as a regression test; only the toggle goes, when you ask.

*(Taste problems aren't break's job. "It's ugly with normal data" is review or polish.)*

## ship

**Reach for it** last: launch week, on an app whose design is settled. It fixes defects, not taste.

*[Interactive demo: ship: a verdict first, what it fixed, then what still needs a real device.]*

```bash
/design-engineering ship
```

Its posture is **fix**. It sweeps the whole app, not one change, across nine lanes (One of the nine areas a ship check sweeps: accessibility, performance, mobile, forms, states, motion, theme, content and leftovers, and marketing): accessibility, performance, mobile, forms, states, motion, theme, content and leftovers, and marketing (only if marketing pages exist).

It comes in sizes:

| Invocation | Does |
|---|---|
| `ship` | recon, every lane, vet, fix, build, report |
| `ship audit` | the same sweep, no edits; every row stays Open |
| `ship quick` | blockers only, every lane, fixed. The hour before a demo. |
| `ship mobile` (or any lane) | recon plus that lane; lanes combine, and compose with `audit` |
| `ship app/checkout` | everything, scoped to one path |

**How it works.** Recon first: the stack, the root layout and head, which routes are product and which are marketing, every data view, existing tokens, where each dev tool mounts. Then each lane against its owner node. Then it **vets**: re-reads every cited line itself and drops anything by-design, duplicated or misattributed. A finding not confirmed at its file and line is never shown.

Then it ranks with the severity tree and fixes in order: accessibility blockers, leaks, the production baseline (one change that clears dozens), per-file fixes, polish. Then it runs your typecheck, build and tests.

> **Never invent.** Copy, a brand colour, a link-preview image, the header height, testimonials: it reads them from the code. If they're missing, it flags the gap. Invented content in production is worse than a flagged hole. It also asks before any fix that changes chosen behaviour, like removing an animation or a route.

**What comes back:**

1. **Verdict, first line.** *Ship*, or *Not yet* with each open blocker in one line. A leaked secret's "rotate it now" sits above even that.
2. **One table per lane** with findings: Severity, Before, After, Why, Status (Fixed or Open, with the reason). A clean lane is one line: "Performance: clean."
3. **Needs a device.** The exact thing to look for: "open the drawer on an iPhone; the padding clears the home indicator."
4. **Needs a human.** Unapplied fixes, each with why, the change it would make, and the owner.
5. **Verification.** The commands it ran and their output, verbatim, failures included.

**"Already in good shape, small diff" is a valid and good result.**

## review, break or ship?

| You have | Verb |
|---|---|
| one change to judge | `review` |
| one component and real-world data | `break` |
| the whole app, and a launch date | `ship` |

**Check yourself:** It's the hour before a demo. You want only the things that would embarrass you fixed, across the whole app. Which invocation?

- [ ] /design-engineering review
- [x] /design-engineering ship quick
- [ ] /design-engineering ship audit
- [ ] /design-engineering break app/

*Why:* ship quick sweeps every lane for blockers only and applies the fixes. audit wouldn't edit anything, review judges one change, and break tests one component's data.

## In the skill

- https://school.agents.org.in/skill/commands/cmd-break.md
- https://school.agents.org.in/skill/commands/cmd-ship.md


---

# Verbs that make things

> Use sound to decide which moments earn a sound and build one family, svg to create or animate vectors, film to plan or measure a launch video, and stage to build faux product UI for a landing page.

Module: The /design-engineering toolkit · Level: intermediate · 9 min · https://school.agents.org.in/design/toolkit/verbs-that-make

The last four verbs make new things: sounds, vectors, films and product demos. Each still starts with a decision, and each comes back in a shape you can check.

## sound

**Reach for it** when you're wondering if your product should make a sound, or when the sounds it makes feel stock, late or annoying.

*[Interactive demo: sound: a sound map where most moments get 'none', and the one that survives says why.]*

```bash
/design-engineering sound
```

Its posture is **decide, then build**. If you only asked *whether* something should make a sound, you get one line and it stops.

For product UI, it classifies every proposed moment and **rejects most of them**. The test is the hundredth use, not the first: a click sound is charming once and maddening by lunch. The survivors become one family made of one material, generated (with ElevenLabs if you have a key, or an offline synthesiser if not), and wired so that sound is off by default, unlocks on your first tap or click, and lands its sharp start on the exact frame the animation makes contact.

**What comes back:** a sound map (A table of every proposed moment with the sound it gets (often none), its material, length, trigger and the reason. What the sound verb returns) (Moment, Sound, Material and contour, Length, Trigger, Why), then the files and the wiring.

For a video, the defaults flip: sound is the medium and silence is placed on purpose. It picks the film's sound register first.

## svg

**Reach for it** when you need an icon, a logo mark or an illustration made or cleaned up, or when you want one to move.

*[Interactive demo: svg animate: the engine for where the file lives, the values, the reduced-motion path, and the size.]*

The second word picks the job:

| Invocation | Does |
|---|---|
| `svg create` | draws on a whole-number grid with named groups and `currentColor`, optimises it, returns the file and its size before → after |
| `svg animate` | decides whether it should move, picks the engine for where the file lives, adds the reduced-motion path, returns the motion plan and the file |
| `svg sticker` | an animated isometric sticker for an empty state or onboarding |

```bash
/design-engineering svg create icons/export.svg
```

The engine follows the file's home. Inline in the page: CSS or the Web Animations API. Used as an `<img>`: keyframes embedded in the file itself, because page CSS can't reach inside. A messy file with no `viewBox` or unnamed groups gets `create` first.

## film

**Reach for it** to plan a launch video before anyone opens an editor, or to measure one that's rendered.

*[Interactive demo: film review: measured numbers beside the targets for the film's register, never a vibe.]*

| Invocation | Posture | Does |
|---|---|---|
| `film plan` | decide, then build | names the register (The kind of launch film you are making, such as UI motion graphics, product camera or kinetic type. It sets the cut rate, stillness and loudness) first, then the beats, the type, the product demo, the motion, the cuts and the sound |
| `film review launch.mp4` | judge | measures at the native frame rate, builds contact sheets, reads the loudness, recounts every cut by eye |
| `film formats` | build | a loop, a logo sting, or a vertical cut |

```bash
/design-engineering film plan "60s launch for the export feature"
```

With no second word, a video file means `review` and anything else means `plan`. A URL is never downloaded without your yes: it tells you the filename, source and size first.

*(film review never edits the composition. It measures and reports.)*

## stage

**Reach for it** when a landing page, feature card, docs page or bento grid needs a picture of your product: a still, or a short demo with a cursor.

*[Interactive demo: stage demo: the plan before any code, then the component, contact sheets and a rehearsal note.]*

```bash
/design-engineering stage demo "the export flow, cursor clicks Export"
```

| Invocation | Returns |
|---|---|
| `stage still` | a framed product shot, at its real size plus a 358px phone design |
| `stage demo` | the still as its opening frame, a 3–5 beat story of 10–16 seconds, a poster frame, a pause control |
| `stage bento` | a planned grid of feature tiles, checked at its widest and narrowest |
| `stage review` | judge only: frames it films itself, ranked framing → hierarchy → motion → polish |

Every job starts the same way: your contract's colours, type and radii become the demo's look, and the plan is written before code (where it sits, the one thing that happens, the 3–7 real strings, what's left out). With Benji Taylor's Stageshow installed, the build goes to it. The demos in this course are built that way.

**A demo that passes review (paste into your agent):**

```text
/design-engineering stage demo "AI answer with sources" for the hero beside our headline.
Write the plan first. Real strings only where the story is.
Film it at 720 and 390, step through every beat, and hand me REHEARSAL.md with what you didn't check.
```

## The whole toolkit, one table

| Verb | Posture | Returns |
|---|---|---|
| `review` | judge | verdict + Before / After / Why |
| `tells` | judge | deletion list |
| `a11y` | judge | table with a WCAG criterion per row |
| `polish` | build | edits + up to five rows |
| `motion` | decide, then judge or build | values in ms, curve, properties |
| `mobile` | fix | wrong / changed / needs a phone |
| `tokens` | build | token map + edits |
| `break` | adversary, then judge | Broken / Ugly / Fragile, then stop |
| `ship` | fix | verdict, lanes, device and human lists, build output |
| `sound` | decide, then build | sound map + files |
| `svg` | build | file + size, or a motion plan |
| `film` | decide or judge | a plan, or measured rows |
| `stage` | build (review judges) | component + contact sheets |

**Check yourself:** You ask /design-engineering sound to add sounds to a to-do app. It proposes sounds for 'task added', 'task completed', 'button press' and 'list reorder'. Which is it most likely to keep?

- [ ] Button press, because it's the most frequent
- [ ] All four, to make the product feel alive
- [x] Possibly task completed, a meaningful moment; the frequent ones get rejected
- [ ] List reorder, because drags need feedback

*Why:* sound judges each moment by its hundredth use. Things that happen hundreds of times a day (presses, reorders) get rejected; a meaningful, less frequent moment may earn one.

## In the skill

- https://school.agents.org.in/skill/commands/cmd-sound.md
- https://school.agents.org.in/skill/commands/cmd-svg.md
- https://school.agents.org.in/skill/commands/cmd-film.md
- https://school.agents.org.in/skill/commands/cmd-stage.md
