---
name: drop-authoring
description: Use when asked to design or change a visible surface in a product that renders a vendored design drop — a new card, panel, stage, state, or restyle — and when deciding whether a request is a drop change at all or legitimately belongs in the consuming app's code. Covers the authoring sequence, what the closed vocabulary forces you to decide up front, the five fixture states, and how the result is verified. For wiring live data into an existing drop, use the `node-design-seam` skill instead.
---

# Drop authoring

The front half of the loop. The `node-design-seam` skill covers the back half —
adopting a drop and wiring data into it. This one covers being handed "add a
card to that screen" and getting it into the drop instead of into app code.

## The one rule

**A design surface lands in the drop. Never rebuild one in the consuming
app's code.**

The drop is rendered byte-for-byte and hash-pinned precisely so there is no
second copy to drift. A surface built in app code is that second copy, and it
does not survive the next drop.

This is not hypothetical. In the codebase this package was extracted from, a
per-app resource card was built in app code with its own stylesheet. It looked
right. Then the next drop shipped its own version of that screen, which replaced
the host stage the card was mounted in: the card vanished, its CSS went dead and
was deleted, and a button elsewhere was left offering to show a page that no
longer had it. The work became a numbered request to the design side — which is
where it should have started.

## Step 1 — which side of the seam is this?

Answer before touching anything.

| The request | Where it goes |
|---|---|
| Anything **newly visible** — a card, a panel, a state, a restyle, moving something | A drop change. Author a new drop, or file it with whoever ships drops for this surface. Never app code. |
| A value on screen is a **literal that should be real** | Also a drop change, but a small one: it needs a `data-bind*` annotation or a view module. Integration cannot add one from outside. |
| A marker expressing **integration state, not design content** — "not wired yet", a trust indicator, a connection warning | App-code overlay is legitimate. Keep it layout-neutral (`pointer-events: none`, out of flow), and delete it the moment the design ships a real treatment. |
| Integration says the vocabulary **can't express** something | Neither a new verb nor an invented selector. It is a view module, or a slot the drop must declare. See the `node-design-seam` skill's table of the three requests that look like missing verbs and aren't. |

If it's a drop change and you are not the one shipping drops, stop and file it.
Describe what the surface shows, the shape of the data that would feed it, and
name a reference rendering explicitly as field-for-field rather than a
prescription for layout — layout is the design side's call, and a request that
over-specifies it tends to come back as the thing you drew rather than the thing
they'd have drawn.

## Step 2 — the authoring shape

`templates/AGENTS.stage-drop.md` in this package is the contract; copy it into
the drop root as `AGENTS.md` and author against it. What that file states as
rules, this section states as **sequence** — the order that avoids rework.

**Decide what is continuous or structured before you lay anything out.** The
closed vocabulary sets text, one allowlisted attribute, or a state attribute —
never a width, a path, an arc, or anything *inside* a string. Everything in
that category is a **view module**: charts and meters, and any value carrying
structure in its own text.

The template enumerates those cases and the `node-design-seam` skill's table
explains why each is not a missing verb. What neither can give you is the
timing, which is this skill's whole reason for existing at this point in the
sequence: an integrator meets the limit at the wall, when the layout already
exists and the fix is a rebuild. You meet it on a blank page, where it costs
nothing. **Decide before the steps below, not during them.**

**Then, in order:**

1. **Markup and appearance** — `stage.html` + `stage.css`. Assume a shadow
   root: inherit nothing, leak nothing, no external URLs, no CDN fonts.
2. **Name the slots.** Semantic and namespaced to your surface (`myapp.*`).
   You own the vocabulary; developers conform their data to your names, never
   the reverse. Scalars get `data-bind` / `data-bind-attr` / `data-bind-when`,
   and the literal you write **is** the fixture.
3. **Place each view module** with `data-view-slot="<the module's own id>"`.
   That attribute is the entire contract between markup and module — nothing
   else may be targeted to place rendered output.
4. **One seam per element.** An element is attribute-bound or render-fed, never
   both; two writers on one text node and no reviewer can tell which drew it.
5. **Design every state you want to exist.** Integration cannot write a class
   or a style, so a status colour, a stale dimming, a collapsed panel must key
   off an attribute you declare and style yourself. If you don't design it, it
   cannot happen at runtime. Theme included — a shadow root inherits none, so a
   multi-theme surface reads its theme from a state slot like any other data.
