# Mockup process, as it actually ran

Claude's account of five days of work with Adi on this homepage, 2026-09-03 to 2026-09-07. A record of
what happened rather than a proposal. Adi's own version is in `mockup-process-draft.md`; this is the
complement, written from the other side of the loop.

**What got made:** 53 standalone option sheets, 55 deployed HTML files, 4 versions of Direction A and 2
of Direction B, and 46 numbered decisions with 54 follow-ups recorded across 2,275 lines of notes. One
homepage.

---

## 1. The shape of a round

Almost every one of the fifty-odd rounds had the same shape, and it never varied much:

```text
Adi names one element  →  I build N variations on a standalone page  →  he looks at it in a browser
→  he names one  →  I merge it and regenerate the other versions  →  I record what was decided and why
```

**A real one, start to finish.** Adi: *"create a new html file called accordion-test.html … my goal is
distinguish between paragraph and list … make it in 5 variations."* I built five treatments of the same
list. He picked the hairline rows, then asked for a second sheet of dotted variants of that one choice.
He picked *"Dotted rule above each row"*. I merged it, and the reasoning went into note 22.

**Two rounds, one element, one axis each.** That is the whole method. The first sheet chose the
technique, the second chose its execution. Neither round could have failed expensively, because
nothing had been built into the real page yet.

## 2. What made his instructions work

This is the part most worth copying, because it is not obvious and it is not about design.

**He named the element, never the outcome.** *"the button here is off"*, *"i want svg art on the right
of the FAQ heading"*, *"make step 1, 2, 3 use similar start line"*. Never *"make this section better"*.
A named element produces a sheet; an outcome produces a guess.

**He asked for a number.** *"5 variations"*, every time. It removes my judgement from the part where my
judgement is worst.

**He rejected without softening it.** *"none of them looks logical"*, *"you are AI you don't understand
how ugly the building idea is"*, *"cancel my previous 40% 60% idea"*. Total time lost to a rejected
sheet: zero, because nothing had been merged. **Blunt rejection is only cheap because of the isolation
rule, and it is the single biggest speed advantage in the method.**

**He supplied his own material.** Stock uploads, Gemini generations, a screenshot of the whole page when
he wanted me to see it as a reader would. He did not wait for me to offer options he already knew he
wanted.

**He corrected the process, not just the output.** *"i see you are locating too when my instruction is
just file name, i want to avoid consuming too much token"*. That changed how I worked for the rest of
the project.

## 3. What I did mechanically

Worth writing down because it is repeatable and most of it should be a tool rather than a script per
sheet.

**Every option sheet was generated, not hand-written.** A Python script read the real page's source,
pulled its `<link>` and `<style>`, scoped every CSS selector under a `.board` prefix, and emitted the
variations against the live design system. **That is why a sheet looked exactly like the page.** They
were all the same shape, and building it once as a tool is the highest-value automation left on the
table.

**Images were cropped to the slot, not to taste.** Read the slot's aspect ratio out of the CSS first,
then crop: 5:7 for the umbrella, 1:1 for the transformation, 16:9 for a mega panel. Crop position was
chosen to hold the subject, not centred by default.

**Measurement, through headless Chrome.** Inject a script that writes measurements into
`document.title`, dump the DOM, read the title. Crude and completely reliable. It is how three
invisible bugs were found.

**Every decision was written down the same day**, with the reasoning and, where it existed, the source.

## 4. Where I was wrong, and what caught it

Five real failures in five days. All of them were caught by Adi or by measurement, none by my own
confidence, which is the point.

| What I got wrong | What caught it |
| --- | --- |
| Told him a client rule was our invention. I had quoted the client's sentence **halfway**, and the second half said exactly the thing I claimed was ours | Reading the planning doc properly, one day late |
| "Since 2004" for the firm's age, argued from their About page | **Adi**, with a state filing and an image filename |
| A find-and-replace matched `stepcta` inside `ministepcta` and silently deleted two thirds of a section | Reading the file back after the edit |
| Shipped a fix for staggered buttons that did nothing, because I put the class on the wrong element | Measuring, and finding the numbers identical |
| Built six variations of a subject that had already been rejected once | Adi, saying so |

**The pattern is consistent.** Every error was fluent, internally consistent, and well argued. None of
them felt different to write than the correct work. That is the whole case for the checks in Adi's
document: they are not ceremony, they are the only thing standing between a fast process and fast
nonsense.

**The one that generalises:** I verified a fact against the client's own website, reasoned correctly
from it, and was still wrong, because the source I checked did not answer the question being asked.
**Checking a source is not the same as checking that the source answers your question.**

