# easel — changelog (newest first)

## 0.62.0 (2026-09-17)
**React to a comment, Slack's way.**

rian asked for a quick "like" on a comment. Under every comment (notes on the
design, their replies, general comments on a concept or a page, and comments
on walkthrough points) there is now a smiley that offers 👍 ❤️ 😂 🎉 👀 ✅.
Each emoji in use shows as a chip with its count and, on hover, who used it.
Yours is highlighted; pressing it again takes it back, and pressing someone
else's adds yours.

- `comment_reactions` table (migration a2c4e6f8b0d2): one row per person per
  emoji per comment. `POST /api/comments/{id}/reactions {emoji}` toggles.
  Anyone who can read the comment can react; only the six offered emoji are
  accepted; a removed comment carries none and takes none.
- Every comment in the API now carries `reactions: [{emoji, users}]`.
- Nobody is notified of a reaction: acknowledging is not news.
- `caddie-ui`: new `Reactions` piece, with `onReact` on `ThreadCard` and
  `ThreadReplies` and `reactions` on `ThreadRow`/`ThreadReply` (README row).
  The choices open in flow beside the smiley, so a scrolling panel cannot
  clip them.
- The tool introduction's example discussion shows a reaction on a reply.
- Tests: `test_comment_reactions.py` (4). 281 pass.

## 0.61.0 (2026-09-16)
**Notes stay on what they were about. Client comments reach the team.**

Alicia left 39 notes on Equinox and asked whether the dots move around
after you place them. They did, and not by user error. rian could see the
dots were out of whack, and found her comments hard to find: he expected
them in his unreads.

**Why the dots moved.** A note stored its place as a percentage of the whole
page. The page changes height whenever something on it opens or closes, and
Alicia had opened two items of the services accordion and three FAQ answers,
so the page was about 850px taller while she commented. Every note was
stored against that taller page: for everyone else each one sat above its
words, by up to a few hundred pixels in the middle of the page, and in her
own session the notes slid each time an accordion opened or closed.
Confirmed by rendering the design headlessly in all 256 accordion states
and matching her notes against the words they name: as the page loads they
miss by 1.08 percentage points on average, with those five items open they
land within 0.2.

- **A note now names its place on the design**: the mockup element under
  the point and the point's position inside it (`pins.anchor`), measured
  back live the way walkthrough highlights are. Opening an accordion moves
  the notes below it with their words. The percentages stay as the
  fallback. A note inside a closed accordion answer sits on its question.
- **Everything measured in the frame is measured again whenever the design
  reports a change**, not only when its total height changes (a closed
  accordion leaves the frame as tall as it was).
- **Alicia's 39 notes were re-anchored** by replaying her page in that
  state and naming the element under each one.
- **Every client comment reaches the team**: a new kind, `comment`
  ("alicia commented on Equinox · Illustrated"), for each comment from the
  client side that the team is not already told about as a mention or a
  reply. Existing client comments were backfilled into the team's unreads.