6. **Read-only unless the ask says otherwise.** Local-only interactions (tabs,
   expand/collapse, hover) are entirely yours and never touch integration's
   vocabulary.

## Step 3 — five fixture states, not one

`default | loading | empty | error | overflow`, per view module.

**`overflow` is the one that gets skipped and the one that breaks layouts.**
Design it against your actual worst case: longest real strings, fullest real
lists, longest real identifiers, largest realistic counts. If a real value could
be longer than your `overflow` fixture, the fixture is what's wrong — and
production is where you'll find out.

`loading` and `error` matter for a blunter reason: a state with no design gets
invented during integration, under deadline, by whoever is wiring data. Every
improvised element in a shipped product traces back to a state nobody drew.

## Step 4 — craft: conform, don't import an aesthetic

If you are extending an existing drop, **read the neighbours first**. Take type,
colour, spacing, radius and motion from that drop's own `stage.css` — its
custom properties are the design system, and a surface that reads as foreign is
a defect however good it looks alone. Spend novelty only where the ask calls
for it.

**Deliberately do not delegate this to a general-purpose frontend-design
skill.** Those skills exist to invent a distinctive aesthetic from scratch and
to emit components and CSS in app code. Both assumptions are wrong here: the
design language already exists and the job is conformance, and the output
belongs in drop files under this contract. If you use one for private
ideation, its output is translated into drop files and is never the source of
a class name, a colour, or a typeface.

## Step 5 — verification is visual

The acceptance surface for your side of the contract is a **pixel golden per
fixture state**, produced independently of any integration:

```sh
npx node-design-seam preview --drop .
```

No config, no app, no developer, and it writes nothing into your folder. Fixture
state × theme × width across the top; view modules are fed their own
`fixtures[state]` and the page names any module missing one. Scalar slots show the
literals you authored — add an optional `tools/preview-states.js` exporting `states`
(a snapshot per state) and optionally `themes` to drive the other four.

**Do not hand-write a preview page.** The served one imports the real
`mountDropFragment` / `createSeam`, so it cannot disagree with what ships. A harness
that interprets `data-bind*` itself can, and then the golden agrees with the harness
instead of with production — the pixel comparison stops meaning anything. If this
drop still carries a `tools/preview.html` from before the command existed, delete it
and re-capture. It is also a scan hazard: it sits under the drop root, so a literal
`data-bind="…"` written in it as an example mints a phantom slot.

- Compare in a **real browser with a GPU, at a fixed viewport. Never headless,
  never against a degraded reference.** Reasoning about CSS does not settle
  rendering questions — in the codebase this package came from, three separate
  CSS-level theories about a reported rendering complaint were all wrong, and
  the font one was disproved only by rendering both declarations at 8× and
  finding them pixel-identical.
- A golden written on a **first run against a missing baseline is a bootstrap,
  not a comparison.** Declare it as a new baseline in the hand-off; never
  present it as a passing diff.
- Goldens are reviewed like code. A golden you updated on purpose is stated as
  such in the hand-off.

## Step 6 — the hand-off

- `AGENTS.md` copied forward into the drop root, unmodified, with its
  `contract:` line intact — that line is how the integrating CLI knows which
  write-vocabulary you authored against.
- Every asset shipped inside the drop folder, hashed with it.
- The slot inventory: what you declared, what changed since the previous drop,
  and anything you deliberately left as a literal.
- Any edit outside your own surface's folder called out explicitly, one line
  each. A silent edit outside your scope is a broken hand-off even when the
  code is right.

## Red flags — stop

- You are about to add a component or a stylesheet to the **consuming app** for
  a surface the drop renders.
- You are drawing a chart, bar or gauge as markup with a bound number.
- You are about to ask integration for a verb that sets a width, a style, or
  wraps a substring.
- You are targeting a class, an id, or structural position to place a view's
  output instead of `data-view-slot`.
- An element carries a `data-bind*` **and** an interpolated value.
- You are naming slots after where the data lives in someone's backend rather
  than what it means on your surface.
- You shipped `default` and called the view done.
- A state exists in your head but has no attribute you declared and styled.
- You are pulling a font, an icon set, or a stylesheet from a URL.
- You are presenting a bootstrapped golden as a passing comparison.
- You are hand-writing a preview page that reads `data-bind*` itself, instead of
  running `node-design-seam preview`.