## 5. Three things measurement found that nobody could see

Listed because they are the strongest argument in the whole document for a mechanical step.

- **A sticky header that never stuck.** A parent had `overflow-x:auto`, which silently made it a scroll
  container, so the header stuck to that instead of the viewport. Measured `top: -1800px` at
  `scrollY: 1800`.
- **An animation that never stepped.** The CSS scoper had prefixed keyframe selectors, producing
  invalid CSS that the browser discarded without a word. The marquee glided smoothly instead of
  stepping, and looked completely fine.
- **A grid running at 934/256 instead of 503/681.** A grid item's automatic minimum size is its
  min-content width, and for an `<img>` that is the file's intrinsic width. Pre-existing, invisible for
  weeks, and only obvious once a landscape image made the proportions wrong.

**Invalid CSS fails silently and a declared behaviour is not a working behaviour.** All three looked
correct in a browser.

## 6. What the record says about the work

- **Imagery took more rounds than anything else**, and the answer was constrained the whole time by a
  client instruction we had in writing from day one. Reading the register properly at the start would
  have saved several days.
- **The best decisions came from the copy, not from design.** The strongest section on the page pulls
  one line out because the writer put *"Most importantly"* in front of it. The best image is a base
  plate with a bolt missing because the section says *"address risks before they become costly
  problems."*
- **Adi's own idea was killed by Adi**, one step after it stopped earning its place. Nothing in the
  process would have caught that. It is judgement, and it is why step 4 is still a person.

## 7. What I would change

1. **Build the sheet generator as a tool.** Fifty-three sheets, one shape, scripted individually.
2. **Read the register before the first sheet, not on day three.** Every imagery round was constrained
   by a rule that already existed in writing.
3. **Split the notes.** 2,275 lines is a superb record and an unusable reference. A one-screen register
   of standing rules, plus the long log.
4. **Measure after every merge, not when suspicious.** Two of the three invisible bugs sat there for
   days.
5. **Treat "make more variations of X" as a signal that X is not working**, rather than as approval of
   X.

---

## 8. File and naming convention, for next time

Agreed with Adi, 2026-09-07, after reading the above. This supersedes the `prototype/` idea in the
earlier draft: mockups stay where they are and get organised in place.

### Location

```text
/srv/apps/{projectname}/wp-content/mockups/
├── index.html                      navigation only, working pages
├── img/                            every image the mockups use
├── home_A_v1.html                  the deliverable pages
├── home_A_v2.html
├── home_B_v1.html
├── about_A_v1.html
├── _easel_ready/                   packaged bundles, ready to upload
│   └── home_A/
│       ├── index.html              both looks in one file, see below
│       ├── img/
│       └── assets/
└── _drafting/                      everything that is not a deliverable
    ├── index.html                  contact sheet: element, date, tested, won
    ├── home_hero.html              element sheets: {pagename}_{element}
    ├── home_steps.html
    ├── home_faq-art.html
    ├── about_hero.html
    └── home_A_v3.html              retired versions keep their full name
```

| Path | Holds | Client sees it |
| --- | --- | --- |
| `mockups/index.html` | Links to the working pages, nothing else | Yes |
| `mockups/img/` | Every image, served locally | Via the pages |
| `mockups/{page}_{type}_{version}.html` | The deliverable pages | Yes, by URL or Easel |
| `mockups/_easel_ready/{page}_{type}/` | A packaged bundle, ready to upload to Easel | Yes, through Easel |
| `mockups/_drafting/index.html` | The contact sheet for everything below | No |
| `mockups/_drafting/{page}_{element}.html` | Option sheets and proofs | No |
| `mockups/_drafting/{page}_{type}_{version}.html` | Retired page versions kept as evidence | No |

**`index.html` is reserved and means one thing:** the navigational page that links to the internal
working pages. **Working pages only.** Test sheets, variation sheets and proofing pages are excluded
from it deliberately, so the index stays a map of the deliverable rather than a list of everything on
disk. That is also why no page file is ever called `index.html`.

**`img/` is not optional.** Every image a mockup uses is served from here, never hot-linked from a
foreign URL. Two reasons: an external image can vanish or change under us, and **Easel needs the assets
local when the mockup is packaged for the client.** A mockup that renders from someone else's server is
not a deliverable.

