genjutsu

Principles

The rules genjutsu enforces, and why each one exists: detect without prescribing, the thesis as the only authority, evidence over ticks, duration by context, easing by direction, reduced motion, the five prohibitions, and the six rules that hold when bunshin hands the work to a team of agents.

All documentation

These are the positions the whole thing is built on. They are not preferences and they are not configurable, because a position you can switch off is not a position.

Detect, never prescribe

genjutsu names what is wrong with evidence and a question. It never answers with a look to copy. No module says “use this instead”, and none carries a house style, because a house style is only a different set of defaults: swap one for another and every project still converges, just somewhere new.

A tell is reported as the marker, the model behaviour that produces it, and the question that decides it. Deciding is your thesis’s job.

The thesis is the only authority

A pattern is a tell only when the validated thesis did not ask for it. A studio that really works from Paris and Tokyo can keep a clock for each city, if the thesis says so.

The exception has to be named. A mood word such as “editorial” or “agency” unlocks nothing, because most tells are exactly the clichés of an agency portfolio. And something you asked for in the brief has to be carried into the thesis by name: it is the thesis that allows it, not the brief.

So the thesis names what it allows in one visible line under it, Allowed patterns:, which you read before you validate it: the mono labels, the paper ground, the looping animation, whatever the design deliberately relies on. A pattern missing from that line is audited as a tell, however well it fits the mood.

The thesis is also what the finished code is audited against. Every promise it made, the durations, the easing, the palette, the type, the layout family, is matched to the file:line that keeps it, or reported as missing.

Evidence, not ticks

Every check distinguishes “checked, here is the evidence” from “not checkable here, here is what to run”. A verdict with no evidence beside it is not a finding. Frame timing, real breakpoints and device settings are handed over marked unverified, never ticked on the agent’s word. An honest audit reads “9 checked, 2 problems found, 8 handed over”; a list of green ticks does not.

Duration comes from context, not taste

Context Duration Why
Micro-interaction: toggle, hover, focus 100 to 150ms Instant feedback, no perceived delay
UI transition: modal, drawer, tab switch 200 to 300ms Smooth without dragging
Page or route transition 300 to 500ms Long enough to establish where you went
Scroll-driven or 3D No duration Tied to input, so it is progress-based

And the rule underneath the table: the more often an animation plays, the shorter and subtler it has to be. A button hover fires a thousand times a day and gets 100ms of opacity. An onboarding reveal fires once, ever, and can afford 600ms of full choreography. Applying the onboarding budget to the button is how an interface becomes exhausting.

Easing comes from direction

Action Easing Why
Entering ease-out, or a spring Decelerates into place, like arriving
Exiting ease-in Accelerates away, getting out of the way
Moving between states ease-in-out Smooth at both ends
Scroll-synced linear Anything else reads as lag against the input
Playful Underdamped spring Overshoot is what reads as alive

Exit is always more subtle than enter. Enter can be 300ms with translate, opacity and scale together. Exit should be 200ms of opacity. Asymmetry in that direction is correct; the reverse is a bug that feels like the interface is reluctant to let go.

Reduced motion is a requirement

Not a nice-to-have, not a phase-two item, and not something to add if a user complains.

Platform API
Web, CSS @media (prefers-reduced-motion: reduce)
Web, JS matchMedia('(prefers-reduced-motion: reduce)')
SwiftUI @Environment(\.accessibilityReduceMotion)
UIKit UIAccessibility.isReduceMotionEnabled
Compose A helper over Settings.Global.ANIMATOR_DURATION_SCALE

An animated project with no handler anywhere is a critical audit failure. And respecting the preference means providing the finished state, not a broken one: a reduced-motion visitor should see the composition, just not the motion into it.

Two more, easy to forget: focus indicators must never be hidden by an animation, and animated text must meet its contrast ratio at every frame, not just at the end. Mid-transition is where fading text quietly drops below the line.

The five prohibitions

Each of these has a wrong and a right form in all three stacks.

1. Never animate width, height, top or left. They trigger layout on every frame. Animate transform and opacity, which composite on the GPU. In Compose that means graphicsLayer { translationY } rather than animateDpAsState on a size. In SwiftUI it means .offset rather than .frame(height:).

2. Never scale to zero. scale(0) makes an element vanish into a point. Scale to 0.95 and fade instead.

3. Never ease-in on an entrance. Ease-in means a slow start, and an element that hesitates on the way in reads as broken.

4. Never exceed 500ms on a UI interaction. Modals, dropdowns, tooltips and tabs all have a user waiting on the other side of them.

5. Never skip reduced motion. See above.

Performance is part of the design

Only transform and opacity get animated, because they are the composite-only properties. will-change is used sparingly and removed when the animation ends, because permanently promoting a dozen elements costs more GPU memory than it saves in paint. Animation loops use requestAnimationFrame, never setTimeout, which drops frames and keeps running in a background tab.

For scroll-driven work the order of preference is CSS animation-timeline first, then IntersectionObserver, then a scroll listener as a last resort. And the target gets verified on throttled hardware, not on the machine that wrote it.

If the motion a thesis promised cannot be delivered in working order, the honest fallback is the static version, said out loud. Half-broken motion is never the answer.

Consistency is auditable

A well-designed project uses three to five distinct durations and three to five named easings. Twelve different cubic-bezier values scattered across a codebase are not a design decision, they are the absence of one. When the audit finds that many, the fix is to name them and centralise them, not to argue for them.

When a team does the work

bunshin hands a whole site to a team of subagents, and a fan-out breaks in ways one agent never does: ten clones can produce ten opinions, two of them can write the same file, and a long run can spend a budget nobody agreed to. Six rules hold it together. They come from a run that worked, and from what broke in it.

Two human touches

You are asked twice: once to summon, with the tier and the product questions, once to choose the direction, and once more only to go past the tier’s round cap. There is no other question, not even the preview menu cast and paint open. Every other decision is the pipeline’s, made from the material, the research and the reviews, and written in the report. A run that asked at every step would be paint with more agents; a run that never asked would spend your budget on a direction you did not choose.

One art director

The main agent writes the direction contract, builds the foundation and the signature surface itself, and decides what the reviews mean. Clones build inside the contract and never redefine it. Turning a cold-eyes review into rules for the next round is art direction, so it is the one step that is never delegated.

Disjoint ownership

Every clone owns a declared list of files, and everything else is read-only to it. A shared need is a request, never an edit. When a shared contract changes, its owner runs first and alone. Two clones editing the global stylesheet at once is how a fan-out ends in a merge by hand.

Evidence or it did not happen

Every finding cites a capture region or a file:line, every “done” has a build behind it that printed exit code 0, and every behaviour is reported with the value a scripted test measured. Reviewers judge the captures of what renders, not the code’s intentions, and nothing is reviewed until it has been built and captured.

The loop ends on a rule

One review, then rounds of refine, prove, verdict and decisions, until nothing above minor is left, the tier’s round cap is reached, or a round fixed nothing. The finish reviewer’s disposition is read before anything else: evidence that failed is captured again before it binds anyone. Never on a feeling, never past the cap without asking. Minor points left at the end go into the report, not into one more round.

Cost announced first

Nothing heavier than reading happens before you pick a tier, and the first message says what each one is expected to cost and where the figures come from: one measured run, a seven-page site in two languages, and two unit costs that run did not measure, estimated instead. It also names the stack and the scaffold it would install, because your answer to the tier covers that install and nothing else. With nobody there to answer, nothing is spent unless the request names a tier. After each fan-out the run notes how many agents it used, and the tokens when the host reports them.