- **The board card shows the notes on a design** ("39 notes on the
  design"), linking straight into the discussion. The comment count beside
  it is the general comments only, which is why 39 notes read as "1 comment".


## 0.60.1 (2026-09-10)

- Re-vendored the BW Auth pack. Admin, People: the owner can now give a member a
  new password (the "New password" button), shown once with a copy button so it
  can go in your own email; nothing is emailed, and they are signed out
  everywhere. It works for accounts used only in this app; anything broader is
  reset from auth.bowden.works/admin.

## 0.60.0 — 2026-09-08
**Zoom, so the margins are visible.**

rian: sometimes there is design hidden in the margins, invisible in the
presentation because we set a fixed width. A zoom, defaulting to 80%, that
reveals more of the margins, with 100% one click away for the real font
sizes.

The frame lays out at the design's own width divided by the zoom, so 80%
gives the page a quarter more room and the stage scales it down to fit: the
same page, smaller, with more of it showing. The control sits beside the
variations switch and is remembered per reader in their browser.

**The part that took the care.** Every anchor easel stores (a marker, an
area, a hand-placed highlight) is a percentage of the design at its own
width, so laying the page out wider would have slid every note off the
thing it was left on. Anchors now come through the zoom on the way out and
back through it on the way in: `x·zoom + 50(1 − zoom)`, which holds the
centre still and moves each anchor by half the slack, exactly as the
centred page moves. A box drawn edge to edge stays edge to edge, because it
means the whole band. Selector-targeted highlights re-measure in the frame
and were always right; they now re-measure when the width changes too.

## 0.59.3 (2026-09-08)

- Re-vendored the BW Auth pack. Adding a person who has no Bowden Works account
  now offers a choice: email them a set-password invite, or generate a password
  and send nothing (shown once, with a copy button) so you can write your own
  onboarding message. Typing an email also proposes the username from it.

## 0.59.2 — 2026-09-07
**A way in to "Who can see this project".**

rian could not find how to see and manage who is on a project (equinox is
next). The page existed, `/p/<project>/members`, the kit's roster with add,
level and remove; the board never linked it. Team tools now has "Who can
see this project".

## 0.59.1 — 2026-09-07
**Fix: the @ search never ran under a polling parent.**

The composer's debounce depended on the search function, an inline arrow
that is a new identity every time the panel re-renders, and the panel
re-renders on every poll. The timer restarted each time and the request
never went. The function now lives in a ref and the debounce depends on
the typed query alone.

## 0.59.0 — 2026-09-07
**@ searches people. The Discussion button nudges.**

rian: review lets you type @ and search the users, and that is important.
And make the Discussion button glow or bounce every once in a while while
the panel is closed, with a comment icon, so people remember that is where
they comment.

- **`Composer.mentions`** in the caddie-ui pack: type `@` and a list of
  people appears under the box, name and `@username`; arrows move, Enter
  or Tab inserts, Escape closes the list and only the list. The app
  supplies the search; easel's is `GET /api/projects/{id}/people?q=`, the
  project's audience, which is exactly who a mention reaches, names from
  the directory cached ten minutes. Every composer has it: the discussion
  panel's replies and starters, the concept cards' threads.
- **The Discussion button** carries the comment icon and, while the panel
  is closed, hops and glows for a second every fourteen seconds. Still
  while open, and still under prefers-reduced-motion.

## 0.58.0 — 2026-09-07
**Mail goes out. A digest an hour. The welcome once, ever. Self-mentions.**

rian, testing as darren: after the whole walkthrough he lands back on the
welcome. Adi's test comments fill the bell. And email has to work: password
resets, the team told on Feedback complete, a digest when notifications
have gone quiet for an hour (review's rules for what lights the bell), and
a client account, test@rian.ca, to test it all with.

- **The transport.** The account service gained one route, `app-send-mail`
  (gateway + id-auth + the kit's `bw_auth.send_mail`): an app mails one of
  its own SIGNED-IN users a message of its own; the address never leaves
  the gateway; separate daily caps per app (300) and per account per app
  (40). easel's outbox now delivers through it, holds a failed send as
  `send_failed` and retries it for a day. Dev Mode still holds everything.
- **The digest** (`services/digest.py`): once a person's unread items have
  gone an hour with nothing new, one email, "New notifications in your
  Design Easel", listing them; the items are stamped delivered and never
  mailed twice; anything read in time is not mailed. A background loop
  ticks every five minutes in production; the dev-mode bar has "Send
  digests now" (no quiet wait) and the outbox shows the result.
- **The bell's rules** match review's already (a mention, a reply in a
  thread you are in, a turn); the one gap, a mention of yourself as a
  reminder, now works.
- **The welcome, once.** Seen is seen at any version: a changed introduction
  no longer re-offers itself. And this browser remembers it too, because
  the "seen" write is refused while viewing as someone else.
- **Feedback complete** already rings the team's bell and queues their
  email; with the transport it now arrives.
- The sixteen test notifications on league are gone. test@rian.ca was
  invited as a client member of league (the invite went out by email).

## 0.57.1 — 2026-09-07
**Three introduction points fewer, one more: the variations switch.**

rian: drop "Why the photos are blurred" and the two restart points (the
board's "Restart any time" and the viewer's "The walkthrough"); add a point
on the options that can be turned on and off. Part 1 is six points, part 2
five: where you are, Variations (the switch in the app bar, skipped on a
design without one), a comment being left, Discussion, the bell. The
hand-off sentences moved to the new last points. TOOL_TOUR_VERSION 4.

## 0.57.0 — 2026-09-07
**rian's eleven notes on the league designs, done.**

rian left eleven "FOR CLAUDE" comments in easel itself, on the two league
concepts and their walkthrough points, and asked for them to be acted on
and then removed. Each one, and what it became:

- **A walkthrough point can be said in a centred card.** New flag on a
  point (`walkthrough_steps.modal`, the editor's "Say this in a centred
  card, with no highlight"): the card sits over the dimmed design with the
  panel's outline beside it, Next goes on to the next point. Used for the
  concept walkthrough's welcome on The Direct Answer (rian's words: the
  focus is the general design, photos blurred on purpose, text later) and
  for a first word on Option B (more traditional, closer to the existing
  site, images up top, practice areas above the fold).
- **The Direct Answer, the mockup itself.** The photo strip became a
  slider of the latest articles (picture, date, title; three real titles,
  three placeholders). The "Did you know?" band moved into the knowledge
  section as the legal facts column, and swapped sides with Common
  questions, which now sits on the left. The map placeholder and the
  contact modal's picture are blurred like every photo.
- **Option B:** "Your legal matter:" reads "How can we help?"; the same
  map and modal blurs.
- **The walkthrough of The Direct Answer:** "Services, one hover away" now
  opens the mega menu with a demo click and highlights the whole open
  menu; the photo-strip point reads as the articles slider; a new point
  highlights Common questions; the facts point follows its column; the
  order follows the page. Hand-placed highlights were rescaled to the
  page's new height.
- The comments themselves are removed, as asked.

## 0.56.0 — 2026-09-07
**Two parts, not three: the project page first, then reviewing a design.**

rian: make the introduction two parts instead of three, 1 then 3 then 2,
so they review the concepts page first and the review screen second. And
on the timeline point, demo a click on the scope stage to show it flipping
over to reveal the scope, then back to the concepts.

- Part 1, the project page: welcome, where we are, the concepts, general
  comments, your preference, when you are done, restart any time. Its last
  button hands into the viewer.
- Part 2, reviewing a design, unchanged in content; its last button is now
  "Start the Design Concept Walkthrough" and starts it right there, concept
  1 then 2. The board no longer has a closing part; `?tour=3` is gone.
- The timeline point lights the whole journey (rail and stage) and a
  pointer does the clicking: it moves to the first stage, presses it, the
  scope flips in; moves to "Back to now", presses it, the concepts flip
  back. Driven through a window event the stage rail listens to; leaving
  the point puts the rail back.

## 0.55.1 — 2026-09-07
**Quieter: no filler under a card point, two compact buttons.**

rian: remove the "points of this part" line under the current point while
a card is up, and make Start and Feedback complete compact, without the
wordy explanations. Both done. The panel shows just the outline while the
words are in the card; the two acts are their buttons and the one link.

## 0.55.0 — 2026-09-07
**The tool introduction in three parts, then the designs.**

rian: the whole tool walkthrough first, then the design walkthroughs. Three
parts: the intro on the Initial Concepts page, over to the review page to
show how a design is reviewed, back to the concepts page for general
comments and the preference. The first welcome as a centred card, not the
sidebar, with the sidebar lit up to say "this is where you walk through".
Highlight the bell. Demonstrate leaving a comment by clicking an area. At
Discussion, pop back out to a card with an example discussion in the panel.
After the tour, the button reads "Start the Design Concept Walkthrough".
His revised commentary sets the tone; revised here for grammar only.

- **Three parts, one order.** Part 1 on the board (welcome, where we are,
  the concepts, restart any time), part 2 in the viewer (where you are, the
  walkthrough, leaving a comment, discussion, the bell, the blurred photos),
  part 3 on the board (general comments, your preference, when you are
  done). The panel says which part and which point. The design walkthrough
  no longer sits inside the introduction; part 3's last button starts it,
  concept 1 then 2, and its end is the plain board.
- **Two points are said in a centred card** (`TourModal`), with the panel
  ringed above the dim: the welcome, where the panel shows the road ahead,
  and Discussion, where the panel shows an example discussion (a page
  comment and a spot comment with our replies under them, a walkthrough
  point comment), built from the pack's own ThreadCard so the example is
  the real thing. Demo only, nothing is saved.
- **A comment being left**, as an example, on the design: a pointer drags a
  box over an area, a marker lands, and the comment appears in the panel.
  CSS keyframes, plays once on arrival at the point.
- **The bell** is a point of its own; the ring goes above the global row
  (`layer: "top"`) since the row sits above everything else.
- **The board's review stage** puts the two acts beside the words, above the
  fold: "Start the Design Concept Walkthrough", with "Start the Full
  Walkthrough (including tool introduction)" under it, and Feedback complete.
  The concept cards' Comment and Prefer links carry tour targets.
- Shipped words are rian's revised ones. TOOL_TOUR_VERSION 3, so everyone is
  offered the new introduction once.

## 0.54.0 — 2026-09-07
**The introduction in the team's own words, from the welcome.**

rian: as admins we used to be able to edit the walkthrough commentary and
reposition the highlights, and I am trying to go through the whole thing
including the initial welcome, as an admin, making changes as I go.

Two things were in the way. The design walkthrough's Edit and Adjust box were
still there for managers, but the tool introduction (the welcome on the board,
the viewer's five points, the closing pair) was shipped code: nobody could
change a word of it. And a manager never met the welcome at all. It ran only
for a first-time client, and the team's ways in started past it, so "the whole
walkthrough" was a route no admin could take.

- Every introduction point now carries **Edit** for managers, in place, while
  walking it: title and body, `{name}` still standing for the reader's first
  name. The words are per project (`project_details.tour_overrides`,
  `PATCH /api/projects/{id}/tour/{key}`) and lie over the shipped ones, so a
  project that says nothing keeps the shipped introduction and a new project
  starts from it. A point the team has rewritten says so on its Edit button,
  folds the shipped words underneath, and offers "Use the shipped words".
- The board's stage now has **"Start from the welcome"** under Start the
  walkthrough, for everyone: the welcome, the stages, the concepts, then into
  the viewer's introduction, the design walkthrough, and the closing points.
  A first-time client sees it as the alternative to the plain start.

Highlights on the introduction are fixed to the chrome they point at, so there
is nothing to reposition there; the design walkthrough's Adjust box is the
repositioning tool and is unchanged.

## 0.53.0 — 2026-09-03
**The demo click waits while the point is read.**

Adi: give the virtual click a delay, two seconds by default, adjustable per
point. Right, and it was worse than a missing setting: the click fired 260ms
after the beat opened, so the design changed while the reader was still on the
first line. They watched the thing happen without knowing what they were
supposed to be watching for.

`walkthrough_steps.click_delay_seconds`, default 2, editable in the point
editor beside the hold: **Wait [2] seconds, then click**. Per point because
the right wait is the length of the sentence: a short line needs a moment, a
longer one needs several. Existing points take the default.

The two numbers now read as the shape of the beat: wait while it is read,
click, hold it open, hand it back.

## 0.52.0 — 2026-09-03
**Say where a point goes, instead of nudging it there.**

Adi: Earlier and Later are hard to edit with, because moving a point one place
at a time is not how you say "this belongs first". He is right. An idea that
arrives late lands at the bottom, and walking it up the list a press at a time
is bookkeeping, not editing.

The editor now shows `Point [3] of 6` with the number as a field. Type the
position, press Enter or Move, and the point lands there. It is a draft like
the rest of the editor, so typing does not move anything under the reader
until you commit it. The whole order still goes to the server in one request.

Moving now keeps the editor open on the point you moved, since you moved it in
order to see it in its new place, not to be dropped back into the list.

## 0.51.3 — 2026-09-03
**Escape closes the smallest thing that is open.**

Adi: pressing Edit on a point dropped the panel back to the Discussion list.
Same root cause as the last two fixes, reached by a different route. The only
thing that ever sends an Escape to the design is the demo click's cleanup, and
until 0.51.2 the frame forwarded that synthetic Escape back to the app as a
real key press, which closed the panel, which ended the walkthrough. The
cleanup was keyed on the whole `steps` array, and React Query refetches on
window focus, so coming back to the tab and clicking anything could fire it.

Both halves were fixed in 0.51.2. This adds the guard that should have been
there anyway: editing a point is INSIDE the walkthrough, so Escape now leaves
the editor rather than throwing away the walkthrough the editor belongs to.

## 0.51.2 — 2026-09-03
**Fix: the beat's own dismissal was ending the walkthrough.**

Adi, after the last fix: the demo click still breaks it. It did, for a
different reason, and this one was never about scrolling.

The frame forwards Escape to the app so a real key press can close the panel
or end a tour (v0.36.3). `sendEscape()` hands the design back after a demo
click by dispatching a synthetic Escape on that same document, and the
forwarder could not tell the two apart, so it told the app the reader had
pressed Escape. The walkthrough ended about three seconds into the beat that
opened the menu, which is the hold expiring. Only a trusted event is forwarded
now: `isTrusted` is exactly the difference between a person and a script.

Second cause in the same path: the effect that fires the demo click depended
on the whole `steps` array, and a refetch hands back a new array every time,
so its cleanup posted Escape and re-clicked on every poll. The reveal was
closing and reopening under the reader mid sentence. Keyed on the beat now.

## 0.51.1 — 2026-09-03
**Fix: a link that goes nowhere threw the design back to the top.**

Adi: the demo click and the Escape broke the walkthrough. Mine, from v0.50.0.

Every link in these mockups is `href="#"`, since nothing is built behind them
yet, and the navigation guard read a bare hash as a legitimate same-document
anchor and let the click through. A real click on one scrolls the document to
the TOP. So the demo click opened the menu and threw the reader back to the
top of the page mid walkthrough, and any client clicking a service link did
the same to themselves.

A bare `#` names no destination, so it is neutralised. A real fragment such as
`#practice-areas` still navigates, because that is navigation inside the
design and it should work. And a demo click never navigates whatever its href
says: it exists to reveal something while the reader is held at a particular
spot.

## 0.51.0 — 2026-09-03
**Fix: the Team menu opened off the edge of the window.**

Adi: the Team dropdown, which sits at the right of the app bar, opened
rightwards and was half hidden.

`caddie-ui`'s `Menu` positioned its popup at `left: 0`, always. That is right
for the page navigator on the left of the bar and wrong for anything on the
right, and the pack offered no way to say so.

Fixed in the pack rather than around it, since easel holds the canonical copy
until caddie's tooling stamps it. The popup now measures itself against its
trigger when it opens, flips to right-aligned when it would run off the
window, and re-checks on resize. `align` is there for a caller that must
decide, but nobody has to: asking every caller to know where its trigger sits
gets it wrong the moment the window narrows. README row updated and the rule
written into the standards proposal, since it belongs to every caddie app and
not just this one.

## 0.50.1 — 2026-09-03
**Fix: marking an area threw the selection down to the footer.**

Adi, trying to highlight the main nav: the box shot to the bottom of the page,
and pulling it back up dropped it there again.

The new-step composer wore `.tour-card`. When the floating tour card was
deleted in v0.33.0 its CSS went with it, and nothing was left matching that
class, so the composer lost `position: fixed` and rendered in the document
flow, at the foot of a design thousands of pixels tall. It mounts the moment a
drag passes one percent wide, and its `autoFocus` asked the browser to bring
it into view, which scrolled the page to the footer mid drag. The stage moved
under the pointer, so the box followed, and the next drag event did it again.

Fixed at both ends: the composer has its own class and styles, so nothing it
depends on can be deleted along with something else, and it now focuses with
`preventScroll` rather than `autoFocus`. The area drag also shares the note
drag's clamped coordinate helper instead of keeping a second copy of the same
sum.

## 0.50.0 — 2026-09-03
**A walkthrough point can be moved, and a demo click can open a CSS menu.**

Adi wanted a point demonstrating the mega menu opening, near the top, since
that is where the nav is on the page. Two things were in the way.

**Points were append-only.** A new one took `max(step_order) + 1` and nothing
in the interface ever sent an order, so a beat belonging second read as the
last thing said, after the closing remarks. `POST /api/options/{id}/
walkthrough/reorder` takes the whole ordered list and renumbers it in one
transaction, and the point editor grows Earlier and Later buttons. The whole
order goes at once on purpose: nudging one point at a time leaves the
walkthrough briefly scrambled, and a client reading it then sees the wrong
story. Points left out of the list keep their place at the end rather than
being dropped, and an order naming a deleted point is refused whole.

**A demo click could not open the menu it was pointing at.** The nav in these
mockups is pure CSS, opened by `:hover, :focus-within`, with no script behind
it, and `element.click()` dispatches a click WITHOUT moving focus. So the
click landed and nothing happened. The bridge now focuses before clicking,
and blurs when handing the design back, since a `:focus-within` menu closes
only when focus leaves it, which neither Escape nor a background click does.

## 0.49.1 — 2026-09-03
The second pass over the sweep, after reading the built bundle rather than
trusting the first pass. Two client-facing lines had been missed: the note
shown when someone clicks a form inside a mockup, and the example blurb a
manager is offered when writing to the client. The team-facing strings easel
owns went too, including the dev-mode bar and my own variant hints.

Left alone deliberately: a bare "—" standing for "no value" in the project
list, which is typography rather than prose, and everything under
`bw-admin/`, which is vendored kit and is fixed upstream, never here.

## 0.49.0 — 2026-09-03
**The client-facing writing, swept of em dashes and loosened up.**

Adi, on rian's rule: our onboarding text reads like a machine wrote it. It
did. The dash was doing the work a full stop or a colon should do, and it was
in almost every paragraph the client meets.

Rewritten rather than search-and-replaced, because swapping one mark for
another leaves the same over-built sentence: "we are reviewing layout,
wording and structure, not pictures. Now, the design itself" becomes "we are
looking at layout, wording and structure rather than pictures. Now to the
design itself."

Covered: the tool introduction (all fifteen points, board, viewer and
closing), the stage view, the choose and unpick dialogs, the walkthrough
panel, the roadmap prose every new project is seeded with, and the League
seed copy.

**The three emails a client can receive were the worst of it**, since a
subject line is the one sentence they see before deciding to open anything.
"League & Williams — concepts ready for you" is now "Concepts are ready for
you: League & Williams". Their tests asserted the old wording and failed,
which is the tests doing their job.

Code comments are untouched. Rian has an em-dash sweep on his own list and
that is his prose to edit.

## 0.48.0 — 2026-09-03
**The look switch moves into the app bar, in the centre.**

Adi, having flipped the first working toggle: it belongs in the bar, in the
gap between the page navigator on the left and Walkthrough, Discussion and the
team menu on the right. He is right, and the pack agrees: the centre slot is
where "what am I looking at" goes, and it has been empty since the mode toggle
was removed. A design is tall enough without a whole band of the window spent
on a control with two buttons in it.

The choices are now a segmented control rather than loose pills, the same
shape the discussion filter uses, so "these are alternatives, pick one" is
read from the form instead of inferred. The row label "Try it with:" is gone,
since the group's own name (Brand) says it in place. On phones the pack wraps
the centre slot to its own full-width line.

## 0.47.2 — 2026-09-02
**Say what is actually wrong: the design has no second look yet.**

Adi added "Brand: Base", then "Brand: Hive", and was told the axis already had
a choice for the design as it ships. True, and useless. Both choices were
blank because the design contains no alternative look to point at, so the
one-press setup never appeared and the manual form was the only thing left,
and the manual form let him build a toggle with one side.

- The refusal now names the existing choice and says what the second one needs:
  the class that switches to the other look, with an example, and what to do
  if the design has none yet. The backend's recovery line reaches the panel
  now instead of being dropped on the floor.
- An option whose design declares nothing leads with that, and points at
  Replace design, rather than presenting a form that cannot succeed.
- The viewer hides any group with fewer than two choices. Half-built toggles
  exist while the team is authoring, and the client must never meet a control
  that cannot be moved.

## 0.47.1 — 2026-09-02
**Fix: removing an option's last variation blanked the board.**

Four hooks in `VariantAuthor` sat after an early `return null`, so the same
component called ten hooks on one render and six on the next. React refuses
that and unmounts the tree, which is a white page, not an error anyone can
read. Removing the last variation flipped the component into the short path
and took the board down with it; pressing Variations flipped it back and did
it again. All hooks now run before any return.

Nothing in this project would have caught it: there is no linter, so
`react-hooks/rules-of-hooks` never ran. `tsc` and a clean build both pass on
this bug.

## 0.47.0 — 2026-09-02
**Setting up a toggle is naming the looks. And an option you can already see
can be uploaded to again.**

Adi spent several minutes failing to add a variation, and could not upload the
folder at all. Both were real.

- **The upload control only rendered while an option had no files.** So a
  design already in front of the client could never be replaced from the
  board, which is most of the work: a revision, a fixed hero, a build that now
  carries a second look. It is always there for the team now, and reads
  "Replace design" once there is one.
- **Adding a variation asked for the table's shape, not the job's.** A group,
  a name, an exact CSS class, a default, and the unwritten rule that the plain
  side needs its own choice with an EMPTY class or there is nothing to switch
  back to. Nobody should need to know that. The design already declares its
  looks, so the panel now lists them and asks the one thing the file cannot
  say: what to call each one. One press builds the axis, the base choice, the
  keys, the order and the default, in one transaction so a half-made toggle
  cannot exist. `POST /api/options/{id}/variants/setup`.
- The axis is read from the class where it follows the convention the bridge
  expects: `v-brand-hive` becomes Brand, with looks named Plain and Hive that
  you can rename before pressing.
- The hand-built form stays for the cases the shortcut does not cover: a
  second axis, a class the design does not declare yet.

## 0.46.1 — 2026-09-02
**The way into variations is a button on the option row, beside Rename and
Remove.**

Adi went looking for it on the board and in the viewer and found neither, so
the placement was wrong twice over. A display line under a row is not a
control surface; the actions are. "Variations" now sits with the other things
you can do to an option and carries its count, so how many looks an option has
is answered without opening anything. The line under the row is display only
now, and renders nothing at all when there is nothing to show.

## 0.46.0 — 2026-09-02
**The board notices a look the design already carries.**

rian looked for variations, found nothing on screen, and concluded the
feature had never been built. It had. Nothing used it, and the only way in
was a small "+ Variation" button inside the folded Team tools, on an option
row, which is not a way in that anyone finds.

A bundle that scopes a rule to `<html>` is the designer saying this page can
wear another look. The board reads that now, per option, and when an option
has a design but no variations it says which class is sitting there unwired
and offers to set the toggle up. It stays silent when the design declares
nothing, so a board of plain options gains no noise.

## 0.45.2 — 2026-09-02
- The introduction's highlight box no longer animates between points: it
  jumps. A transition can lag behind the target (and a background tab
  freezes it entirely), and a box that is not where the words say is worse
  than one that moves without ceremony.

## 0.45.1 — 2026-09-02
- The introduction's highlight re-measures its target when the layout moves
  (the panel pushing the page, the stage settling its width), not only on
  scroll and resize — the design's box sat stale otherwise.

## 0.45.0 — 2026-09-02
**The introduction starts on the board.** A first visit (or `?tour=1`)
opens it there: Welcome · Where we are (the stage line — done, now, next;
initial concepts for the homepage only; this round is about picking a
direction and gathering feedback for the final homepage drafts) · The
concepts · Next: the walkthrough. Next hands into the viewer's own points
(where you are, the walkthrough, Discussion, notes, the blurred photos) and
then the design walkthrough — Option 1, then Option 2 — and the last point
brings the reader back to the board for the close: Your preference, and
When you are done (Feedback complete). "Replay the tool introduction" in
Team tools starts it on the board.

## 0.44.0 — 2026-09-02
- **The discussion list stays a list.** A conversation opens INLINE under
  its card — the replies, then a composer — when you press Reply (the count
  sits on the button); the card itself never opens a thread. A note's card
  still takes you to it on the design; the page and version cards do
  nothing but Reply. Fixes "replies don't post": they did, but the old thread
  view did not show them.
- **Walkthrough points in the discussion are the point itself** — its own
  words as the root, the comments on it as replies — and no longer open the
  walkthrough when clicked (a quiet "Open in the walkthrough" link remains).
- **Images open in a lightbox**, not a new tab.
- caddie-ui: `ThreadCard` takes `expanded` + children; `ThreadReplies`;
  `Lightbox` / `openLightbox` (mount one at the app root).

## 0.43.0 — 2026-09-02
**The tool introduction, redone in the walkthrough's shape.** "How to use
this" is now **Tool Introduction**: eight points in the side panel — the same
rail of points as a design walkthrough — with the part of the tool each is
about lit up on the page (dark, blurred surround). A point with no target
(the welcome, the close) dims the whole page so the panel is the focus. It
speaks to the reader: "Welcome, Darren". Points whose control this person
does not have (the preference mark for the team) skip themselves. The old
coach-mark bubbles are gone. On a first visit "Start the walkthrough" still
runs it first, then the design walkthrough follows.

## 0.42.0 — 2026-09-02
- **Re-file a conversation.** Inside an open conversation, "Move to…" moves
  it to the page, to this version, or to a walkthrough point; the comments
  keep their authors and times and join the target's conversation; a note on
  the design loses its marker. The author or the team.
  `POST /api/threads/{id}/move`.
- **Labels say where a conversation is:** Page › Homepage · Homepage ›
  Option 1 · Option 1 › Point 4 (a note on the design) · Walkthrough ›
  Point 2. "Version" is gone; the walkthrough's "Step" is now "Point".

## 0.41.1 — 2026-09-02
- The spotlight adjuster (a beat's box, an area note) commits the box as it
  is at release, not as it was at the last render — a fast drag could commit
  a stale box.

## 0.41.0 — 2026-09-02
- **Markers move after posting.** Drag a marker to move a note (the author,
  or the team); an open area note gets the walkthrough's adjuster — drag
  inside to move, a corner to resize. `POST /api/pins/{id}/move` takes the
  size too.
- **Clicking a note in the list takes you to it** on the design (highlight +
  scroll) without opening the thread; **Reply** opens the thread. A
  walkthrough point's card opens the walkthrough at that point; the page and
  version cards open their thread. "View on canvas" is gone from the list
  (still there inside an open thread).
- **Stronger highlights everywhere.** The surround of a walkthrough beat, an
  open area note, a draft, and the tool tour's coach marks is now a dark,
  BLURRED overlay with the box cut out of it, so the highlighted area stands
  out — a box-shadow could darken but never blur.

## 0.40.0 — 2026-09-02
- **Notes can mark an area.** Press and drag on the design to mark "this
  whole block"; the note carries the area (`pins.w_percent/h_percent`) and,
  while it is open, the area is highlighted like a walkthrough beat (the
  rest dimmed). Closed area notes keep a dashed outline.
- **The marker appears the moment you press** — a pulsing draft marker (or
  the dashed box while dragging) — and nothing is saved until the note is
  posted.
- **Opening a note from the Discussion list takes you to it** on the design
  (scrolls to the marker, highlights it).

## 0.39.2 — 2026-09-02
- The comment-mode hint no longer mentions "Play" (there is no Play button):
  "Click anywhere on the design to leave a note · close Discussion to hide them".

## 0.39.1 — 2026-09-02
- Fix: the viewer crashed on open (0.39.0) — the discussion panel looked up a
  walkthrough point with no subject selected.

## 0.39.0 — 2026-09-02
**The viewer's bar, cleaned up.** Navigator · a quiet preference mark
(☆ Prefer this one / ★ Preferred) beside it · ▶ Walkthrough · Discussion. No
Comment/Play toggle, no Copy link, no Contents button; the team's tools
(author walkthrough, import notes, copy link) live in a "Team" menu.
- **One Discussion mode.** The panel open IS comment mode — notes shown, a
  click on the design drops one; closed is play. The panel lists everything
  there is to talk about on the design as items: the page as a whole, this
  version as a whole, each walkthrough point that has been talked about,
  and the notes on the design — with starters for the page and the version
  when nobody has spoken yet — and drills into any of them for the threaded
  conversation. Older links (`?mode=comment`, `?drawer=1`, `?pin=`) open it.
- `GET /api/options/{id}/discussion`: pins, page, version and points in one
  read; every conversation write refreshes it.

## 0.38.1 — 2026-09-02
- The welcome popup is gone: a first visit's **Start the walkthrough** runs
  the tool tour (how this page works) first, then the design walkthrough —
  the viewer holds the walkthrough until the tool tour ends.
- The board speaks to the person: "Hi Darren — 2 concepts are ready for
  you." on the review stage, and "Thanks, Darren — we've been notified" after
  Feedback complete.
- League: the scope stage rewritten as concise client-facing copy (what we're
  designing; the direction you gave us).

## 0.38.1 — 2026-09-02
**The viewer's Settings tab is gone; authoring lives on the board alone.**

The tail of moving variations to the option row. Two copies of one form is
drift, and the copy in the client's discussion panel was the one in the wrong
place. Removed with its now-orphaned styles and the imports it took with it.

Also: the variant strip no longer renders for a manager when the option has
no variations. It held the "+ Add a variant" button; without it, it was an
empty grey bar across the top of the design saying nothing — which is what
sent Adi looking for a feature that was there.

## 0.38.0 — 2026-09-02
**The board is the project's journey.** Title, blurb, then a stage rail —
Scope and Initial Direction · Initial Concepts · Concept Revision and
Finalization · Inner Page Mockups · Signoff · Development — with done / here /
next obvious, and the stage being read below it (‹ previous · Back to now ·
next ›). Done and planned stages are prose; the ACTIVE stage is the review:
the concepts as cards (picture, tag, a line on what each represents), one
big **Start the walkthrough** (every concept, one after the other; the last
beat returns here), quiet "Look at it" / "Prefer this one" affordances,
general comments per concept, and a big **Feedback complete** that marks the
round as ours and tells the team (bell + mail). Team tools fold under it.
- Stages mirror caddie's `stages` shape and live in easel until the hub owns
  them (`project_stages`; seeded from the roadmap template on first read;
  managers edit text and status from the board).
- `POST /api/projects/{id}/feedback-complete`; the rollup reads the marker as
  `waiting_agency` (unless everything is decided); re-presenting clears it.
- Concept threads (`option`) on the Interaction Standard seam; blurbs and
  uploaded thumbnails per concept (`/api/options/{id}/thumbnail`).
- caddie-ui: `Stages` (the rail) and `Prose` (block-level markdown-lite on
  top of `Body`). `InlineThread` shares the inline conversation between
  walkthrough points and concept cards.
- Plan: `.logs/planning/board-redesign-stages.md`.

## 0.37.2 — 2026-09-02
**Variations are authored on the board, as children of the option they belong
to** (Adi: "a screen can have several Options, then an option can have several
Variations" — and a settings tab inside the *discussion* panel is not that).

Authoring lived in the viewer's side panel under a Settings tab, which put a
team authoring tool inside the client's conversation and one tab deep in a
panel nobody had reason to open — Adi looked at the option page and reasonably
concluded the feature was missing. Now the board's option row carries its
variations on a child line, which is the shape of the structure being edited.
The toggle stays in the viewer: that is where you *look* at the difference.

- `components/VariantAuthor.tsx` — standalone, so the board's own redesign can
  move it without rewriting it.
- Each choice shows its group, its name, whether it is shown first, and its
  class on hover (or "the design as it ships" for the base look).
- Nothing is labelled until there is something to label: an option with no
  variations shows a quiet "+ Variation" and no empty row.

## 0.37.1 — 2026-09-02
**A variation choice may name no class — that IS the design as it ships.**

Found while writing the upload steps for a bundle whose plain side is written
`html:not(.v-brand-hive) { … }`: it has no class at all, so authoring its
"Plain" choice meant inventing a do-nothing class to satisfy a required field,
which puts a lie in the data. Worse, without that choice the single
alternative is permanently on — the seed picks the first choice on every
axis — so the base look would have been unreachable.

- An empty class is now accepted and means the base look; one per axis, since
  two would render the same page under different names.
- The bridge guarded: `classList.remove("")` throws a SyntaxError, which would
  have taken out the whole variant handler on the first toggle.
- The form says so where the axis is being built, because the base choice is
  the one that gets forgotten.

## 0.37.0 — 2026-09-02
**Variations, made usable: pick the class, name the default, keep the look in
the link — and the two pieces the caddie pass dropped, put back.**

The backend has recorded a note's variation since 0.31.0. The UI that showed
it went missing when the pins and panel were rewritten onto the pack, so a
note still remembered its variation and nobody could see it. Restored, with
the shared answer to "is this note about the design I am looking at?" now in
`lib/variants.ts` — the stage and the panel must answer it the same way.

- **A note says which look it was about.** Quiet when it is the one on screen
  (provenance, not news); amber when it is another, because opening it moves
  the design under the reader. Notes from another variation are dimmed and
  dashed on the stage rather than hidden — a client who flips a toggle and
  finds their own note gone concludes the tool lost it.
- **Opening one takes you there**, from the pin or from the panel row. A point
  on the hexagon header can mean nothing against the plain one.
- **The class is picked, not typed.** A variant class is one the design scopes
  to `<html>` — that IS the mechanism, so the bundle already declares which
  looks it can wear and `declared_root_classes()` reads them back. Typing it
  by hand fails *silently* when wrong: the frame simply does not change, which
  is the worst way to find out. A typed class the design does not contain is
  called out before saving — never refused, because the design may not be
  uploaded yet and the designer knows their own file.
- **"Shown first" is a fact about the axis**, not about one choice: setting it
  clears the others, or the opening view would depend on row order.
- **A link carries the variation** (`?v=brand:hive`). Copy Link promises "what
  you are looking at", and a toggle changes what is on screen — without this
  the recipient opened the other design.

## 0.36.5 — 2026-09-02
- The shell's re-measure coalesces on a timer, not requestAnimationFrame,
  which a background tab pauses — a tab opened in the background showed the
  stale offset until it was looked at.

## 0.36.4 — 2026-09-02
- Fix: the global row's measured bottom (`--cu-shell-bottom`) missed the
  moment the dev-mode strip mounted above it and only caught up on the next
  scroll. The shell now re-measures on any DOM mutation under body, any
  scroll in any scroller, and resize — one measurement per frame.

## 0.36.3 — 2026-09-02
- Fix: with the dev-mode strip on, the side panel's head — its × and the
  "‹ Exit walkthrough" link — sat hidden under the global bar, so the team
  had no visible way out of a walkthrough (a client, with no strip, had).
  The panel now pins to the global row's measured bottom
  (`--cu-shell-bottom`, published by `GlobalBar`), whatever sits above it.
- "Exit walkthrough" is also a button in the walkthrough footer, beside
  Back/Next.
- Escape pressed while the design has focus now closes the panel or tour:
  the mockup bridge forwards that one key and the app replays it.

## 0.36.2 — 2026-09-02
- A pasted or dropped file no longer writes its markdown into the text box.
  It waits in the strip under the box (thumbnail, or a PDF tile) and is
  appended after the words on Send; a comment may be a file alone. Same wire
  format, nothing to read while writing. (Pack change: `Composer` keeps its
  own pending list; `imagePrefix` is no longer a Composer prop.)

## 0.36.1 — 2026-09-02
- Fix: a refusal from the auth middleware (401 signed-out, 403 read-only
  View As) reached the person as "Upload failed (403)" / "Request failed
  (403)". The middleware answers with a top-level `{error_code, summary,
  details}` while routes wrap theirs in `detail`; the client read only the
  latter. Both shapes are read now, so the composer says "You are viewing as
  someone else, read-only. Return to yourself to make changes, or start View
  As in act mode." — which is what happened.

## 0.36.0 — 2026-09-02
- Paste a screenshot into any note. The Interaction Standard's attachments
  addendum (2026-08-22, punchlist's pattern), now in easel and in the pack:
  paste, drop or paperclip in the composer uploads at once, the draft gets
  `![name](/api/projects/{id}/attachments/{file})` markdown (a pending strip
  shows thumbnails, each removable), and Send posts a normal comment. Bodies
  render through the pack's new `Body` (markdown-lite: attachment images,
  links, bold, code) — images load ONLY from the app's own attachment URLs.
- Backend: `POST /api/projects/{id}/attachments` (201, `AttachmentOut`) and
  `GET /api/projects/{id}/attachments/{name}`; files live DB-free under
  `data/uploads/{project}/` with the hostile-surface rules (png/jpg/gif/webp/
  pdf by extension AND magic bytes, our uuid filenames as the traversal
  guard, 10 MB cap, per-project cap, nosniff, content-type from our table,
  visibility re-checked through the project on every read, 404 never 403).
  The unused per-comment attachment stub (`/api/comments/{id}/attachments`,
  `/api/attachments/{id}`) is retired; the `attachment` table stays for now.
- caddie-ui: `Composer` gains `onAttach`/`imagePrefix`; `ThreadCard`,
  `ThreadView` take `imagePrefix`; `Body`/`renderBody` added.

## 0.35.0 — 2026-09-02
- The walkthrough panel shows the WHOLE tour: one collapsible section per
  design that has a walkthrough, plus "How to use this" (the tool tour), with
  the section you are in expanded and live. Other sections open to their
  points; a point in another design jumps there (tour open, at that point).
- Comments on walkthrough points. Each beat carries its own conversation
  (one thread per point, like a screen's), read and written inside the tour.
  "Ask a question" — which paused the tour and dropped a pin near the spot —
  is gone; the composer is on the point itself. Step comments count in the
  status rollup like screen comments, badge the point in the timeline and
  the outline, and notify with a link straight back into the tour
  (`?mode=play&step=N`).
- Timeline points, not numbers: the rail's nodes are dots (filled = behind,
  ringed = here, hollow = ahead). `numbered` remains for ordinals that mean
  something.
- caddie-ui pack: `PanelSection` (Gutenberg's PanelBody — a collapsible
  group in the side panel) and `Timeline` badges as a list. Contents
  entries now carry their `beats`; walkthrough steps carry
  `comment_count`/`thread_open`; new `GET /api/steps/{id}/thread` and
  `POST /api/steps/{id}/comments`.
- Fix: arriving on another design with `?step=N` (the footer's "Next: …",
  the outline's jumps) opens the tour at that point without a refresh — the
  viewer stays mounted across designs and only read the link once.

## 0.34.1 — 2026-09-02
- Fix: the navigator's popup opened into an invisible box — the app bar's
  left slot clipped it with overflow:hidden. The slot no longer clips; the
  trigger truncates its own text.

## 0.34.0 — 2026-09-02
- Fix: switching directions did not switch. The frame kept the URL it was
  born with (so a token refresh would not reload it mid-review), but that
  pinned it across OPTIONS too — the previous mockup stayed on screen until a
  full browser refresh. The birth URL is now per option.
- The page → direction navigator: one dropdown that reads "Homepage · The
  Direct Answer · 1 of 2" and opens to every page as a group with its
  directions beneath — concept tag, open notes, chosen star, the current one
  checked — plus "Back to the board". Replaces the back-crumb and the two
  chevrons, which read as a label; a client could not tell there was a second
  concept. Scales to many pages and many directions.
- caddie-ui gains `Menu` / `CountChip` (a grouped dropdown switcher).

## 0.33.1 — 2026-09-02
- BW-auth kit refreshed to the current standard (scaffolder re-run, byte-for-
  byte): the View As banner is now a compact draggable pill — amber read-only,
  red when acting, never dismissable, position remembered per browser —
  instead of a page-wide bottom bar. No behaviour change beyond the badge.

## 0.33.0 — 2026-09-02
- The walkthrough lives in the side panel now, as a TIMELINE: every beat on a
  vertical rail — done, current (its words and approval expanded in place),
  ahead — and any node jumps. Navigation pinned in the footer; the spotlight
  stays on the design beside it. The floating tour card is gone. "Ask a
  question" pauses the tour and resumes at the same beat once the note is
  posted or abandoned.
- Shell: the side panel now sits BELOW the global row (it was covering the
  bell and account menu), and the app area — app bar, variant strip, the
  stage — reflows beside an open panel instead of being covered, so a
  spotlight or pin on the right of a design is never hidden under it.
- caddie-ui gains `Timeline` and the `cu-panel-push` rule.

## 0.32.2 — 2026-09-02
- App row carries controls only (the option switcher, modes, actions); the
  project/screen path lives in the global row's locator, not twice.
- Thread-card actions ("Reply", "View on canvas") get real hit targets — a
  12px word was a miss on a phone and a near-miss with a mouse.

## 0.32.0 — 2026-09-02
- The caddie-alignment pass (rian). New vendored `caddie-ui` pack — the shared
  chrome every caddie app should carry: a two-row shell (a GLOBAL row: brand,
  where-you-are locator, bell, account menu; an APP row for the screen's own
  functions), the one side panel (header · tabs · scrolling body · pinned
  footer), the one bell in review's format, and review-modelled thread
  components. easel now mounts them: the global row is back above the viewer,
  discussion lives in the side panel (pins on the design are anchors; floating
  balloons are gone), Contents and Settings are tabs of the same panel.
- Proposal for the caddie session in `.logs/planning/caddie-ui-standard-proposal.md`.
- agents.md: standing rule — every feature checks against caddie or makes a
  written case for changing caddie; shared chrome comes from the pack.

## 0.31.0 — 2026-09-02
**A note remembers which variation it was left on.**

A pin is a point on a *design*, and a toggle changes the design — "make this
bigger" against the hexagon header is not the same remark as against the plain
one. Until now the two were indistinguishable a week later.

- `pins.variants` records what was showing when the note was left. Null where
  the option carries no variations — not an empty dict pretending to be a
  choice.
- **Every note stays visible, whichever variation you are on.** Hiding the
  others would mean a client flips a toggle, finds their own note gone, and
  concludes the tool lost it. They are dimmed and dashed instead, and **opening
  one switches to the variation it was left on**, so the remark is read against
  the design it was about.
- The thread names it — *Full hive* — in the client's own words. Labels are
  resolved at read time from the option's own variants, so renaming a variation
  does not leave old notes citing a name that no longer exists.

**And the League hexagon variation is one bundle, not two.** `home_v1` and
`home_v2` have **identical markup** and differ by 16 lines of CSS: v1 hides the
header watermark, the mega-menu corner hives and the footer hive, and squares
off the social buttons. So the merged bundle scopes exactly those to
`html:not(.v-brand-hive)` — the default renders precisely `home_v1`, and the
class renders precisely `home_v2`. The bridge swaps the class, so the flip costs
no reload and the client keeps their scroll position.

## 0.30.0 — 2026-09-02
**Draft and published screens.** A screen being worked on is the team's alone;
the client meets it when you publish it, not when you start it.

- A draft is invisible to the client **everywhere** — the board, the contents
  rail, the screen count, the round summary — not merely dimmed.
- **It does not hold the project open.** The rollup answers "where does the
  *client's* decision stand", and a draft is not in front of them to decide on.
  Publishing it reopens the decision; there is a test for both directions.
- **New screens start as drafts.** You cannot accidentally show someone half a
  page. Everything that already existed was visible and stays visible — the
  migration backfills `true`, because a migration must not make things vanish
  from a live board.
- **Draft is the loudest thing on the card.** The mistake it prevents is
  presenting something you thought was hidden — or worse, waiting on a client
  who cannot see the screen at all.
- The test helper now publishes by default, since "a screen" in almost every
  test means one the client can act on. `published=False` builds the draft state
  deliberately, the same way `with_mockup=False` builds the unfinished one.
- The migration's `ALTER COLUMN` is skipped on SQLite, which cannot do it and
  does not need to: SQLAlchemy always sends `published` explicitly, so the
  server default is never what decides.

## 0.29.0 — 2026-09-02
- **Screens and options can be renamed.** Both `PATCH` endpoints have existed
  since the rebuild with nothing calling them — a name you cannot change is a
  name you have to get right first time, and nobody does.
- **An option's tag is edited in the same breath as its name.** The tag is the
  one-glance difference between two options on the board, so editing the name
  without it would leave the pair describing themselves inconsistently.
- This also fixes a smaller trap: **"+ Add option" names the new option
  "Option 4"**, and until now that was permanent.
- Manager-side only, and quiet — the rename control appears on hover rather than
  sitting next to the client-facing name all the time.

## 0.28.1 — 2026-09-02
- **A short-hero build of The Welcome Desk** (`home-welcome-desk-short-hero.html`,
  source in `leaguelaw/prototyping/`). One rule differs from `home_v3`: the hero
  is **560px** instead of `calc(100vh - 92px)`, which computed to ~990px on a
  1080 screen and grew with the display.
- **Not 500px, and the arithmetic is why.** The consultation form is ~452px and
  is centred on `calc(50% - 26px)`; the practice-area strip is pinned to the
  hero's bottom at ~55px. At 500 the form is clipped 2px at the top *and*
  overlaps the strip by 5px. 520 is the bare minimum that clears; **560 is the
  shortest comfortable height — and is what the file already declared as its own
  `min-height`**, so the designer had worked the floor out.
- Confirmed while doing it: `home_v3.html` is **byte-identical** to the bundle
  already on option 2, so nothing needed swapping — only the hero height was
  ever wrong.

## 0.28.0 — 2026-09-02
**Deleting a note leaves nothing behind.**

- **No more "removed" tombstones.** A deleted comment is simply not shown. The
  row survives — a conversation is a record — but *removed / removed / removed*
  down a thread is debris, not information, and the pin thread was the one place
  still printing it. The screen conversations already did this; now both agree.
- **A note can be retracted entirely — marker and all.** Deleting a *comment* is
  soft, because a conversation is a record. Withdrawing your own *note* is a
  different act, and a marker sitting on the design with nothing behind it is
  worse than either outcome. **Remove note** in the thread header takes the pin,
  its thread, its comments, their revisions and any attachments.
- Same authority as removing a comment: **yours always, any if you are the
  team.** Retracting your own remark is not censoring a conversation.
- The board comes clean with it — open counts, status, index and round summary
  all drop back, verified by test.
- Deletion order respects the foreign keys that are now enforced: revisions and
  attachments, then comments, then the pin, then the thread.

## 0.27.2 — 2026-09-02
- Fix: the SPA shell now sends `Cache-Control: no-cache` (revalidate; the ETag
  makes it a cheap 304) and hashed assets `immutable`. The shell previously
  carried no Cache-Control at all, so browsers cached it heuristically and kept
  loading the OLD bundle after a deploy — a fix could be live, /api/meta could
  report the new version, and the UI would still be running old code. Found
  while verifying the padding fix: the deploy was correct and the browser was
  not.

## 0.27.1 — 2026-09-02
- Fix: dead space to the right of the mockup on screens wider than the design.
  The stage filled its container (up to 1600px) while the mockup rendered at
  its own natural width, so the difference hung off the right edge. The stage
  is now sized to the rendered mockup and centred. This also fixes a
  correctness bug the padding was hiding: pin and spotlight coordinates are
  percentages of the mockup document but are positioned against the stage box,
  so while the two widths differed every pin and highlight sat proportionally
  off — worse the wider the screen.

## 0.27.0 — 2026-09-01
**Fix: clicking a link in a concept left the client staring at a blank frame.**

A concept page is full of links to pages that were never part of the bundle —
each league homepage carries **52** of them, to lawyer profiles and inner pages.
Following one navigated the frame to a 404, and there was no way back.

- **The bridge stops the frame navigating away.** Same-document anchors still
  work, because they are part of the design; everything else — inner pages,
  absolute URLs, `mailto:`/`tel:` — is cancelled. Demo forms are stopped too:
  a submit is the same blank frame by another route.
- **`preventDefault` cancels navigation only.** The page's own click handlers
  still run, so a takeover, a reveal, or a walkthrough's demo click behaves
  exactly as designed.
- **A dead click is explained**, not merely dead: *"That link isn't part of this
  preview."* Silence would have the client concluding the preview is broken
  rather than that the link is out of scope.
- **Fixed in the bridge, not in the HTML.** Rewriting 52 hrefs per file would
  fix today's five bundles and nothing uploaded afterwards, and it would have to
  be redone every time a concept is re-exported from its source. This covers
  every bundle, including ones that do not exist yet.

## 0.26.0 — 2026-09-01
**Anyone can fix their own mistake, and nobody can quietly rewrite the record.**

- **Edit your own note**, on a pin thread or a screen conversation. The endpoint
  has existed since the rebuild with nothing calling it.
- **Pin comments gained edit *and* remove.** They had neither — the main feedback
  channel in the app was the one place a typo was permanent.
- **"edited" is a label you can open.** It shows every earlier wording with when
  it was replaced, oldest first. A label alone would say the record had moved
  without letting you check where it moved from, and that is the half that
  matters when two people remember a sentence differently.
- **The history is readable by anyone who can read the comment.** A history only
  the author can see is not a record, it is a diary. The whole point is that an
  edit cannot change what was agreed.
- **A manager may remove a note but never rewrite one.** Removing someone's
  comment is moderation; editing it is putting words in their mouth. The API
  drew that line already and the UI does not soften it.
- Saving without changing a word files no revision — pressing save should not
  manufacture history.
- Revisions survive a comment being removed: soft delete keeps the row, and what
  it said stays readable.

## 0.25.0 — 2026-09-01
- **You can remove a note from a screen's conversation** — your own always, and
  anyone's if you are the team. Useful while testing, which is what it was asked
  for. The endpoint has existed since the rebuild; nothing in the UI ever called
  it.
- **Soft, not hard.** The row survives and the reading does not — a conversation
  is a record, and the Interaction Standard is explicit that one is never
  hard-deleted. Deleted notes simply stop being shown.
- **Fix, exposed by building this: a thread with nothing left to read went on
  turning the board.** Because deletion is soft, a screen conversation whose
  every comment had been removed still had `resolved == false`, so it kept
  counting as open feedback in the rollup, on the index, on the board and in the
  round summary — forever, with nothing to read. **What is open is the reading,
  not the row.** Fixed in all four places, with a test.
- The remove control only renders where you are actually allowed to use it,
  rather than appearing and then refusing.

## 0.24.0 — 2026-09-01
- **A feedback count now says what kind of thing is open.** "1 open" made the
  reader supply the noun, and they could supply the wrong one — open *what*? A
  comment mark before the number answers that before it is read.
- **One mark, in all four places** a count appears: the board's option rows, the
  contents rail, the projects worklist, and the screen's own note toggle. An
  icon used in three places out of four teaches nothing — you stop trusting that
  its absence means anything.
- It replaces the 💬 emoji on the screen-notes toggle. Stroke-based and
  `currentColor`, so the same mark reads white on the red badge and muted in the
  rail without a second asset, and it sits with the bell rather than beside it
  in a different style.
- Tooltips spell it out in words too — *"1 open note"* — for anyone who does not
  read the glyph.

## 0.23.2 — 2026-09-01
- **The blurb editor is a textarea.** It was a single-line input in a 30% rail —
  enough room to see about four words of the thing the client reads first.
  Writing is editing, and you cannot edit what you cannot see. Five rows, and
  resizable.
- **Paragraphs survive** to the client: the blurb renders with line breaks
  intact, so if you write two, they read two.
- The hint says what it is for rather than dictating a length: *the client reads
  this under the title; short enough that a busy person actually reads it.*
- `description` gains a generous 4,000-character cap. The column is `Text` so
  nothing was truncating, but an unbounded field on a paste is an invitation.

## 0.23.1 — 2026-09-01
**One shape system.** The stylesheet had grown **fourteen different corner
radii**, and the primary call to action was square.

- **Shape now encodes kind**, in three tokens:
  - `--r-pill` — things that **describe**: status chips, tags, counts, badges,
    and segmented controls (the group reads as one object)
  - `--r-control` — things you **press or type in**: buttons, links-as-buttons,
    inputs. One shape for every action on the page.
  - `--r-panel` — things that **contain**: cards, dialogs, rails, list shells.
- **Fix: "Choose" had no radius at all.** It lost `border-radius: 99px` when the
  dead pre-tile CSS was removed in 0.12.2, and it never carried the `.btn` class
  that would have given it one — so the most important button on the board was
  the only square thing on it.
- Two controls were wearing pill shape because they were written as badges — the
  screen-notes toggle and the dev-mode switch. They are actions, so they are
  control-shaped now.
- Everything is a token; the only literals left are two circles and the tiny
  drag handles, which are too small for the control radius to read as anything
  but a blob.

## 0.23.0 — 2026-09-01
- **Every comment carries when it was written** — the first note, every reply,
  the screen conversations and the discussion drawer. Small and grey: a review
  is a record, so the time has to be there, but it is never the thing you are
  reading.
- **Relative while recent, a date once old.** "3h ago" is what a person wants to
  know about a note; "47d ago" is arithmetic nobody asked for. The exact moment
  always rides along as a tooltip, because a record has to be readable precisely
  when that matters.
- An edited comment says **edited**.
- One implementation of "when", not two: the index had grown its own copy, and
  it now uses the shared helper.

## 0.22.1 — 2026-09-01
**Fix: posting a note failed on Postgres. Pins have never worked in production.**

`create_pin` inserted the Pin with `thread_id=0` as a placeholder before the
thread existed. `pins.thread_id` is a real foreign key, so Postgres refused the
insert outright — while **SQLite, which ships with foreign keys OFF, accepted it
silently**. Every test passed while the app's central feature was broken for
every real user.

- **Thread first, then pin.** The order now never violates the key. `subject_id`
  is still provisional for a moment because it wants the pin's id and the column
  is UNIQUE, but nothing is written that points at something absent.
- **The test engine turns SQLite's foreign keys ON.** This is the actual fix:
  the same dialect gap hid a second bug earlier (deleting a chosen option left a
  dangling selection), and it will keep hiding them until the tests enforce what
  production enforces. The suite still passes — 188 — so nothing else in the
  codebase relies on the gap.
- **The composer says when it fails.** It failed in silence: the note stayed in
  the box, the button did nothing, and there was no way to tell a server error
  from a mis-click. A composer that swallows an error costs someone the thing
  they just wrote. Both the note composer and the reply box now show the error
  and a pending state.

## 0.22.0 — 2026-09-01
**Dev Mode, and the email layer behind it.**

easel still sends no mail — the gateway's send endpoint is owner/hosting-peer
only with its own recipient allowlist, so easel cannot use it as-is and
connecting it is rian's decision, not a config change. What exists now is the
decision, the record and the switch, so the day that decision comes the work is
already done and already checked.

- **Dev Mode is a banner, not a settings page**, because the failure it prevents
  is *forgetting*. Testing with mail live sends a client a message about a click
  you made to see what a button did, and you find out when they reply to it.
- **On by default.** The safe default for a switch whose off position mails
  clients is the one that does not.
- **An outbox, not fire-and-forget.** Every message easel decides to send is
  written down with the reason it did not go (`dev_mode` · `no_transport` ·
  `no_address`), and the copy is readable in the app. Two things follow: the
  email design is testable **before** a transport exists, and "we turned it off
  and nothing was lost" is checkable rather than hopeful.
- **Exactly two moments earn an email** — *your concepts are ready* and *we
  answered your note*. Everything else stays the bell. A tool that mails on
  every event trains people to ignore it, and the one message that mattered goes
  with the rest. There is a test that pins that boundary: a pin, an approval, a
  selection and a page comment all mail nobody.
- **Only a manager's reply mails, and only to the client side.** Us replying to
  each other is not news to anyone.
- `sent_at` is the Interaction Standard's §7 delivery seam — modelled at the
  rebuild and never written to until now.
- When a transport lands, `outbound._deliver()` gets a body. Nothing above it
  changes.

## 0.21.0 — 2026-09-01
**Where this round stands** — the round in one read.

The answer to a round lived in four places: which option was chosen, which
variants with it, which walkthrough beats were approved, and what is still open
across two kinds of thread. Reading it meant opening every option in turn.

- **Decided** names the screen, the option and the variants locked with it.
- **Still to decide** lists screens that have options and no answer yet.
- **Open feedback** covers **both** kinds — pinned notes and page-level
  conversations — each with who said it, an excerpt, and a click that jumps
  straight to it. A summary that showed one kind and not the other is a summary
  you could not trust.
- **Waiting on ‹name›**, which also fills the tool contract's `actors_waiting`
  — an empty list since the rebuild. caddie's job is telling a client where
  their project stands, and "waiting" without "on whom" is the half that does
  not help.
- **Open to both sides, worded from neither.** The team needs "what do I owe";
  the client needs "did what I said land". It is the same list of facts.
- Deliberately shaped like the contract's status body, so none of this is thrown
  away when caddie's drop-in lands.

## 0.20.0 — 2026-09-01
**"Has the client even opened it?"** — the most common anxiety in this workflow,
and the data was one column away.

- The board and the viewer send a **heartbeat** while the tab is visible, and it
  doubles as the ecosystem presence ping — easel only ever sent that once, at
  sign-in. `bw_client.report_presence` is already throttled and fire-and-forget,
  so beating on it is free.
- **The sidebar tells the team**: *darren — looking now* / *2h ago* / *hasn't
  opened it* (in amber, because that is the one worth acting on).
- **Manager-side only.** It answers a question *about* the client; showing a
  client who else has been looking is nobody's business but ours.
- **`last_active_at` is its own column, not a reuse of `last_seen_at`.** Sharing
  one would mean every heartbeat silently cleared the "new since your last
  visit" flags out from under someone who had not scrolled to them yet — two
  clocks, one deliberate and one constant. Tested both ways, including that a
  client whose *first* action is a heartbeat still gets their new flags later.
- A backgrounded tab stops beating: it is not someone reading.

## 0.19.0 — 2026-09-01
**What changed since you were last here.** The engagement guarantees a second
visit — the homepage is decided, then the remaining pages are designed against
it — so "what is new?" is the first question a returning client has, and nothing
answered it.

- A new screen is flagged **New**. An option is **New** if it did not exist last
  time, or **Updated** if it did but has since gained a mockup or a walkthrough
  beat. Two flags on purpose: *there is another design to look at* and *the one
  you already read has moved on* are different messages.
- The sidebar says it in a sentence: *"3 things changed since you were last
  here."*
- **A first visit marks nothing new.** Arriving to find every single thing
  flagged tells you nothing, so newness only starts counting once there is a
  previous visit to be new since.
- **Reading the board does not clear the flags.** Marking it seen is an explicit
  call the SPA makes once it has rendered — a GET that quietly moved the marker
  would clear the flags in the very request that returned them, so a client who
  glanced at the board would be told nothing had changed. There is a test for
  exactly that.
- Newness is **per person**: what is new to the client is not what is new to us.
- `project_visits` is its own table rather than a column on `tour_progress` —
  that row answers "have they been walked through the tool", this one answers
  "what has changed since they were here", and folding them together would leave
  a table whose name lied about half its contents.

## 0.18.1 — 2026-09-01
**"Ask a question" no longer strands you.** It switched to comment mode and left
the client with a crosshair cursor, a design, and no idea what was being asked
of them.

- It now opens the note composer **already placed on the beat's highlight** —
  they are asking about *that*, so there is nothing to aim at and nothing to
  guess. The composer says **About "Your issue, one click"** and asks *"What
  would you like to ask about it?"*, so the question stays tied to what they
  were reading.
- **A standing reminder in comment mode**: *"Click anywhere on the design to
  leave a note · Play hides the notes."* The tool tour explains this once on
  arrival; a client back a week later has forgotten, and a mode you have to
  remember is a mode people sit in without using. Shown only when nothing is
  already open, so it never talks over the thing it is explaining.
- A beat with no highlight falls back to a sensible spot near where they are
  reading rather than refusing to open.

## 0.18.0 — 2026-09-01
**The walkthrough runs across a screen's options, not once per option.**

Adi's designer notes assume exactly this — the full walkthrough lives on the
entry option and every other option carries only what changed, so *"all six are
one continuous experience, not six links in an email"*. That only works if the
client is carried from one option to the next rather than left to find them.

- The last beat of an option now offers **"Next: The Bold Welcome →"** when the
  next option on that screen has beats, landing on its first beat in Play mode.
- **Back off the front** of an option lands on the **last** beat of the previous
  one, so the reading runs both ways rather than being a one-way door.
- The step counter names the option it belongs to when the walkthrough spans
  several — and stays uncluttered when it does not.
- Neighbours are the options **with beats**, so an option nobody has written a
  walkthrough for is skipped rather than dead-ending the tour.
- It rides the `?step=` addressing from 0.17.1: a hand-off is just a link, which
  is why the whole thing is a few lines rather than a new mechanism.

## 0.17.1 — 2026-09-01
- **A walkthrough beat is addressable**: `?step=3` opens the walkthrough on that
  beat, and the URL keeps itself up to date as you move through it — so "look at
  step 3" is a link, like the pin and feedback-tab parameters before it.
- **One-based**, because that is what the card says ("Step 3 of 5") and what a
  person means when they say step 3.
- Arriving on a step link switches to **Play mode**: the walkthrough plays with
  the notes hidden, and landing in Comment mode would show the beat over a page
  covered in pins.
- The wanted step is held until the beats load — a link is read before its data
  is — and ignored if it is out of range rather than opening on nothing.

## 0.17.0 — 2026-09-01
**A beat can perform a demo click.** Some designs only explain themselves when
something is opened — clicking a practice area reveals that team's takeover —
and describing that in words is a poor substitute for showing it.

- A beat carries `click_selector` and `click_dismiss_seconds`. When the beat
  opens, the bridge clicks that element; after the hold it hands the design back
  (Escape, then a background click, since plenty of reveals close on one and not
  the other). **Default 3 seconds; 0 means leave it open** — sometimes the
  revealed state *is* the point.
- **The spotlight target and the click target are separate fields**, because you
  routinely point at one element and click another.
- **Point at the spot instead of writing a selector.** "Point" arms the frame;
  the next click on the design reports a selector back and fills the field
  rather than doing anything. The card fades out of the way while you aim.
- Moving to another beat asks for the design back early, so the next beat is
  never read through a panel the previous one opened.
- **The import format learned it too**: a line starting `>` is the click, with
  an optional trailing number for the hold — `>.tcell.injuries 5`.
- **No sandbox change.** The bridge already runs inside the frame; this is the
  page's own script dispatching on the page's own element. No new token, no new
  permission — which is the only reason it is allowed to exist.

## 0.16.0 — 2026-09-01
**A walkthrough can be edited.** Until now a beat could be created and deleted
but never changed — the walkthrough is writing, and writing gets revised.

- **Edit, in the card itself** (manager only): title, body, the CSS selector it
  points at, and whether the client is asked to approve it. Delete is there too.
  The `PATCH` endpoint had existed since the rebuild with nothing calling it.
- **Drag the highlight to move or resize it.** *Adjust box* in the card, or
  **press and hold the highlight** for about half a second. Drag inside to move,
  drag a corner to resize; the opposite corner stays pinned and the box cannot
  be inverted.
- **Adjusting by hand clears the selector, deliberately.** A hand-placed box and
  a live-measured element are different intents. Keeping both means the selector
  silently wins on the next load and the drag looks like it was thrown away —
  so the box becomes the target, and the editor is where you put a selector back
  to return to tracking the element.
- Coordinates stay document percentages like everything else on the overlay, so
  a box stays correct at whatever scale the mockup renders — the stage's pixel
  size never enters the stored value.
- The press-and-hold target is manager-only, so it can never eat a client's
  click on the design.

## 0.15.0 — 2026-09-01
Two of Adi's asks: get the written notes in, and make a link carry the view.

- **Import a walkthrough from written notes.** The walkthrough exists as prose
  long before easel does — Adi's designer notes carry a dozen-plus beats as
  title + one sentence + the element they point at. Re-entering that by dragging
  boxes over a mockup is retyping a document that already exists, so easel takes
  the document. One beat per blank-line-separated block: first line the title, a
  line starting `@` the CSS selector, the rest the body, a leading `!` to ask
  the client to approve that beat.
  - **Replace is off by default** and takes the old beats' approvals with it
    when used — an approval belongs to the beat it was given on, and a replaced
    beat is not the beat the client approved.
  - Parsing lives on the server: one implementation of the format, and testable.
- **Copy link, carrying exactly what you are looking at.** The viewer's URL now
  also holds whether the discussion is open (`drawer`) and which tab of it
  (`feedback=open|resolved`), alongside the mode and pin it already carried. So
  "look at note 7" and "see the open ones" are links you paste into a message
  rather than instructions someone has to follow by hand.
  - Falls back to a prompt when the clipboard is refused (permissions, insecure
    context) rather than silently pretending it copied.

## 0.14.0 — 2026-09-01
**A conversation about a screen as a whole**, alongside the pins — not instead
of them.

Not all feedback is spatial. *"I prefer the second one but the tone feels
formal"* is not about a rectangle, and until now a client had to invent a
location for it or keep it to themselves.

- **Built on the standard's own seam, not a new table.** The Interaction
  Standard defines `thread.subject_type` as app-defined and plural
  (`"stage"`, `"mockup"`, `"flow_step"`). easel used only `"pin"`; screens are
  `"screen"`. `UNIQUE(subject_type, subject_id)` means a screen has **at most
  one** thread — a running conversation, not a pile of notes. The pile is what
  pins are for.
- **Pins remain the primary act** and are untouched. The screen note is
  deliberately quieter, and its empty state points back at pinning: *"For a
  thought about one part of a design, open it and pin a note there instead."*
- **It turns the board.** An open screen conversation counts as client feedback
  in the rollup and on the index, exactly as a pin does. Feedback that does not
  move the status is feedback the client left into silence — the worst outcome
  there is, which is why this is in the rollup rather than beside it.
- **One comment surface, several subjects.** Replying, resolving and attachments
  all run through the existing thread paths rather than a parallel set — the
  pin lookup simply became optional, which is what the standard assumed all
  along.
- No date fields were added: caddie owns the roadmap and its `target_date`, and
  rian's position there is explicit — shown, never enforced.

## 0.13.0 — 2026-09-01
The board becomes a real sidebar plus a work column.

- **The sidebar holds everything ABOUT the project** — status, present-to-client,
  the blurb, then "How to use this" and the contents. The right column is
  nothing but the screens. Only the title spans both.
- **The columns start on the same line.** The rail began at the top while the
  screens sat a margin lower, which is precisely what stopped it reading as a
  sidebar. The grid's top margin is gone and the sidebar is one sticky column of
  blocks.
- **"Present to client" is back, in the sidebar.** It was removed from the head
  in 0.12.1 for being the loudest thing on the page; in the sidebar it is
  available without dominating, and it still only means something to the team.
- **"View ↗" is the primary action on a row, and it opens a new tab.** The board
  is the big picture: a client who navigates away to look at one design loses
  the sense of how many there are and what else is waiting. Keep the map open,
  read the design beside it.
- **Choose stops shouting before there is anything to choose between.** It was
  solid accent and simply said "Choose", which reads as a demand made of someone
  who has not looked yet. Now the row states plainly where the option stands —
  *Not chosen yet* / *★ Chosen* — and Choose waits as an outlined action in the
  colour still reserved for it. You cannot choose what you have not seen, so
  View is the solid one.

## 0.12.2 — 2026-09-01
- **Removed 30 lines of dead CSS** that survived the thumbnail removal: a whole
  duplicate `.opt-list / .opt-row / .opt-open / .opt-name / .opt-choose` block
  from before the tile experiment, still declaring contradictory rules for the
  same selectors (a row-direction `.opt-open` against the live column one, a
  neutral `.opt-choose` against the live accent one, `cursor: default` on a
  control that is now clickable). Only source order was keeping the right ones
  winning.
- **The option row's left alignment is explicit rather than inherited.**
  `align-items: flex-start` on the name/tags column and `justify-content:
  flex-start` on the tag strip, so nothing about the row's height or the card's
  width can centre them.
- The two `.opt-row` declarations are merged into one.

## 0.12.1 — 2026-09-01
Adi's tidy-up pass on the project board.

- **A fixed 1280px frame, centred**, so the board is the same shape on every
  monitor — with everything inside it aligned left against that frame. Centring
  the frame and left-aligning its contents are not in tension: the first gives
  consistency, the second gives the eye one edge to start from.
- **The head reads title → status → blurb**, each on its own line, instead of
  the status and a paragraph of prose sharing one.
- **The blurb is a place for one short line.** Managers get an inline "Edit".
- **"Re-present to client" is gone.** The button now appears only while a
  project is UNSENT. Once presented, its only effect was another notification —
  sitting in the most prominent place on a board the client cannot even see it
  on. "Present to client" still does the thing that matters.
- **The contents rail is 30%**, down from 35%.
- **An option row reads as something you OPEN.** It led with a ○/★ marker, which
  reads as a radio button — a control you tick, not a design you go and look at.
  The marker is gone, the row is the target, and an arrow slides in on hover.
- **Choose wears the brand accent.** Getting the client to commit is the whole
  job of this page, so the one act that does it stops looking like one more
  neutral control. That colour has been reserved for direction-picking since
  v0.6.1; this is it being spent on the thing it was reserved for.

## 0.12.0 — 2026-09-01
- **The board is two columns**: a contents rail on the left (~35%), the screens
  on the right. The rail is **open**, not a collapsed strip — with thumbnails
  gone there is width to spare, and the pane's second job (*what have I been
  through*) only works if you can see it without asking for it.
- **The rail is sticky.** It stays put while the screens scroll past it. A map
  you have to scroll back up to find is not doing its job.
- **The board is left-aligned, not centred.** It is two columns of left-reading
  content, and centring a column of list rows in a wide viewport only pushes
  them away from the edge the eye starts at. Max width 1500px, no auto-centring.
- At phone width it collapses back to one column and the rail returns to a
  toggle, as before.

## 0.11.1 — 2026-09-01
- **One screen per row.** The board stacks screens in a single column rather
  than tiling them. A multi-column grid made the reading order depend on the
  viewport width; stacked, screens appear in the order the client meets them —
  which is also the order the walkthrough runs in.
- **The board column narrows to 1100px.** It went wide (1920px) to fit a row of
  thumbnails per screen. Options are a list now and screens are stacked, so the
  wide column stopped earning its keep: a list row spanning 1900px leaves a
  name and its controls an eye-journey apart. Sized for reading instead.

## 0.11.0 — 2026-09-01
**The board is a list.** Thumbnails are gone from the project overview.

Adi's call, and the reasoning is worth keeping. At tile size the frame rendered
at roughly **10% scale**, so a 4:3 crop of the top of a page was a silhouette —
and the very thing that crop strained to communicate ("no hero" vs "reduced
hero") was already sitting beside it **in words**, as the concept tag. The tag
wins: legible, exact, free.

The deeper reason: **the board's job is navigation and status; comparing designs
happens in the viewer**, where they are full size and the variant toggles live.
Once that is true, thumbnails on the board are decoration.

- Each option is a row: chosen marker · name · concept tag · the locked variant
  labels · walkthrough beat count · "No mockup yet" where it applies · open
  feedback · Choose · Remove. More information than the tile carried, and all of
  it readable.
- Screen cards need far less width without a row of thumbnails, so the board
  grid floor drops 470px → 360px, which buys more screens across at the same
  viewport.
- Drops one sandboxed iframe **per option** from the board — the page-weight
  cost flagged when per-option tiles landed.
- The `Thumb` component and its browser-window framing are removed rather than
  left as dead code. Live thumbnails still exist where they earn their place:
  full size in the viewer.

## 0.10.1 — 2026-09-01
- **The header's right side is a row.** `.app-topbar-right` had `margin-left:
  auto` and nothing else, so the bell — a block-level wrapper — stacked *above*
  the account menu rather than sitting beside it. That is what made the header
  read as untidy. It is a flex row now: bell first, then the account menu, so
  the thing that tells you something is waiting sits to the left of who you are.

## 0.10.0 — 2026-09-01
The home page becomes a worklist, and the app bar starts carrying information.

- **The index answers "what needs me?"** It was a grid of cards reading
  *name · state · 2 screens · 3 options* — a directory listing. Rows now lead
  with whose move it is, grouped **Needs you** / **Nothing waiting**, and sorted
  needs-you first then most recently active.
- **"Needs you" is answered FOR THE ASKER.** The same project is the team's move
  when client feedback is open and the client's when it is their turn, so the
  flag is computed per side rather than baked into the state. One project, two
  honest answers.
- **Three fields the API never returned**: `open_feedback` (open *client*
  threads — our own notes are not feedback waiting on us), `last_activity`, and
  `needs_you`. Activity deliberately counts **comments as well as events** — a
  reply is not an event, and a project whose only recent news is a reply would
  otherwise read as idle, which is exactly the project you most want to see.
  Both come from grouped queries, not per-project ones: the index is the page
  whose cost grows with how much work you have.
- **The app bar**: a drawn mark beside the wordmark, and a **Team / Reviewing**
  chip. The chip is capability-derived, never a level name — the auth kit's
  contract forbids switching on one — and it flips during a View As, which is
  the moment it earns its place. The bar is sticky now.
- At phone width the row drops the state chip and screen count: the name,
  whether anything is open, and how stale it is are the whole answer.

## 0.9.4 — 2026-09-01
**rian's M2 ask 4: option 2 gets a smaller hero.**

- New concept `home-bold-welcome-reduced-hero.html` (source of truth:
  `leaguelaw/prototyping/`). **Exactly one rule differs** from The Bold Welcome:
  the hero is `655px` instead of `calc(100vh - 92px)`.
- **655px is measured, not chosen.** It is the live League & Williams hero
  height, read from `home-replica-v3.html` (`.hero { height: 655px }`) — which
  is what rian meant by "closer to the live site". The old rule computed to
  ~990px on a 1080 screen *and grew with the display*, which is what made it
  read as a full-bleed fullscreen hero.
- Nothing else moved: no type sizes, no spacing, nothing below the hero. So
  comparing option 2 against option 1 changes **one variable**, which is the
  whole point of the two-option model.
- Geometry checked before shrinking: the hero form is ~414px and the practice-
  area strip is pinned to the hero's bottom, so both still clear inside 655px.
- **The seed is now two options** (ask 3's shape): The Direct Answer *(No hero)*
  and The Bold Welcome *(Reduced hero)*. The Welcome Desk is retired from the
  seed rather than deleted — its file stays in the bundle if it is ever wanted.

## 0.9.3 — 2026-09-01
- **The project blurb is editable in the app.** It renders under the title *for
  the client*, and there was no way to change it — the endpoint existed but
  nothing called it, so seeded text was stuck behind an owner-only
  `docker exec`. Managers get an inline editor, with the line that matters said
  plainly next to it: the client reads this under the title.
- **The league seed's blurb is rewritten for the person who reads it.** It said
  "Seeded by Claude (2026-09-01) from the prototyping library — curation pending
  Adi's review… walkthrough rects are provisional." That is our provenance, our
  curation state and our option-ordering plan, printed on the first thing darren
  opens. Now: "Homepage concepts for review. Open one, leave a note anywhere on
  it, and choose the direction that feels right."

## 0.9.2 — 2026-09-01
- **Option tiles keep their size when the option count changes.** The tile grid
  used `auto-fit`, which collapses empty tracks and stretches whatever is left —
  so removing one of three options made the other two grow. That is backwards
  twice: the same design changes size depending on how many neighbours it has,
  and a one-option screen ends up with a *bigger* picture than the three-option
  screen carrying the actual decision. `auto-fill` makes the track count a
  function of available width instead, so tiles are the same size everywhere and
  removing one leaves a gap rather than a reflow.
- The screen card's minimum width moved 420px → 470px so three 140px tiles plus
  their gaps always fit, which is what makes that size consistent at every
  viewport. At 1920px the board still shows three screens across.

## 0.9.1 — 2026-09-01
Removing an option — asked for after an accidental "+ Add option", and it turned
up two real bugs on the way.

- **You can now remove an option or a screen from the board** (manager only), so
  a mis-clicked extra option is no longer permanent. Quiet controls: a delete
  should never be the most eye-catching thing on a card.
- **Fix: deleting a chosen option left a dangling direction.** `selected_option_id`
  survived the delete, so the board reported a locked direction that no longer
  existed — and on Postgres the foreign key would have refused the delete
  outright, i.e. a 500 rather than a friendly refusal. The selection (and its
  variants) is cleared first now.
- **Fix: walkthrough steps and approvals had no cascade.** Deleting an option
  that carried a walkthrough would have hit the same wall in production. They
  are removed explicitly.
- **An option carrying a conversation cannot be removed at all** — 409
  `HAS_FEEDBACK`, naming how many notes are on it. The Interaction Standard's
  rule is that a conversation is never hard-deleted; the practical reason is
  that a mis-click must not be able to destroy a client's feedback. Resolved
  notes protect it too — resolved is not deleted. Screens inherit the rule,
  since they cascade to their options.
- The refusal is shown to the manager as the server wrote it, recovery line and
  all, rather than as a generic failure.

## 0.9.0 — 2026-09-01
**rian's M2 ask 3: two options plus toggleable variants.** A concept is no
longer one option per combination — it is a couple of options with toggles
inside them, so every comparison changes exactly one variable. Adi's own notes
named the problem this fixes: in `v1…v6` order the 2→3 and 4→5 steps changed the
opening *and* the brand at once, "the hardest kind of comparison to hold in your
head".

- **A variant is a CSS class the bridge swaps on `<html>`.** One bundle serves
  every combination, and toggling costs **no reload** — so the client keeps
  their scroll position while flipping, which is the whole reason to prefer this
  over separate files. The bridge only ever removes and adds classes the app
  names, so a page's own classes are untouched and page content can never
  influence which classes exist.
- **A variant choice is part of the locked direction, not decoration.** Adi's
  closing note asks the client for "an opening AND a brand level" — two
  decisions — so `screens.selected_variants` rides alongside
  `selected_option_id`, and a screen whose option carries axes is not *decided*
  until every axis is answered. The rollup cannot report `done` on half a
  decision.
- **The toggles are captured at the moment of choosing.** What they are looking
  at IS what they are approving, so there is no second gate — and the confirm
  dialog names the whole decision ("The Direct Answer with Full hive"), because
  locking a brand level they never read would be asking them to approve half an
  answer. A client who never touched a toggle still records a complete choice:
  the axis defaults fill in.
- **The team is told which variant was picked** — "Direction picked: The Direct
  Answer (Full hive)". The dropped half is the one we would have got wrong.
- **Axes are grouped and mutually exclusive**: an option can carry several
  ("brand", "header"), and the client picks one value on each.
- Pins stay **per option**, not per option-per-variant. A drifted anchor is a
  much smaller problem than feedback that vanishes when you flip a toggle.
- Managers author variants **where they are used** — inline above the design,
  not in a settings screen. A CSS class only means something against the design
  in front of you, and the fastest way to learn a class is wrong is to add it
  and watch the frame not change.
- The board names the locked variants on the chosen option.

## 0.8.2 — 2026-09-01
- **Reverted the part-starring feature (0.7.0).** It was built outside rian's M2
  mandate — his four asks are the tool walkthrough, the contents pane, two
  options with variants, and option 2's smaller hero; starring was not among
  them. Pulled before it could reach darren and start producing feedback in a
  shape rian had not designed for. The idea is recorded in the handoff and can
  come back if he wants it.
- Removed: `step_stars`, the toggle endpoint, star fields on the step and option
  payloads, the markers on the stage, the walkthrough card's star, the
  end-of-tour tally, the board's ★ proportion, and the seven tests.
- **The CREATE migration is deliberately kept and a DROP migration added**
  rather than deleting it: production has already stamped `c4a7e91b6d20`, and
  removing a migration the database has run leaves alembic unable to locate its
  head at boot. Forward-only, like every other schema change here.
- The client lede no longer mentions starring.

## 0.8.1 — 2026-09-01
The board takes the shape of Adi's prototype: wide, framed, searchable.

- **The board runs to 1920px.** It was inheriting the app's 960px column, which
  is right for lists and forms and wrong for a page whose whole job is putting a
  screen's options next to each other. Options need width; the column was the
  reason they never had it. Up to four screens across, without squeezing the
  option tiles inside any of them.
- **Thumbnails are framed as browser windows** — a chrome bar with dots and the
  option's tag. Not decoration: at this size a bare crop reads as an image, and
  these are *pages*; the frame is what says "website" and what makes a row of
  them read as versions of one thing. Taken from the prototype, which does the
  same.
- **Hover dims the thumbnail and offers "⤢ Open & review"**, replacing the
  corner gradient caption.
- **Screen cards carry an index and their slug** (`#1 Homepage /homepage`), so a
  card reads as a page in a flow rather than a floating tile.
- **Search across screens and options**, appearing once a project has more than
  two screens. Matching an option keeps its whole screen — you are looking for
  "the one with the big hero", and the screen is the context that makes the
  answer legible.

## 0.8.0 — 2026-09-01
The board rebuilt around the question it exists to ask: *which of these?*

- **A thumbnail per option, side by side.** A screen card used to show ONE
  thumbnail — of the leading option — and list the rest as text, so a screen
  presenting three designs showed one and named two. Each option now gets its
  own tile with its name, tag, ★ proportion, open-feedback count and Choose
  control. Thumbnails are for recognising a design rather than judging it: at
  tile size "no hero / full hero / headline hero" reads at a glance, which is
  the comparison the board owes the client.
- **Thumbnails scale from a fixed desktop width.** They were sized as a
  percentage of the card (500% at scale 0.2), which cleared these concepts'
  `min-width: 1380px` only while the card stayed large. One tile per option is
  much smaller — a 160px tile rendered the frame at 800px and clipped the design
  down its left edge. The frame now lays out at a real 1440px and scales by
  `100cqw / 1440`, which is correct at any tile size.
- **The contents pane folds to a strip on the board.** As a 15rem sidebar it
  duplicated the grid — its sections ARE the screens, its items ARE the options
  — while taking width from the designs. It is now a collapsed strip above the
  grid that still carries the overall progress count, and opens on demand. In
  the viewer, where the grid is not on screen, it is unchanged.
- **The board tells the client what it wants.** A one-line lede for the client
  side: open a design, note anything on it, star what is working, choose one
  when it feels right. The tour says this once; the board now says it on every
  visit, for whoever skipped the tour or came back a week later.

## 0.7.2 — 2026-09-01
- **The viewer costs one bar of chrome instead of two.** The app topbar (brand,
  bell, account menu) is no longer rendered in the viewer; the bell and account
  menu move into the viewer's own toolbar, ruled off on the right so they read
  as app chrome rather than as another action on this design. Roughly 45px of
  height back, on the one screen where height is the product.
- **The toolbar no longer wraps to a second row.** It was `flex-wrap: wrap`
  unconditionally, so as soon as its contents grew it silently doubled its own
  height. Now nowrap on desktop, with the project and screen names truncating;
  phone width still wraps, because there genuinely is not enough room, and the
  mode toggle drops to its own row rather than squeezing.
- Slimmer metrics throughout the bar: 44px min-height → 34px, tighter padding on
  the bar, its buttons and the mode toggle.

## 0.7.1 — 2026-09-01
- **The client can unpick a direction.** Clearing a selection used to be
  team-only, which left the client able to perform the irreversible act but not
  to reverse it — the one person who has to ask permission to change their mind
  being the one whose mind it is. Clicking the ★ chosen marker (in the viewer's
  breadcrumb or on the board) now opens the same confirm dialog in its undo
  mode, and the gate reopens: the project drops out of `done` back to
  `waiting_client`.
- **An unlock is never silent.** The team is notified on unpick as well as pick.
  The unpick notification is timestamped rather than keyed on the screen, so a
  pick/unpick/pick/unpick cycle reports every leg instead of deduping the second
  withdrawal away — the failure there would be us building on a direction that
  had been taken back.
- The confirm dialog now covers both directions of the act, deliberately in one
  component: what we promise about committing has to stay consistent with what
  we promise about reversing. The choose copy changed accordingly — it used to
  read "clearing the choice altogether is something we do for you", which is no
  longer true.

## 0.7.0 — 2026-09-01
**"I like this bit."** A client can star the parts of a design that are working,
as they read down the page — structured praise to sit alongside the pins, which
only ever captured problems.

- **Stars attach to walkthrough beats.** A beat is already a named,
  selector-targeted part of the design ("Your number, in your face" → `.phone`),
  so starring costs no new authoring pass over the concepts and no `data-region`
  convention. A marker sits on each part in Play mode; one click stars it.
  Comment mode is untouched — there a click on the design is still a pin, and
  the two gestures must never be ambiguous.
- **The walkthrough card carries the same star**, next to (never merged with)
  the approval control. Approval says "understood"; a star says "I like this".
  Two questions, two controls.
- **A star is never a gate.** `step_stars` is a separate table from
  `step_approvals` and feeds nothing but the summary — starring every part of a
  project cannot close it. There is a test that says so.
- **Finishing the walkthrough shows the tally**: which parts they starred, and
  the direction-choice button while it is all fresh. Deliberately not a
  leaderboard — stars inform the decision, they never compute it.
- **The board shows a proportion, not a count** (★ 3/5). Options carry different
  numbers of beats — the designer notes put the full walkthrough on the entry
  option and only differences on the rest — so a raw count would rank options by
  how much was written about them.
- Starring is silent: no notification per star. A client working down a page
  would otherwise fire a dozen alerts at us; the signal is worth reading in
  aggregate, at the end.
- Our own stars don't rate our own work (client-side stars only), mirroring how
  approvals already work.

## 0.6.2 — 2026-09-01
- Picking a direction wears a **star**: hollow (☆) while it is an offer, filled
  (★) once it is the choice — on the viewer's "Choose this direction" button,
  the overview's per-option control, the breadcrumb's chosen marker, the
  contents pane's badge, and the confirm dialog. Adi's read, and the right one:
  choosing a design is an act of favouriting, not a form submission, and a tick
  says "task complete" where a star says "this is the one". The tick metaphor
  is gone from this control entirely so both states tell one story.
  Glyphs are decorative (`aria-hidden`); every control keeps its text label.

## 0.6.1 — 2026-09-01
Adi's human pass on the arrival experience. Three notes, three changes.

- **The tour bubble no longer scrolls away.** It was positioned purely from the
  target's rect, and a rect is in viewport coordinates — so on a long mockup,
  scrolling carried the guidance up and out with its target, and you had to
  scroll back to find it. While the target is on screen the bubble still sits
  beside it (that placement was right and is untouched); the moment the target
  leaves the viewport the bubble docks to the bottom and offers **Show me** to
  scroll back to the spot it is describing.
- **The brand accent is reserved for "Choose this direction".** The active
  Comment/Play tab was filled with the *same* `--brand-accent` as the primary
  button, so the two competed for the same meaning — most visibly when the tour
  spotlit the toggle, which is the moment the client is being pointed at the one
  action we actually want from them. The mode toggle now uses a neutral selected
  state; it reads just as clearly as a segmented control and leaves the accent
  unique on the page.
- The spotlight ring is amber from a token rather than a hardcoded hex, for the
  same reason: the ring must read as "look here", never as "press this".

## 0.6.0 — 2026-09-01
The arrival experience (M2 asks 1 and 2, plus finding 7): a client now meets the
tool before they are asked to judge the design.

- **A tool walkthrough on first arrival.** Landing on a board offers a welcome
  panel with a way straight past it; accepting takes you into the first ready
  option and runs four coach-mark beats over a real design — Play vs Comment,
  why the photos are blurred, how to leave a note, how a direction gets locked.
  Two of the beats are Adi's designer-note pins 2 and 3 moved here verbatim,
  which shortens every design tour by the same two beats.
- **Offered once per person per board.** `tour_progress` stores a seen-version
  per person per board; finishing and dismissing both record it, so nobody is
  asked twice. Bumping `TOOL_TOUR_VERSION` in `app/tour.py` re-offers it to
  everyone below that version, the way the hartlingowners tour does.
- **The tour is data, not code** (`frontend/src/lib/toolTour.ts`): revising it
  is editing sentences. A step may offer several targets and the first one
  actually laid out wins, so a beat still lands when the layout collapses at
  phone width; a step whose target has vanished is skipped rather than shown
  pointing at nothing. Escape always exits, and it honours
  `prefers-reduced-motion`.
- **A sidebar table of contents**, on the board and behind a Contents button in
  the viewer. Sections are "How to use this" then one per screen, entries are
  the options — grouping that falls out of the structure already there rather
  than a tours table nobody asked for. It doubles as the progress read: a client
  sees what they have been through, a manager sees what the CLIENT has been
  through. An entry with nothing to approve is never counted as done, so the
  progress can't overstate itself.
- In the viewer the pane is an overlay, deliberately: a sidebar would resize the
  stage, and the stage's width is what the mockup's scale-to-fit is measured
  from.

## 0.5.0 — 2026-09-01
Adi's first pass: the six fixes from the client-journey findings (handoff
2026-09-01). Behaviour changes, not cosmetics — the status contract and the
client's view of a board both changed.

- **Options without a mockup are invisible to the client.** A fileless option is
  work in progress: the manager still sees it (that is where the upload control
  lives), the client no longer meets a dead frame inviting feedback on nothing.
  Enforced past the view — `/view`, `/pins` and `/walkthrough` 404 for a client
  by id, and a screen with nothing ready doesn't appear for them at all. A
  manager minting a view on an empty option now gets `NO_MOCKUP_YET` (409)
  rather than a token for a document that does not exist.
- **D6 corrected: `needs_attention` no longer fires on the happy path.** It used
  to mean "selection complete but open client pins — conflicting signals", which
  is precisely what a good round produces, so the most alarming state in the set
  was the terminal state of success. Open client feedback is now plainly
  `waiting_agency`; `needs_attention` means the one genuinely anomalous thing —
  a board presented with nothing on it to review. Rollup counts READY options
  (those with a mockup), so an unfinished board can't report a decidable screen.
- **One status vocabulary for both sides.** The chip renders to clients too, and
  read "Needs our reply" — our sentence about ourselves, shown to them. Labels
  now describe the work, not whose turn it is from a point of view:
  In preparation · Awaiting review · Feedback to address · Direction locked.
- **The mockup no longer reloads mid-review.** The view token was refetched
  every 10 minutes; each refetch produced a new URL, and a new iframe src is a
  reload — which threw the client back to the top of a 4,600px page. The viewer
  now pins the first URL for the life of the mount, and the token outlives a
  review sitting (15m → 60m) so lazy-loaded assets keep resolving.
- **Replies appear while you are reading.** Pins and their threads poll on a 20s
  interval, so an answer lands without the client navigating away and back. The
  frame is untouched by it — only the overlay re-renders.
- **Picking a direction is prompted, never silent.** The click opens a dialog
  that says what choosing means and what happens if they change their mind
  (they can re-pick; clearing stays a team act). Same sentence the tool
  walkthrough will teach.
- Fix: the viewer's option navigation and its "Choose this direction" button
  identified the current option **by title**, so two options sharing a name
  would have chosen the wrong one — a live hazard under the coming
  two-options-plus-variants model. By id now.

## 0.4.2 — 2026-09-01
- Fix: walkthrough targeting resolves the first VISIBLE match of a selector,
  not merely the first match. `.rcall` on the league homepage matches six
  copies of the phone number hidden inside hover mega-menus, so the spotlight
  landed on an off-screen element; the seed now targets `.phone` and the
  bridge skips hidden matches (and hidden ancestors) generally.
- Fix: the viewer measures stage width via a callback ref. As an effect it ran
  while the component still showed its loading placeholder, so the observer
  never attached and the scale-to-fit silently did nothing.

## 0.4.0 — 2026-09-01
- Fix: mockup bundles now ship their relative assets. The league seed uploaded
  only index.html, so every `src="img/..."` 404'd and the concepts rendered
  image-less. Seed walks the page's image references and stores the whole
  bundle; the overview gains an "Upload folder" control (webkitdirectory) so
  multi-file bundles land intact. Regression test covers relative-asset serving.
- Fix: walkthrough steps target CSS SELECTORS, resolved against the live mockup
  by the viewer bridge, instead of hand-guessed percentage rects — a rect of
  "7% tall" on a 4,600px page highlighted a 320px band instead of the phone
  number. Drag-authored rects remain the fallback.
- Fix: the viewer renders the mockup at its natural width and scales it to fit
  the stage. These concepts carry `min-width: 1380px`, so a stage-width frame
  clipped the right edge (nav and phone number cut off); the same mechanism is
  what makes a desktop concept legible at phone width.

## 0.3.1 — 2026-09-01
- League seed: project "League & Williams — website concepts" — Homepage with
  the three openings (Direct Answer / Welcome Desk / Bold Welcome, quiet
  brand) + Your Team, bundles from leaguelaw's prototyping library, 5-beat
  walkthrough in Adi's designer-notes voice (closing beat requires approval),
  darren granted as client on this project only. Idempotent app/seed_league.py.

## 0.3.0 — 2026-09-01
- The product surface: projects/screens/options API, isolated mockup serving
  (/m/ signed bundle tokens, CSP sandbox on every response, injected bridge),
  pins + threads + comments + attachments, guided walkthrough with per-step
  client approvals, notifications bell, live six-state status.
- Full SPA: projects list, overview grid with live sandboxed thumbnails and
  direction picking, the reviewer (comment/play modes, pin overlay, discussion
  drawer, walkthrough player + drag-to-author), generated OpenAPI TS client +
  TanStack Query. 94 backend tests green.

## 0.2.0 — 2026-09-01
- Stack conversion to the caddie bar: Postgres sidecar (internal network, no
  published port), SQLAlchemy 2.0 + Alembic, auth-kit store moved to the app DB
  via bw_store_sqlalchemy managed mode; adi's admin membership migrated in the
  initial migration.
- Instances ON (`instance_noun = "project"`); conformance 62/62 with the
  instance suite active.
- Domain schema landed: projects/screens/options/mockup files/pins,
  Interaction-Standard-shaped thread/comment/attachment/notifications,
  walkthrough steps + approvals, append-only events, caddie_links seam.
- Six-state status rollup implemented as a pure function with lifecycle tests.
- Three-stage image build (OpenAPI schema dump -> generated TS client -> SPA).

## 0.1.0 — 2026-08-28
- BW-app scaffold deployed (sign-in, levels, invites, View As, access matrix).