**`_drafting/` holds everything that is not a deliverable**: option sheets, proofs, and retired
versions. Underscore rather than a dot, deliberately. **Tested on this container: a dot-directory is
served over HTTP exactly like any other** (`GET /wp-content/mockups/.drafting/probe.html` returned
200), and directory listings are already 403 for every directory either way. A dot would only hide the
folder from `ls`, while implying a privacy it cannot deliver. The underscore sorts it to the top, stays
visible to the people working on it, and promises nothing false.

**What keeps drafts away from the client** is `index.html` linking working pages only, plus the client
only ever receiving specific URLs or an Easel board. Nobody browses the folder. **So `_drafting/` is
organisation, not security** — anything in it is reachable by anyone holding the URL, and nothing
genuinely private goes there.

**`_drafting/index.html` is what makes the folder usable.** Listings are off, so without it a sheet is
only reachable by someone who remembers the exact filename, which is how 53 sheets became unfindable on
this project. One row each: **element, date, what was tested, what won.** That turns a junk drawer into
the reasoning record — progress checking, proof of what failed, and the argument behind a decision, all
one click away. It is also the cheapest version of a decisions register, and it could eventually be
generated from the notes instead of maintained by hand.

### `_easel_ready/` — packaging two versions into one bundle

Easel presents a mockup as a **folder**, and its variation switch works by toggling a class on
`<html>`. So two page files become one bundle, and this is the step that converts our working files
into something the client can be shown.

**One correction to the naming.** The target cannot be `_easel_ready/home_A.html`, because the bundle
needs its images alongside it and **Easel looks for `index.html` as the entry**. So it is a folder
named for the page and direction, with a fixed entry filename inside:

```text
_easel_ready/home_A/
├── index.html          merged from home_A_v1.html + home_A_v2.html
├── img/                only the images the page actually references
└── assets/             logos and other shared files it references
```

**How the merge works, and the rule that matters most.**

`home_A_v2` is `home_A_v1` plus a small nameable change. Merging keeps **all of the variant's markup**
and scopes **only its CSS** to a variation class:

| | Rule |
| --- | --- |
| Class name | `html.v-{variation-name}`, e.g. `v-brand-circles` |
| **The base is the absence of the class** | There is no `v-base`. With no class on `<html>`, the file renders exactly as v1 |
| The guard | The variant's elements need a rule that hides them by default, so unstyled wrappers do not render |
| Scoping direction | Either scope the variant's CSS to `html.v-name`, or scope an existing hide rule to `html:not(.v-name)`. Both work; pick whichever is fewer edits |
| Images | Copy **only what the page references.** The `easel-home-A` bundle carried 17 of the 22 images in `img/`, leaving out unused treatments |

**Why one bundle rather than two folders.** Two folders means uploading the same photographs twice,
re-downloading them on every switch, and losing the client's scroll position each time they toggle.
One bundle means the browser adds a class and repaints: nothing fetched, nothing reloaded, no jump.

**Verify by rendering, not by reading the CSS.** The failure mode is a base that shifts when it should
be byte-identical to v1. Check that the page with no class still matches, then check the class turns
the variant on.

**Worked example on this project.** `easel-home-A/` was built from `home-A-v1` and `home-A-v4`, where
v4 was v1 plus a CSS block, one class on the hero, and four wrapper divs holding ring SVGs. All that
markup stayed; the CSS was scoped to `html.v-brand-circles` with `.brandring { display:none }` as the
guard. 22 files, 1.5 MB, one 156 KB `index.html` serving both looks.

### Making a bundle: the procedure

**Scenarios.** Decide which one you are in before starting, because the answer changes the work.

| You have | Do this |
| --- | --- |
| **One version** | No merge. Package only: copy the page as `index.html`, copy the assets it references, verify, upload |
| **Two versions, small difference** | Merge into one `index.html` with a variation class |
| **Two versions, large difference** | **Stop. These are not versions, they are directions.** They go to Easel as two separate options, each with its own bundle |
| **Three or more** | One class per variant, plus an Easel variation **group** so only one is active at a time. Variants that touch the same elements must be in the same group |

**The gate, and it is worth taking seriously.** If the diff between two files runs to hundreds of
lines, they are `A` and `B`, not `A_v1` and `A_v2`, and merging them produces a bundle nobody can
maintain. The naming convention already encodes this: same letter means small nameable difference.
**If you cannot name the difference in one short phrase, do not merge them.**

**Steps.**

1. **Diff the two files** and read every hunk. On this project v1 and v4 differed by a CSS block, one
   class on the hero, and four wrapper divs.
2. **Take the variant's markup wholesale** into the merged file. Markup is inert without its CSS.
3. **Scope the variant's CSS** to `html.v-{name}`, or scope an existing hide rule to
   `html:not(.v-{name})`. Whichever is fewer edits.
4. **Add the guard.** Anything that would show without the class gets a default `display:none`. This is
   the step people skip, and it is what makes the base shift.
5. **Copy only referenced assets.** Grep the merged file for `src=` and `url(` and copy exactly those.
6. **Name the entry `index.html`.** Easel will not find it otherwise.
7. **Verify by rendering** (below). Never by reading the CSS back.

### Proving it is Easel ready

**The whole risk is that the base moved.** The client is meant to see v1 exactly as approved, and a
merge that shifts it by four pixels destroys trust in everything else on the page. So the test is a
comparison, not an inspection.

```bash
# 1. base: merged file with no class, against the original v1
google-chrome --headless --disable-gpu --no-sandbox --window-size=1440,20000   --screenshot=/tmp/a.png file:///…/home_A_v1.html
google-chrome --headless --disable-gpu --no-sandbox --window-size=1440,20000   --screenshot=/tmp/b.png file:///…/_easel_ready/home_A/index.html

# 2. variant: same file with the class forced on, against the original v2
#    (add <script>document.documentElement.classList.add('v-brand-circles')</script>
#     to a throwaway copy, or set the class in the markup for the test)
```

Then compare. Identical page height is the fastest signal that nothing shifted; a pixel diff is the
proof.

**Pre-upload checklist.**

- [ ] Entry file is named `index.html`
- [ ] **Base renders identical to the original v1**, same height, same pixels
- [ ] **Class renders identical to the original v2**
- [ ] Every `src=` and `url(` resolves inside the bundle. No `http://` or `https://` except the font
      stylesheet, and no absolute paths beginning with `/`
- [ ] Only referenced assets copied. Unused treatments and rejected crops stay out
- [ ] Class name is `v-{something-readable}`, matching what the Easel setup panel will show
- [ ] Bundle opens correctly from `file://`, which is the cheapest proxy for opening from anywhere

### The prompt

Paste this, filling in the three names. It states the traps as requirements, which is why it works.

> Build an Easel bundle at `wp-content/mockups/_easel_ready/{PAGE}_{DIRECTION}/`, merging
> `{PAGE}_{DIRECTION}_v1.html` as the base with `{PAGE}_{DIRECTION}_v2.html` as a variation named
> `v-{NAME}`.
>
> Rules:
> 1. **The base is the absence of the class.** Do not create a `v-base`. With no class on `<html>` the
>    merged file must render byte-identical to v1.
> 2. Keep all of the variant's markup. Scope only its CSS to `html.v-{NAME}`, or scope an existing
>    hide rule to `html:not(.v-{NAME})`, whichever is fewer edits.
> 3. Add a default `display:none` guard for any variant element that would otherwise render unstyled.
> 4. Name the entry file `index.html`.
> 5. Copy only the assets the merged file actually references, preserving relative paths.
> 6. **Verify by rendering, not by reading the CSS.** Screenshot the original v1 and the merged file
>    with no class at 1440 wide and confirm the page heights match and the pixels match. Then confirm
>    the class turns the variant on.
> 7. Report the file count, the bundle size, and both page heights.

**Both kinds of file in `_drafting/` earn their place.** Element sheets carry the page prefix because
`hero.html` goes ambiguous the moment an About page exists. Retired page versions keep their full name,
because a superseded version is often the evidence for why the current one looks as it does: on this
project `home-A-v3` exists purely to show what following the brand guide literally produces.

### Naming

`{pagename}_{type}_{version}.html`

| Example | Reads as |
| --- | --- |
| `home_A_v1.html` | Home, direction A, version 1 |
| `home_A_v2.html` | Home, direction A, version 2 |
| `home_B_v1.html` | Home, direction B, version 1 |
| `about_A_v1.html` | About, direction A, version 1 |

**The letter and the number mean different sizes of difference, and this is the whole point of the
scheme.**

- **Same letter, different number: nearly identical.** `home_A_v2` is `home_A_v1` plus one small,
  nameable change. On this project, v1 is the base style and v4 is the same page with circle branding
  added. If you cannot say the difference in one short phrase, it is not a version.
- **Different letter: a different direction.** `home_A_v1` and `home_B_v1` are expected to look
  substantially unalike. Different structure, different density, different answer to the same brief.

### What A and B actually mean

A working doctrine, not just a label.

**A is our preferred design.** What we would recommend, given the brief plus our judgement as web
designers.

**B follows the instruction.** The client's direction, built as well as we can build it.

**Both get our best work. That is the whole point.** B is not a control, not a demonstration, and never
an argument that the client is wrong. It is their idea, taken seriously and made as good as it can be
made. **We are not trying to win.** We are giving them two genuine proposals and letting them choose
with something real in front of them, which is far more respectful than telling them their idea will
not work.

**Why B is worth building at all.** The brief comes from the people who know the business. They hold
insider knowledge of the market, the customers, the objections and the subject that a web designer will
never have, and **B is that knowledge rendered exactly as they expressed it, unfiltered by our
interpretation.** What they usually do not have is a real page in front of them, so an instruction can
read perfectly in a document and behave differently once built: a section runs longer than imagined, a
density that sounded clean turns busy, a hierarchy shifts once real copy is in it. B lets them see
their own direction properly built, which no amount of discussion can substitute for.

**The two directions carry different expertise, and neither one is senior.** B holds theirs. A is our
interpretation of the same thinking with the page craft applied. **The risk in A is ours to watch: an
interpretation can quietly drop something they knew mattered and we did not.** That is exactly why B
gets built rather than described.

**Ask who wrote the brief, because it is not always the client.** It may come from a partner agency. The
principle is not "the client is always right", it is **proximity to the source of truth**, and on most
projects somebody sits closer to the business than we do.

| Written by | What it carries | How to treat it |
| --- | --- | --- |
| The client | Direct knowledge of their own business | Highest deference. Render it faithfully |
| A partner agency | Often longer and more frequent contact with the client than we have. On this project the marketing partner had been talking to them well before we existed | High deference. Raise strains **with them**, not around them |

**Whether AI helped write it changes nothing.** Anyone may use it, us included, and the responsibility
sits with whoever put their name to the brief. Treat a brief by its author, not by the tool. If a
section reads fluent but thin, that is a question for the author, not a reason to discount what they
sent.

**And route the conversation correctly.** When a partner wrote the brief, findings go to the partner
first. Going around them to the client is both discourteous and a good way to get two conflicting
answers, and on this project the split is already explicit: content and messaging decisions belong to
the marketing partner, not to us and not to the client alone.

**If B has limits, they come from the instruction, never from our effort.** That distinction is the
whole integrity of the exercise. A B built carelessly is a straw man: the comparison stops being
honest, and clients can tell. If B is not chosen, it has to be because of the direction itself and not
because we put less into it.

**Silence is not an instruction.** A brief only states what its author thought to state. Everything it
does not mention is assumed to be handled competently, not assumed to be absent. Nobody buying a car
specifies that it comes with wheels, that the tyres are rubber rather than iron, or that the bearings
turn. **Following a brief means doing what it says, plus everything a competent web designer would
obviously do that nobody should have to ask for.**

The unstated baseline, present in B whether the brief mentions it or not:

- Works on a phone, and at every width in between
- Readable contrast, visible focus states, alt text, sensible heading order
- Links that work, buttons that look pressable, hover and active states
- Images sized for their slot, pages that load
- One type scale, one spacing rhythm, one icon family
- Forms that validate, and that tell you what happened after you submit
- Nothing that breaks at 1366px because it was only ever checked at 1920px

**The test, when you are unsure.** Is this a decision the brief *made*, or a topic the brief never
*addressed*? A decision gets followed. A silence gets best practice. *"No team photographs on the home
page"* is a decision. Missing alt text is not a decision, it is an omission, and shipping it as
obedience would be malpractice wearing obedience as a costume.

**When it is genuinely ambiguous, ask in one line.** It is far cheaper than either guess.

**The discipline.**

1. **Build B as instructed, and build it well.** Full effort, same care as A, same finish.
2. **Do not silently turn it into A.** Quietly fixing the parts we disagree with is not obedience, and
   it takes the choice away from the client.
3. **Where the instruction strains, say so plainly and offer the fix.** With specifics, in the
   walkthrough or the review, framed as *here is what we noticed and here is what we would suggest*.
   Naming it is help; hiding it is not.
4. **Show both.** Two finished proposals side by side is a conversation held with artifacts instead of
   adjectives, which is the same reason option sheets work.

**How that played out here.** Direction B, built from the brief, came back carrying seventeen icons
where the brief allowed eleven. Nobody had done anything wrong and nothing was being proved; the count
simply behaved differently on a real page than it did in a document. We raise it, we suggest a fix, and
the decision stays theirs.

### One note on this project

The files here use hyphens (`home-A-v1.html`) rather than underscores. **Leave them.** The convention
above starts on the next project, where it costs nothing; renaming 55 deployed files now would break
every link already shared and buy nothing.
