# Backlog - what the sweep of system against product left open

Stage 08, step 6. Page: `design/kit/backlog.html`.

This file is the output of the step the pack calls **«звід системи з продуктом»**: three lists,
taken by walking the whole corpus in a browser after the system was built, against the same walk
taken before it. It is not a table of opinions - every line below has a number behind it and an
instrument that will reproduce the number: `node tools/census.mjs`.

## The instrument had to be built first, and that is finding zero

Step 1 produced `census.md` - 22 229 observations over 180 screens - and closes with its own
sentence: **«the script is the artifact, not the table»**. The script was not kept. Only
`btn-census.json`, which is the result. So «два заміри одним приладом» was impossible until there
was an instrument, and rebuilding one from memory is exactly the hand fix this repository bans for
tools as loudly as for pages.

`tools/census.mjs` is rebuilt from the METHOD as `census.md` states it: two viewports (390 and
1280, because the desktop header and the mega menu do not exist at mobile width), in a browser and
never by grep (the header is injected by `_nav.js` and is not in the markup at all), computed
style rather than the written rule, and the control test verbatim - `a` / `button` / `label` /
`[role=button]` / `[onclick]`, **or** an element that INTRODUCES `cursor:pointer` its parent does
not have.

**What is not reproduced is said rather than tuned.** `census.md` folds its boxy controls into 24
forms on four axes and excludes chips, tabs, thumbnails, pagination and fields from the action
family - by a list of classes that lived in the lost script and appears nowhere in the prose.
Tuning until the numbers matched a published table would prove only that they can be tuned.

## The corpus, and the number that changes what any earlier census is worth

| | static walk | with panels opened |
|---|---|---|
| loads (screen x width) | 460 | 460 |
| panels opened first | – | **11 946** |
| clickable observations | 35 714 | **63 154** |
| boxy controls | 7 118 | **14 896** |

**43% of the product's clickable surface is behind a state**, and a walk that only reads what is
already painted cannot see any of it. That applies to step 1's own 22 229 as well: it was taken
statically, which is the honest reason the two measurements do not reconcile arithmetically.

## List 1 - product to system

**Nothing is missing from the system.** Every control that renders in the grey layer either has a
component, or lives on a screen that has never been coloured.

Two candidates came back and both were on `wireframes/overview.html`, the hub that LISTS the
screens rather than being one - `flowlink` and `sm-item`. The hub is excluded by the same line
already drawn through `kit/` and `concept/`; CLAUDE.md draws it by name.

**22 controls are tirage, not gaps** - every occurrence sits on one of the 54 grey-only screens:

`addr-save` (4 screens) · `bsearch` (3) · `uachip` · `bcard` · `chub-goal` · `chub-cat` · `thumb` ·
`ttl` · `read` · `cert` · `moz` · `cta` · `rev-google` · `q-exit` · `sc-goal` · `sc-chip` · `clr` ·
`ov-row` · `ov-cb` · `ov-prod` · `ov-chip` · `ov-all`

They close when their screens are coloured, which the roadmap already parks after stage 09.

## The scope was missing on 23 screens, and it changes what list 2 means

Step 7.95 moved eight private stylesheets of the coach flow into
`design/system/components/` and scoped every selector in them: `.coach .qa-row`, `.coach .cnew`,
**360 selector occurrences across 18 files**. The scope class went onto the eleven screens of that
step by hand, on `<body>`.

Nothing else ever put it anywhere. `clone-to-colour.mjs` takes a screen out of `wireframes/`, and
the grey layer has **no body class at all** - 142 files, 142 bare `<body>` tags - so every state
screen coloured at 8.13 and 8.14 arrived without it. On those screens the whole coach layer of the
system is **inert**: not overridden, not losing on specificity, simply never matching.

`tools/scope.mjs` puts the question to the browser as a difference rather than as a name list: add
the class, read the computed style of every element, take it away, read again. **23 screens moved.**

| | |
|---|---|
| coloured screens whose base wears a scope they lack | **23** |
| of them moving 40 or more elements | 9 |
| the loudest - `coach-session-priceblock` | 87 of 1 434 elements, 56 selectors on 87 |

**Which screen a scope belongs to is written in the screen, not guessed.** Every state ends its
script with `wfBar('<base>.html', '<state>')` and every base names itself, so a state wears exactly
what its base wears. The alternative offered itself and was wrong: pairing the scope with
`wfHeader('coach')` / `wfCoachNav(` catches 36 screens and misses three that already carry the
class - `cart-coach`, `coach-landing`, `coach-verify` are coach screens without the coach rail.

**Five more screens move and must NOT be painted**, and they are the evidence that the scope is
load-bearing rather than decorative: `concept/directions` (203 elements from 2 selectors),
`account-orders` (67 from 2), `kit/order-row`, `kit/badge`, `checkout-loggedin`. There `.coach .x`
would collide with an `x` that means something else, which is what a namespace is for. The check
reports them in a list of their own and never writes them.

One screen wears the scope for nothing - `cart-coach`, where no scoped selector bites. **Decided
2026-08-15: kept, and the check stopped calling it a defect** - see item 6. The rule that used it
there moved to a correct guard at step 7.96, and a namespace with nothing to bite costs one class
token, where a namespace missing from one screen costs a class of silent bugs. What the check asks
now is the opposite direction: a screen wearing a scope its base does NOT wear.

## List 2 - system to product, and this is the whole of the remaining work

| | at step 6 | 2026-08-17 |
|---|---|---|
| screens carrying a private rule | **31** | **0** |
| private rules in total | **1 154** | **0** |
| of them redrawing a class the system already owns | **886** | **0** |
| of them declaring something that exists only there | **210** | **0** |

**Zero.** The last two were one deferred decision, not two jobs - `.cv-card{ max-width: 560px }` on
`coach-verify-error` and `coach-verify-deadend` - and it was taken on 2026-08-17 rather than deferred
again, because the measurement the deferral lacked turned out to decide it. See item 2. The
right-hand column is a re-measurement by the same instrument, not 1 154 minus what left.

**«Overriding» was measured as «the system owns this class too», and on the coach state screens
that was not the same thing** - until 2026-08-14 the system did not reach them at all, so the
private block was the only paint on the page rather than a layer on top of one. The counts stand;
the word did not, and the sweep below is what makes it true.

The twelve loudest screens are **all in the coach flow**, which is the primary audience:

| screen | private rules overriding the system |
|---|---|
| `coach-session-addclient` | 74 |
| `coach-session-addempty` | 74 |
| `coach-session-priceblock` | 67 |
| `coach-session-oos` | 65 |
| `coach-session-loading` | 64 |
| `coach-session-newclient` | 52 |
| `coach-home-empty` | 44 |
| `coach-home-free` | 39 |
| `coach-tariff-cancel` | 34 |
| `coach-order-error` / `coach-order-loading` | 33 each |
| `coach-clients-cap` | 30 |

**This list is not cosmetic debt, and one measured defect proved it - though not for the reason
first written here.** `coach-session.css` answers the phone with `@media (max-width: 479px)`:
`.qa-row` stacks, and the action takes the whole second line. The base screen reads that and passes
at 360. The four state screens did not, «Додати клієнту» hung 10px past the viewport, and
`html{ overflow-x: hidden }` CLIPPED those 10px rather than scrolling, so the right edge of the
button could not be reached at all. `node tools/accept.mjs 360` returned **4 failures over 204
screens**, all four that page.

**The cause written here was wrong, and the wrong cause is kept because it is instructive.** It
said «a private block wins over a linked sheet». It does not: `.qa-row` is one class and
`.coach .qa-row` is two, so the system already outranked the copy. The rule never matched, because
the page was never inside `.coach`. **Specificity was blamed for what a missing scope did**, and the
sentence was plausible enough that nobody asked the browser.

Fixed 2026-08-14 by `tools/scope.mjs --apply`. `accept.mjs 360` now returns **0 over 205 screens**.
The private `.qa-row` copies are still on those pages and now lose to the system on every
declaration, which moves them out of «overriding» and into the inert set below.

**And it is why stage 10 cannot start on top of this.** The product carries 222 media blocks: 170
in the system and **52 in the private blocks of these 31 screens**, adding 7 boundaries of their
own on top of the system's. A responsive scale cannot consolidate what does not live in the system.

## List 3 - a class nobody wears

**Zero dead classes remain.** Six were deleted by the owner on 2026-08-14; the seventh was an
error of the instrument.

Two further buckets are named rather than counted, because neither is a defect:

- **30 classes behind a state the walk cannot reach.** A script writes each of them, so they are
  live. The walk calls every `open*` / `toggle*` global, but it cannot SCROLL (`stuck` and
  `pdp-stuck` in pdp-tabs, `uiv-scrolled` in the header) and it does not advance a flow past its
  first step (`auth-load`, `auth-spin` - the dialog after submitting). This is a named limit of
  `census.mjs`, not a hole in the system.
- **43 classes waiting for their screen.** Worn by the grey layer, with no coloured twin yet -
  mostly `system-page.css` (16), `cookie-banner.css` (15) and `account-shell.css` (10).

### The instrument was wrong four times, and every error had the same shape

Each answered a question NEXT to the one being asked, and each produced a plausible number:

| reported | actual cause |
|---|---|
| **962** dead classes, `coach` and `wfh` among them | the map was built from CONTROL rows, so «never worn» meant «worn by nothing clickable» |
| `svg` `jpg` `png` `html` are dead component classes | they are the tails of `url(../../visuals/…jpg)` and `a[href="index.html"]` |
| 12 classes of `cat-overlay.css` are dead | nothing had OPENED the overlay - `census.md` withdrew this exact finding once, about `.tbuy` |
| `menu-pop` is dead | `design/system/menu.js:39` sets it at wire time and CSS hides it until it opens; the collector skipped `display:none` |

The fourth cost a 35-minute walk twice over: the correction was announced before it was verified,
the patch had not applied, and the re-run produced a byte-identical record - 1518 distinct classes,
50 417 total, in both files. **The identity is what exposed it.**

### Deleted 2026-08-14, and what each deletion cost

Two were lint, one was free, three removed a designed state. The reasoning of the three is kept
here verbatim so re-adding any of them is one line of CSS and a paste.

**`.only-mobile` / `.only-desk`** (`account-shell.css`, 6 rules including two 860 media blocks).
Lint: 0 wearers on 141 grey screens and 87 coloured ones. This CLOSED a question `account-shell.html`
had held open since step 5 - «два імені є тільки в css і в жодному html. Або утилітний файл, або
видалення». The page's note «three different breakpoints for one thought» is now two: 860 left
with the utilities, and the scale of boundaries is stage 10's subject.

**`.field--err`** (`field.css`). Free: the selector read `.field--err, .field.err`, and the measurement
ended the argument written above it - the runtime writes `.err` and something wears it, the tidy
name is worn by nothing. **No declaration changed.** The state fires exactly as before.

**`.certthumb--pending`** (`cert-thumb.css`, one declaration). *What the system can no longer say:*
«this certificate is claimed but not yet on file». Verbatim:

> пунктирний край, без мітки «PDF», без печатки, без підйому - документа на партію ще немає.
> Аркуш лишається аркушем, але нічого не обіцяє: ні файлу, ні натиску.

**`.qans--wait`** (`qa-item.css`, one rule). *What the system can no longer say:* «the shop is
preparing an answer». Verbatim, including the five anatomy notes and the 390 measurements:

> Питання чекає на відповідь днями, і саме таким його бачить більшість читачів. Досі пару можна
> було намалювати лише завершеною: `.qans` це магазин, який говорить, а мовчання вигляду не мало
> зовсім.
>
> 390 на `product.html`: обидві коробки 358 завширшки, поле 12 з чотирьох боків, відступ 8 згори,
> радіус `0 8 8 0` · очікування 46.4 заввишки - рівно стільки, скільки коротка відповідь.
>
> 1. **Модифікатор, а не друга коробка.** Рядок мусить стояти рівно там, де стане відповідь: коли
>    вона прийде, коробка виросте вниз, і більше в парі не зрушить нічого.
> 2. **Чорнило** - `--text-secondary` замість `--text-body`. Список гортають у пошуках відповідей;
>    рядок тим самим чорнилом, що й відповідь, заводить око в коробку, де нічого немає. Це примітка
>    про відповідь, а не відповідь.
> 3. **Грань** - `--line-strong` замість `--line-action`. Помаранчева грань - єдине місце системи,
>    де акцент носить лінія без кнопки поруч, і дозволено це тому, що вона позначає голос. Голосу
>    тут ще немає, тож грань відступає до того самого чорнила, яким `.empty` малює свою рамку.
> 4. **Не `--line-hair`.** На поверхні #FAF9F7 грань #E9E7E2 це 2px нічого; #D9D9D9 ще читається як
>    шов. Заміряно на обох.
> 5. **Ні курсиву, ні пунктиру, ні дати.** Курсив - другий типографічний регістр заради одного
>    рядка. Пунктир на 46px висоти читається як обрізок, а не як рамка.

**`.tsx--unproven`** (`trust-strip.css`, the class plus two `:not()` that were always true).
*What the system can no longer say:* the difference between a proven trust signal and an unproven
one - and «trust first» is design principle 1. **Not one tile moved**: 290 x 66.38 tile,
38 x 38 icon box, 308 x 283.5 strip at 390, to the hundredth, the same figures measured when the
exception was added. Verbatim:

> другий рядок каже «на цю партію - уточнюємо», наведення не спрацьовує - сертифіката на партію ще
> немає. Смуга не знімає обіцянку, вона перестає вдавати, що вже її довела. 7.72: «роль - це
> обіцянка про те, що робить контрол». Там, де за плиткою документ, підйом хоча б вказує на щось
> справжнє; на «уточнюємо» за ним нема нічого.

## The migration was attempted, measured, applied and reverted - and that is the record

**262 of the 1 185 top-level private rules measure as inert** - 215 matching no element on their
own page (clone residue: `clone-to-colour.mjs` copies the full screen's block into its empty and
loading states whole) and 47 matching elements without moving a single value. `coach-order-loading`
alone is 37 of 42.

**The cut was applied and then reverted, because the proof caught it.** `tools/tree-diff.mjs` -
git archive the reference into its own tree, serve both, one Chrome, compare the computed style of
every element on 40 properties at 390 and 1280 - found **9 movements on 5 screens**:
`coach-clients-cap`, `-empty`, `-error`, `-loading` and `coach-home-loading`. At 390 a rail link
turned **orange** (`rgb(242,240,237)` to `rgb(255,90,0)`); at 1280 the page grew 555px.

**The cause is the method, not a bug.** The probe tested each rule ALONE, with every neighbour in
place, which answers «is this rule redundant GIVEN all the others». **Inertness is not additive.**
`coach-clients-cap` held both `.acc-nav` and `.acc-link[aria-current="page"]`; neither alone changed
anything, and losing both turned the rail into the mobile chip strip whose current chip
`account-shell.css` paints with `--bg-action` under `@media (max-width: 959px)`.

Two corrections went in and both are right: the walk is cumulative now, against a **full-document**
snapshot (a rule restyles only what it matches, but LAYOUT travels - a parent grows with its child
and is not in the selector), and the set found at one width is re-offered at the other, because
their **union was never tested anywhere**: 390 may clear {A,B} and 1280 {A,C}, and {A,B,C} is a
configuration no browser was asked about.

**They did not close the gap.** The probe still clears rules the fresh page does not. The remaining
difference is between MUTATING a loaded document and LOADING one without those rules - the page's
own scripts have already run and have reacted to what they saw. That is a hypothesis, not a
measurement, and it is written here as one.

**So the deletion waits for a probe that can be trusted, and the proof stays the gate.** The
measurement is not wasted: the 262 are identified, the two kinds are separated, and
`tools/tree-diff.mjs` will name any page a future cut moves, at any width, to the element and the
property.

## What step 6 still owes

1. ~~**36 controls wear `btn` with no rank and render as bare text.**~~ **Closed 2026-08-14.**
   `button.css` has no `.btn` rule - the finish IS the rank - and `clone-to-colour.mjs` matched
   `class="btn"` and `class="btn dark"` as whole strings, so every button carrying a utility class
   beside them slipped through: `btn qa-add` (14), `btn cs-save` (7), `btn dark cs-go` (6),
   `btn dark co-new` (3), `btn dark cgo-btn` (2), and four more, over 13 screens. Same family as the
   8.13 lookahead: a pattern tight enough to be right about the case in front of it and wrong about
   the set.

   **The rank came off each screen's base and was never chosen by hand**, which mattered: the grey
   layer marks `cs-go`, `co-new` and `cgo-btn` as `dark` - its word for a primary action - and the
   coloured bases had deliberately made all three `btn--outline` at 7.95 and 8.7, because a screen
   carries one accent fill and these were not it. Ranking them off `dark` would have put three new
   orange fills into the coach flow and called it a bug fix. **34 of 36 were answered by the base**;
   the two the product could not answer are decided in `tools/btn-rank.mjs` with their neighbour
   written beside them - `cart-coach-empty .cont` takes `btn--outline btn--s` from the buyer's own
   `cart-empty`, and the bar's `.blocked` on `coach-session-priceblock` takes the base bar's
   `btn--accent btn--l`, its `aria-disabled="true"` already being something `button.css` answers.

   The transform now matches `btn` as a **token** rather than as a whole attribute, and hands the
   final say to `btn-rank.mjs`: its own default is a starting rank, not the decision.
2. **Move the 886 overriding rules into their components.** This was the step's own remaining body
   of work and the precondition for stage 10. **CLOSED 2026-08-17: 886 -> 3 -> 2 -> 0.**

   **THE LAST TWO NEEDED A DECISION, AND THE DECISION NEEDED A MEASUREMENT NOBODY HAD TAKEN.**
   `.cv-card{ max-width: 560px; margin: 0 auto }` stood on `coach-verify-error` and
   `coach-verify-deadend`, byte for byte identical, deferred in the honest words «whether 560 becomes
   the panel's width is stage 09's decision». What the deferral did not say is that a THIRD screen
   carries `.cv-card` - `coach-verify-loading` - and shipped the other answer: 828px at 1280, with a
   centred headline in a 778px box, while its two siblings centred the same rank across 510.

   There is no selector separating the two that declared it from the one that did not, so the rule
   could not be moved without deciding. It was decided, and written beside the rule as «variable ->
   value -> why»: `.coach .cv-card` gains `max-width: 560px; margin-inline: auto`, because (1) two of
   three declare exactly this, identically, and a rule two screens write the same way is a component
   rule in the wrong file; (2) the system already caps what is inside the card - `.cv-lead` 440,
   `.cv-card.mid .cv-actions` 340 - so a width for the card is what those two caps are already half
   saying; (3) `.mid` MEANS centred, and 828 is too wide to centre in.

   **A/B'd in the live pages at 1280 / 900 / 390**, rule injected and removed: `error` and `deadend`
   byte-identical at every width and every number, `loading` 828 -> 560 at 1280 and 900 with its
   button row wrapping to two lines and the card 52px taller, `loading` identical at 390 because the
   cap never binds there. `scrollWidth - clientWidth` 0 everywhere, both ways. Two of the three
   buttons in that row are the prototype's own demo switches, not product controls.
   **Reversible in one declaration**, and the whole cost of being wrong is 268px on one desktop
   screen.

   **The inert half is gone, 2026-08-15: 655 of 1 154 private rules removed from all 31 screens,
   and the proof says nothing moved.** `tools/tree-diff.mjs --dir` compared the computed style of
   every element on both widths against the tree as it stood minutes before the cut: **62
   comparisons, 0 elements moved.** That is the same comparison that refused the June attempt with
   9 movements on 5 screens.

   | | before | after |
   |---|---|---|
   | private rules on the 31 screens | 1 154 | **499** |
   | private `@media` blocks | 52 | **19** |
   | bytes inside the `<style>` blocks | – | 67 723 |

   **The old probe measured 262 inert; this one found 655, and the difference is the method.**
   `private-css.mjs` deleted one rule at a time out of a LOADED document and asked whether anything
   moved, which answers «is this rule redundant given all the others» - and inertness is not
   additive. `inert.mjs` decides by LOADING the page without the rules, tries the whole block
   first, and on a failure halves it and offers each chunk **on top of what is already proven
   safe**, so every accepted set has been tested as a set. Both readings are then taken again:
   computed style on 85 properties including `::before` and `::after`, and a PNG hash.

   **The shape of the result is the clone transform, not chance.** The states cloned from a
   coloured base give up 88-93% (`coach-order-loading` 38 of 41, `coach-orders-loading` 28 of 31),
   because `clone-to-colour.mjs` copies the base screen's whole block into a state that does not
   contain most of the elements it paints. The states with an anatomy of their own give up almost
   nothing: `coach-verify-tier` 2 of 27, `coach-home-loading` 1 of 19, `coach-home-error` 0 of 5.
   **The 499 that remain are the honest subject of this item** - what is left is either a real
   override to move into its component or one of the 210 local declarations of item 3.

   **The count nobody had re-taken was 886**, and it was deliberately never restated as a smaller
   number: it had been measured as «the system owns this class too», before the scope fix made the
   system reach these screens at all, and 655 rules had left the corpus since. Subtracting would
   have been arithmetic rather than a measurement.

   **TAKEN AGAIN 2026-08-17, BY THE SAME INSTRUMENT ASKING THE SAME QUESTION: 3.** `private.mjs`
   walks the corpus and classifies each private rule by how many homes its classes have in the
   system. Every category is empty except one: **3 rules, one home, `coach-verify.css`** - and they
   are the `.cv-card` set item 3 deferred to stage 09 by decision. There is nothing behind this item
   that is not already behind that one; the two halves of list 2 converged on a single open decision.

   **AND ONE OF THE THREE HAD NEVER DRAWN, WHICH THE SAME RUN FOUND: 3 -> 2.** With the subject line
   fixed, `inert.mjs` walks 3 pages instead of 30 and finished in 42 seconds, so it was worth asking:
   `coach-verify-loading` answered **1 of 1 removable, whole block, pixels identical**. Its rule was
   `.cv-card{ padding: 40px 24px }` and the system writes `.coach .cv-card` - (0,2,0) against
   (0,1,0), so the screen has been rendering `--space-32` since 8.40 took the anatomy over. The note
   above it read «40 is a rung, so the waiting screen's padding is a choice rather than a drift, and
   unmaking a choice is stage 09's call»; the cascade had unmade it a day earlier and nobody asked
   the browser. Deferring that rule was deferring nothing. **This is `.qa-row` from list 2 in
   mirror image** - there a private rule was blamed for winning when it never matched, here a
   private rule was preserved as a decision when it was already losing. Both times the sentence was
   plausible and the reading was not taken.

   **AND ITEM 2 DID NOT CLOSE ITSELF - IT WAS CLOSED BY ITEM 3'S SWEEP, WHICH IS WHY THE NUMBER HAD
   TO BE TAKEN RATHER THAN REASONED.** The split into «886 overriding» and «210 local» was a
   forecast of two different jobs, and the work turned out to be one: a rule copied from a component
   and a rule invented on the screen were answered by the same walk, screen by screen, and neither
   pile could be finished without the other. The forecast was useful and the boundary inside it was
   not real.

   **THE ITEM ALSO PUBLISHED A NUMBER THAT HAD STOPPED BEING TRUE: «31 screens carrying a private
   `<style>` block».** On 2026-08-17 that was still what both walks counted, and it was reading 30 -
   with 3 rules under it. Twenty-seven of those thirty declared nothing at all: 22 carried the note
   left where their rules had been, and **5 carried nothing but the blank lines the rules used to
   occupy** (`cart-coach-empty`, `coach-client-empty`, `coach-session-addclient`,
   `coach-session-addempty`, `coach-tariff-cancel`). The subject line of `private.mjs` and
   `inert.mjs` asked «is there a `<style>` element» where the question was «is there a rule»; it is
   one predicate in `lib.mjs` now, shared, and the report names the note-only pages and the blank
   shells separately so neither can hide inside the other again. The five shells were given the note
   their twenty-two siblings carry, each destination read out of the system by name.

   **AND WRITING ONE OF THOSE FIVE NOTES FOUND WHERE A MIGRATION HAD MOVED DEAD CODE INTO THE
   SYSTEM.** Step 8.31b lifted `#wf-bar{ position: relative; z-index: 80 }` off the two
   `coach-session-add*` screens into `design/_stand.css`, with nine lines on why the stand bar must
   stay above `overlay.css`'s scrim at 55 - «the bar that says WHICH SCREEN YOU ARE LOOKING AT
   disappears under the dimming on exactly the screens whose subject is the modal». The reasoning is
   correct about the grey layer. On the coloured layer the bar has been **`display: none`** since
   2026-08-06, hidden by that same file four lines below the rule, because `.uiv-side` replaced it
   as this layer's chrome; measured 2026-08-17, `getComputedStyle` gives `display: none` and height
   0 on every screen tried. Five more rules were painting it, and `body:has(.cart-drawer) .wf-bar{
   position: relative; z-index: 80 }` was in addition a **verbatim repeat** of the rule above it.
   All six are gone; `display: none !important` stays as the file's one statement about the bar,
   `!important` because the markup writes `id="wf-bar"` and an id outranks a class.

   **A migration that verifies the DESTINATION and never asks whether the SUBJECT renders will move
   dead code into the system and give it a home** - and every gate passes, because the rule is real,
   the values are tokens and the component is the right one. `--text-oninverse`'s use list in
   `tokens.css` lost `.wf-bar` along with the paint.

   **`tree-diff` reported «зрушило елементів 39» on every design page for that deletion, and the
   screen did not move by a pixel.** It reads computed style, and `querySelectorAll('*')` sees a
   hidden element. The arithmetic closes it exactly: the bar's subtree is 13 elements on
   `coach-home`, 12 on `index`, 11 on `coach-tariff-cancel`, times the three rows the tool reads per
   element - 39, 36 and 33, the numbers reported, with no differing row outside that subtree. The
   limit is written into the instrument now, because the next person to delete a rule off a hidden
   element will read the same alarming number.
3. **Decide the 210 local declarations**: a component each, or a deletion each. **CLOSED 2026-08-16,
   except three rules deferred to stage 09 by decision.**
   `tools/private.mjs` splits what is left by how many homes the class has in the system, and the
   first pile - «one home, so every difference is a gap or a resurrection» - is being walked rule by
   rule.

   | | at the start | now |
   |---|---|---|
   | private rules on the coloured screens | 468 | **2** |
   | of them with exactly one home | 157 | **2** |
   | screens still carrying rules (of 88) | 31 | **2** |
   | private `@media` blocks | 19 | **0** |
   | components in the system | 82 | **84** |

   **TWO OF THOSE CELLS DID NOT SURVIVE RE-MEASUREMENT, AND THAT IS ITEM 8'S SUBJECT.** «29 screens»
   and «4 media blocks» were both taken before the last batches of pile 1 and never re-taken: counted
   again on 2026-08-16, the screens were 23 going into this batch and the media blocks **1**. Neither
   was ever a wrong measurement, and both were a wrong published number - which is exactly the class
   `tools/inventory.mjs` was built for and the class it does not yet reach.

   **PILE 1 IS CLOSED, AND SO IS EVERYTHING ELSE.** The two that remained were
   `.cv-card{ max-width: 560px; margin: 0 auto }` on `coach-verify-error` and `coach-verify-deadend`,
   deferred to stage 09 on purpose - and taken on 2026-08-17 instead, once the third screen carrying
   the same class was measured. A third rule stood beside them until that day,
   `.cv-card{ padding: 40px 24px }` on `coach-verify-loading`, and it was not a deferred decision at
   all - it lost to `.coach .cv-card` on specificity and had never drawn. See item 2 for both.

   **PILE 2, FIRST BATCH: THE SKELETON. 20 rules on 5 screens to zero, and no declaration replaced
   them.** `skeleton.css` opens with a census - the bar «written SIX times in FOUR files» - taken at
   7.28 over 40 colour screens. The coach flow entered colour at 7.95, so the census never saw it.
   Counted again over the five coach loading screens, the bar is written **nine** times:

   | | height | radius | ground | where |
   |---|---|---|---|---|
   | `.sk-line` | 13px | 6px | `--bg-sunken` | `coach-client-loading` |
   | `.skln` | 12px | 6px | `--bg-rule` | `coach-order-loading`, `coach-orders-loading` |
   | `.qa-skel .sk` | 12px | 6px | `--bg-rule` | `coach-session-loading` |
   | `.skline` | **10px** | **4px** | **`--bg-sunken`** | the system, and 78 of 84 bars |

   **`--bg-rule` is `--warm-200`, which is `--line-hair`**, so three coach screens were painting a
   skeleton bar in the hairline colour while every other skeleton in the product paints `--warm-100`.
   Measured by `tree-diff`: `rgb(233, 231, 226)` -> `rgb(242, 240, 237)` on 21 bars. The file states
   the answer in one line: «the ground of a skeleton bar is `--bg-sunken` and is stated once».

   **And the rows around the bars were the real component, retyped.** Not a family resemblance -
   the same declarations, side by side: `.sk-row` = `.oc-item` (40 / 1fr, gap 11, hairline),
   `.sk-ph` = `.oc-ph` (40 square, radius 7), `.sk-top` = `.oc-top`, `.sk-actions` = `.oc-actions`
   (gap 9, margin-top 14), `.qa-skel` = `.qa-row` (44 / 1fr, gap 11, padding 9/0), `.skhd` =
   `.ccard-hd`, `.skav` = `.ccard-av`, `.skacts` = `.ccard-acts`, `.skclist` = `.clist` (same grid,
   same 640 flip, same gap), `.skgrp` = `.od-grp`.

   So the answer is not «move these into a component». It is 7.68's rule read to the end - **a
   skeleton is a promise about the box that replaces it, and the only promise that cannot drift is
   the box itself.** The markup on those five screens now wears `.ocard`, `.oc-item`, `.oc-ph`,
   `.ccard`, `.ccard-hd`, `.ccard-acts`, `.clist`, `.od-grp`, `.ord`, `.qa-row` - the real thing,
   standing empty, with `.skline` inside. `.skpulse` was already this shape and says so in the file:
   «the container is usually a real component».

   **Exactly two declarations were added, and both were written twice already.** The button
   placeholder (`.sk-btn` 38 x 120 and `.skacts i` 38 with `flex: 1`), both 2px short of the
   `btn--s` they stand for, whose `min-height` is `--size-40`; and `.load-note`, the sentence under a
   skeleton. Plus three rules that read the anatomy the way 8.30's two do: what `.od-grp` and `.ord`
   need when they hold bars instead of their real children, and what a bar needs in a flex row or an
   `auto` grid track - the percentage-of-nothing defect 8.30 measured in the account rail, in its
   second habitat.

   **One of those three rules was written from one screen's markup, and the corpus refused it.** The
   holder rule read `.od-grp:not(:has(.od-grp-h)), .ord:not(:has(.ord-body))` for one draft. Both
   halves of the `.ord` clause were wrong: `coach-cabinet.css` gives `.coach .ord` a padding of its
   own, so it answered a question nobody had asked - and only `account-orders` writes `.ord-body`, so
   the guard would have matched the four **real** order cards on `coach-orders.html`. Same family as
   item 1's `class="btn"` whole-string match. Removed with the reading beside it, and `tree-diff`
   proves the removal: **the six loaded screens these skeletons stand in front of moved 0 elements at
   both widths.**

   **This completes 8.24 rather than reversing it.** That step took `coach-clients-loading` off a
   third skeleton-card name and onto `.skcard`; the same reading one step further says it is not a
   skeleton card at all, it is the client card standing empty. `.skcard` keeps the catalogue and the
   account, where the box that replaces it is a `.pcard` - **naming those is stage 09's, not a move
   this step has measured.**

   **PILE 2, SECOND BATCH: THE ERROR BOX AND THE EMPTY BOX. 26 rules on 7 screens to zero.**
   `empty-state.css` has said since 7.28 that empty and broken are different things, in a sentence on
   its own stand page: «нічого не знайдено» is the result of a person's choice and leads forward,
   «не вдалося завантажити» is our breakage and leads back. **No screen in the coach flow had read
   it.** `.errbox` was worn by exactly ONE screen in the product - `account-error` - and
   `--line-danger-soft` had exactly one use in the whole system: that box's border.

   | box | edge | radius | padding | glyph |
   |---|---|---|---|---|
   | `.err` · `coach-client-error` | 1px solid `--line-strong` | 14 | 40/22 | 32px on `--mark-disabled` |
   | `.cerr` · `coach-home-error` | 1.5px dashed `--line-strong` | 16 | 44/26 | a 60px ring, `--text-secondary` |
   | `.ord-err` · `coach-order-error`, `-orders-` | 1px `--line-hair` | 14 | 44/24 | **none** |
   | `.empty` · `coach-clients-error` | – | – | – | the catalogue's illustrated nothing-here-yet plate |
   | **`.errbox`** | **1px `--line-danger-soft`** | **`--radius-12`** | **48/24** | **`--text-danger`, 34** |

   **Not one of the four carried the danger role** - not a different red, no red at all. Flat grey,
   and on `coach-client-error` the warning glyph sat on `--mark-disabled`, the ink for something
   switched OFF. This is the primary audience in the worst minute of its journey, and the product was
   saying nothing about what had happened.

   With them went what those screens drew by hand: `.err-btns .btn{ padding: 13px 24px }`, a size on
   top of `btn--s` and the same deletion 8.30 made for `.cs-empty .btn`; `.ord-empty, .ord-err` in
   **one selector**, one plate doing two opposite jobs; and `p.lead` on the two client screens, which
   is `.es` under another name carrying **42ch on one screen and 46ch on the other**.

   **And the measure was missing from the box, which is why both screens grew it.** `.emptybox .es`
   is capped at 440 and `.errbox .es` at 420; `.empty .es` had no cap at all, so on a wide column its
   body ran the full width of its holder. Measured at 1280: **660px on `coach-clients-empty`, 726 on
   `listing-error`, 818 on `coach-client-empty`, 982 on `product-error`** - up to a hundred characters
   a line. One declaration, 440, the number the sibling box already carries. `tree-diff` says it moves
   **exactly one element on each of those four screens and nothing at 390**, where the box is
   narrower than 440 anyway.

   **Two screens were given the `⚠️` glyph, and that is a look decision rather than a measurement.**
   Every other error box in the product carries one, and a box the component paints `--text-danger`
   for is a box that expects one. It also keeps 8.15's rule honest: `.errbox` is deliberately NOT in
   the `:first-child` selector, because after this step no error box lacks a glyph, and a selector
   that covers nothing is the same noise as an empty exemption. The reasoning is written at that rule.

   **PILE 2, THIRD BATCH: THE COACH VERIFY FLOW. 32 rules on 4 screens to zero, and the deferred
   three are all that is left of it.** Every name in those blocks was prefixed `cv-` - this file's own
   anatomy, carried by the four state screens while the base carried none of it. Four things it cost,
   and not one is tidiness:

   1. **`.lead` is `.cv-lead` with one word missing.** `coach-verify.css` has declared `.coach
      .cv-lead` since the split - `--fs-14`, `--text-secondary`, `margin: --space-8 0 22px`. Three
      state screens wrote `class="lead"`, a name one word away from the one that would have inherited
      it, so each grew its own: **14px / 13.5px / 13.5px**, three margins, two measures.
      `coach-verify-tier`, which does write `cv-lead`, is the control: it never needed a rule.
   2. **`.cv-lead` had no measure**, so `coach-verify-tier` rendered it **828px wide at 1280** - 93
      characters on one line, on a shipped screen. Same finding as `.empty .es` last batch, same
      answer: 440, the number the product already carries twice. The base measures 444 in its own
      column and moves 4px.
   3. **`h1.cv-h1` at 24/800 Inter.** The heading rank of this flow is the display face at `--fs-30`,
      declared for `.cv-body h1` and `.cv-card h1` - and tier's h1 is inside neither, so the selector
      never reached it and the screen typed its own. **The fourth heading with two faces for one
      rank**, after the cabinet, `coach-orders` and this flow's own states.
   4. **A bare `.btn{ display: block }` on two screens**, third occurrence of the shape after
      `.tier-cta` at 8.29 and `.cs-empty .btn` at 8.30.

   **And the first draft of that fourth line was wrong, which is why it is worth its own paragraph.**
   It said the rule turned the main action's `btn--full` back into a block. The browser said no:
   `cv-cta` is written `btn--accent btn--l btn--full` with **no bare `btn`**, so the rule never
   reached it. Measured on `coach-verify-error` at 1280 before and after, it reached the SECONDARY
   action, and there it was doing real work - the outline button was **510 x 40 with `margin-top:
   11px`**, and the atom alone makes it **203 x 40 with no margin**: a natural-width control glued to
   the bottom edge of a full-width one. So the answer was not to keep a bare `.btn`. Two stacked
   actions are a GROUP, this file already had the class for it (`.cv-actions`, on the dead end), and
   the error screen simply was not wearing it. **A claim that survives one screen and dies on the
   next is what a browser is for.**

   **`text-align: center` came out of the deferred set on purpose.** What stays private is three
   rules - `.cv-card{ max-width: 560px }` twice and `{ padding: 40px 24px }` once - because which of
   the card's answers becomes the panel's is a decision. Centring is not part of that question: it
   splits the four screens on **what they hold** rather than on a value. The waiting screen and the
   dead end centre one message; the error screen keeps its text left because it carries a LIST. So it
   is `.cv-card.mid` in the component, and everything that follows from centring - the disc, the ring,
   the lead, the sub, the note, the action column's 340 cap - follows from it in one place.

   **PILE 2, LAST BATCH: 25 rules on 8 screens, and the pile is closed.** What was left after the
   verify flow was the same three atoms retyped a third time, plus four button paddings.

   **The card, typed out by hand on two screens.** `account-shell.css` declares `.acard` (1px
   `--line-hair`, `--space-16`) and its colour block adds `--radius-12`, `--bg-page` and
   `--elevation-1`. `coach-home-free` and `coach-home-empty` wrote radius **14** and neither the plate
   nor the shadow, and they win on load order. `.ah`, `.ah h3` and `.ah a` are declared there too, and
   all three had drifted: gap 10 against `--space-12`, the caps label on `--text-secondary` against
   `--text-muted`, the link at 12.5 against `--fs-14`. **This is 8.29's `plan-card.css` finding a
   second time, in the same flow** - «THE FACE WAS `.acard`'S, TYPED OUT BY HAND».

   **The pill, twice.** `.cord .co-status` on `coach-home-free` overrode `status-pill.css`'s own
   `.co-status` at (0,2,0) - an 11px/800 pill on `--line-strong` where the atom draws `--fs-12` on
   `--space-4/--space-12` - and the override also cut it off from its `.ok` state. The word on that
   screen is «Доставлено», so the markup takes `ok`: status-pill.css settled at 7.96 that this half is
   the markup's and no stylesheet can write it. And `.cl-oostag` on `coach-session-oos` was
   `.ci-oostag` under another name; the system draws that tag in mono at `--fs-10` on `--bg-sunken`,
   with the reason written beside it, and the copy was 11px/800 Inter on nothing.

   **The empty box, a seventh time - and 8.30 had already removed the sixth from this screen's
   sibling.** `coach-session-newclient` carried the same block `coach-session-empty` lost, in the same
   flow, written in element names (`.ico`, `h3`, `p`) exactly as `coach-client-empty` was. A rename
   map executed on one screen and not on the next, which is the mechanism this item has found in five
   separate places now.

   **Four button paddings, and two of them the system had deleted BY NAME.**
   `coach-clients.css:411` reads «`.cc-cta .btn{ padding: 13px 22px }` DELETED - button.css owns the
   padding», and `coach-client-loading` was still carrying that exact rule. `:332` reads
   «`.ccard-acts .btn` KEPT ONLY ITS `flex: 1`», and `coach-clients-cap` was still carrying all three
   declarations. The third, `.cli .cgo-btn`, declared three things the system already declares in
   three different files - `margin-left: auto` at coach-cabinet.css:477, `white-space: nowrap` at
   button.css:116, and the padding at the atom.

   ### And one fix was measured, found to be a regression, and reverted the same step

   The empty box sets `.ei{ font-size: 0 }` and shows the SVG `marks.js` puts there. The private box
   had no such rule, so its `🛒` was drawn by the font - and `marks.js` does not map `🛒`, `UIV_EMOJI`
   in `design/_nav.js` does. So the moment the component took over, the icon slot measured **259 x 0**:
   the mark was not faint, it was ABSENT. Same seam as `🗑` at 7.13 and `📦` at 8.1, both recorded in
   that file, so the obvious answer was a third row.

   **It cost two shipped buyer screens.** `.ei` is also the slot `design/_nav.js:1566` fills with the
   mascot on `cart-empty` and `cart-coach-empty`, and `'🛒':'cart'` replaced the mascot with a cart
   outline on both - caught by reading the element's own `innerHTML` against the reference tree, which
   is what `tree-diff` counts as an element-count change and nothing else would have shown. Reverted;
   the box takes `📦`, the glyph its three sibling empty states already use. **A third route writing
   into the same element is why a two-map seam cannot always be closed by a row in the second map.**

   **Four of the eight loading screens were not breathing.** `.skpulse` has been in the system since
   7.28 with its own `prefers-reduced-motion` answer, and `coach-client-loading`,
   `coach-order-loading`, `coach-orders-loading` and `coach-session-loading` simply never got it -
   `coach-session-loading` said so in its own comment, «static, motion deferred». All eight now.

   **And one class was dead from the other end:** `.skel` on `coach-orders-loading` is declared by
   nothing, in this layer or the grey one. List 3 asks «which class does no markup wear»; this is the
   mirror question, and no instrument here asks it.

   **Every rule closed so far was a state re-opening a decision its base had already taken**, and
   after 109 of them that is the mechanism rather than a pattern: `clone-to-colour.mjs` reads
   `wireframes/`, so a cloned state arrives carrying the grey layer's answers to questions the
   coloured base had already answered differently. The seven `coach-session-*` screens were the
   clearest case - 31 rules, and `coach-session.css` and `stepper.css` had DELETED all four of them
   with the reason written beside each deletion. One was a live defect on all seven: a
   `margin-bottom: 76px` meant as clearance for the sticky bar, put on an element that is not last
   on the page, so it drew a 76px hole in the middle of the screen instead. Measured 96px between
   `.cs-panel` and `.cs-summ` on the states against 20px on the base; 20px on all of them now.

   **The biggest entry in the pile was not a rule, it was a block.** 48 private rules on the four
   `coach-clients-*` screens were `account-shell.css` retyped by hand - a decision taken and written
   down at 7.95 and only partly executed. It had drifted on five properties (`.acc` gap 20/28
   against 24/32, `.acc-prof` 15/14/13 against 16/12/12, `.acc-tier` 11px against 12, `.acc-link`
   13/15 against 8/16, and `1fr` where the system writes `minmax(0, 1fr)`) **and it cancelled the
   phone pattern outright**: the copy's `.acc-link{ width: 100% }` outranks the system's chip strip
   below 960 by document order, so four cabinet screens showed ONE full-width row where every other
   cabinet screen shows a scrollable strip of chips. Deleted; `account-shell.css` draws it now.

   **This is the change June measured, called a regression and reverted** - «на 390 рядок рейки
   став помаранчевим», recorded above in item 2's own history. That orange is the current chip
   taking `--bg-action`, which is what `coach-home` and `account` have been showing all along. The
   revert was right for its instruments and wrong about the screen.

   **Three exceptions to the mechanism so far, and each is worth its own line.** One was the system
   being wrong (`.qadd-res`). One a genuine gap (`.cnew .cn-tx`). And one - `.cv-card` - was the
   system being right about the screen it was written for and wrong about three it reached, which is
   the expensive kind: it laid the three coach-verify state screens out **in a row** on every
   desktop, and the private rules were the only thing keeping them survivable at 390. Photographed
   before and after; the record is in `docs/decisions.md`, step 6 eighth pass.

   **One rule was dead in a way nothing in `tools/` could see.** A `margin-bottom: 4px` adjacent to
   a `margin-top: 18px` collapses, and the larger wins - the measured gap is 18px with the rule and
   18px without it, while computed `margin-bottom` reads 4px either way. Every instrument here
   compares computed style, so all of them called it alive. What told the difference was reading the
   DISTANCE between two elements; that check is `tools/gap.mjs` now.

   **The `.cnew` card answered differently on its two screens, and that is the case against a
   blanket sweep.** `coach-cabinet.css` had ended 7.98 with a note to whoever came next - «Whoever
   confirms the deletion of the block deletes these eight with it» - after removing the same card
   from `coach-home` for carrying the rail button's words and href. Measured at 390:
   `coach-home-free` said «Нова сесія» to `coach-session.html`, 515px under a rail button saying
   «Нова сесія» to `coach-session.html`, so it went, and accent fills on that screen went 4 -> 3.
   `coach-home-empty` says «Зібрати першу сесію» and carries the sentence 7.98 itself named as the
   empty state's job, so it stayed and its six structure rules moved into the component. **Open, and
   the owner's:** that screen still points two accent fills at one destination with different words.
   Principle 2 says one clear next step; which of the two carries it is a look, not a measurement.

   **The largest single finding was a rename map executed on a base and never carried to its
   states.** `coach-tariff.css` opens with nine numbered items - «TWELVE RULES DELETED, ONE CUT TO A
   SINGLE DECLARATION, ONE MARKUP OVERRIDE REMOVED, TWO RULES WRITTEN» - each with a real reading at
   four widths. `coach-tariff.html` carries it and has an empty `<style>` block. Its two states had
   none of it: the H1's inline override, the bare `.tf-lead`, `.tf-badge` instead of the status pill,
   `.tfov` / `.tfdlg` instead of the system's scrim and dialog, and - item 9 - **`btn--accent` on
   «Скасувати підписку»**, the orange fill on the control that ends a paid subscription, where
   `button.css` settled at 7.61 that «the destructive control is the OUTLINE carrying the danger
   ink». Executing the map reproduced its measurements to the hundredth of a pixel on a screen it was
   never taken on: the dialog «before 350 x 357.53, radius 14 -> after 342 x 428.34, radius 12»
   arrived as 342 x 428.344, radius 12. `coach-tariff-cancel`'s `<style>` block is now empty.

   **The same map turned up on a second flow, and with it a control class the instruments cannot
   see.** `coach-orders`'s three states still wore a bare `<h1>` and a bare `.sub` where 7.96 had
   moved the base to `.acc-h1` and `.sub acc-sub` - the title went Inter 22/800 to Oswald 30/600, the
   face every other cabinet screen wears. And `coach-home-free`'s order rows carried
   `<a href="coach-order.html">Деталі</a>` with **no class at all**, dressed by a private
   `text-decoration: underline` at 12px, where the base draws `btn--outline btn--s` and
   `btn--accent btn--s`. That is item 1's family, and `btn-rank.mjs` walks past it: it finds a control
   «wearing `btn` without a rank», because that is the shape the transform produces, and a control
   wearing NOTHING never enters its subject. **The transform's output got an instrument; its input did
   not.** Written into `tools/README.md` beside the tool rather than fixed by guessing which anchors
   are prose links.

   **A THIRD flow was found wearing two title faces for one rank**, after the cabinet and
   `coach-orders`: `coach-verify`'s base draws its heading Oswald 30/600 inside `.cv-body` and its
   three state screens drew theirs Inter 22/800 inside `.cv-card`. Joined rather than deleted -
   `.cv-card` is not `.cv-body`, so a deletion would have left the three with nothing.

   **And the instrument itself had been handing out wrong homes.** `private.mjs` read every class in
   a selector, so `.loy .lrung.now` registered `.now` as `loyalty-rung.css`'s and a private
   `.cv-steps2 li.now` - a verification checklist - came back as «one home: loyalty-rung.css», which
   reads as an instruction to move it there. Within one compound only the FIRST class names the
   thing now. The reclassification moves rules **both ways** - two files lose a spurious entry, four
   gain real ones - which is what says the old reading was wrong rather than merely loose. None of
   the twelve earlier passes rested on a modifier match; that was checked, not assumed.

   **A third name for the skeleton card, and an anatomy nobody had answered.** `coach-clients-loading`
   drew its skeletons as `.skccard` - radius 14, its own `.skline` tone and radius, its own
   45/68/88 widths against the system's 50/72/90 - while `listing-loading` and `account-loading` wear
   `skeleton.css`'s `.skcard` with no private rule at all. Renamed in the markup, five rules gone.
   And the reason both coach loading screens had grown the same private `padding: 15px 16px` is that
   `.skcard` puts its padding on `.skb` and these two have no `.skb`: the box had no inside. **The
   same number appearing on two screens is a component's answer, not a screen's** -
   `.skcard:not(:has(.skb)){ padding: var(--space-16) }`, and the two screens that DO wrap in `.skb`
   measured 0 moved, which is the whole risk of that guard.

   **The empty state was written in element names on one screen and oversized on another.**
   `empty-state.css` draws its box with `.ei` `.et` `.es` `.eact` and five coloured screens write
   them; `coach-client-empty` wrote `.ic` / `<h3>` / `<p>`, so its whole look came from five private
   rules - including a **solid** `--line-hair` edge where the system draws a **dashed**
   `--line-strong` one, which is the difference between a panel and a placeholder. And
   `coach-home-empty` had shrunk the full-page `.emptybox` privately to fit inside `.acard`, where
   the component already offers **`.emptybox.mini`** and `account-empty` takes it for exactly that
   reason. Both fixed in the markup; ten private rules gone, and `coach-client-empty`'s `<style>`
   block is now empty.

   **At the tail of the pile the verdict starts being true about a NAME and wrong about the object,
   and the instrument was not printing its grounds.** «One home» means the class is declared in
   exactly one component - but `buy-box.css` declares `.bb .tier`, a wholesale-price badge inside the
   buy box, while `coach-verify-tier`'s private `.tier` is a whole plan card. **Three of the last
   eight homes were that shape** (`.bb .tier`, `.acc-prof .av` against a skeleton's `.sk-prof .av`,
   `.goalcta .hint` against `.cs-empty .hint`). `private.mjs` now prints the selector that produced
   each home, so a wrong one reads as wrong in a glance. Same repair `tree-diff` got two passes
   earlier: **do not soften the verdict, show what it rests on.**

   **`.ctab.add` was the one rule held back «for a look», and reading answered it.** The note said
   the tab has a bottom edge the base lacks AND a different height, 44 against 90, so it might be a
   deliberate «a tab with nothing under it». `coach-session.css` declares **both** `.coach .ctab` -
   three lines, `min-width: 118px`, `border-bottom: 0`, because the tab joins the panel below it -
   and `.coach .ctab.add`, one row, `min-width: 0`, dashed. So 44 against 90 is the difference
   between the add-tab and a client tab, not between two editions of one tab, and the bottom edge was
   the private rule redrawing `.ctab` from scratch without knowing the system removes it. **The look
   would have seen a tab that looks fine; what answered it was two rules read side by side.**

   **The first component born out of the private blocks.** `.upsell` on `coach-clients-cap` and
   `.tf-upsell` on `coach-tariff-free` were the same panel under two names, and the second one cited
   the first in its own comment - «same shape and same reasoning as `.upsell` on
   coach-clients-cap.html». Two files agreeing in prose while disagreeing in numbers is the
   definition of a component nobody wrote. `upsell.css` is that component, with the full five: css,
   `design/kit/upsell.html`, a row in the stand registry, a line in `inventory.md`, and an
   `@import` into the level-3 coach group. Three values settled (padding 18, ground `--bg-surface`,
   heading `--fs-16`), none moving more than 2px.

   **The second component, and its copy cited the atom by name before retyping it.** `.tier` on
   `coach-verify-tier` and `.tf-col` on the two `coach-tariff` screens are the Free / Pro card in two
   editions: same job, agreeing in prose, disagreeing in every number. Both had also typed out the
   FACE of `.acard` - grand, radius, ground, lift, padding - and `.tier`'s own comment NAMED the
   atom in the sentence above the copy. Read off the running pages at 390 the two were identical on
   all five properties, which makes it re-derivation with the source cited: every value right, every
   token right, and the next change to the product's card reaching one of the two.
   `plan-card.css` is the component; the markup wears `acard` on all three screens; the five
   declarations are gone. **`.tier.pro` and `.tf-col.on` stayed two states on purpose** - one says
   «we recommend this» (accent, an invitation), the other «you are on this» (ink, a statement about
   the account) - and folding them would have put one screen's meaning on the other's.
   Four defects came out with it, all measured: the choose screen's price was set in **Inter** where
   the same figures on the tariff screen are mono (`.tier-price` joins price.css's list, **and so
   does `.tf-price2`, which is my own miss from 8.28** - that pass took the rank and the air and did
   not follow the family); `.tier-flag` was the fifth edition of badge.css's pill and stood 3px
   taller than `.tf-mini` because the copy omitted the one declaration it never wrote, `line-height`;
   the list ticks were typed into the markup as `<span class="m">✓</span>`; and `.tier-cta`'s
   `display: block` had been quietly turning `.btn--full`'s flex box back into a block. Two lists on
   ONE screen had been drawing the same sentence two ways - a tick on body ink above, a middle dot
   on secondary below - and now draw it once. `tree-diff` accounts for all 33 moved elements on each
   tariff screen and 4 renamed rows.

   **The last four rules of pile 1 were four different files, and none of them was a name collision
   after all.** Every one had a real home the census had pointed at from the wrong angle.

   `.sk-prof` on `coach-home-loading` was not «account-shell's `.av`» - it was **15 rules retyping
   the buyer's loading screen**, which carries none. And the skeleton it drew was a promise about a
   screen that no longer exists: 5 stacked rail rows where the phone draws a 48-tall chip strip, a
   48 strip against the real 145.17, an 86px CTA for a block deleted at 7.98, 2 side-by-side boxes
   against 3 full-width cards, and `.skpulse` on no element of the page while every buyer skeleton
   breathes. Its `.sk-spin` said «this was the third edition» in its own comment and never asked the
   next question; skeleton.css answers it - «one slow breath, NEVER A SPINNER».

   **Then the photograph found what every number had passed.** The rewritten screen matched its twin
   on every measurement and the picture was empty - a blank profile card and seven empty pills, and
   the twin had been shipping that at 390 all along. These bars are PERCENTAGES and below 960 their
   containers size to them: `.acc-prof .who` is a flex child with nothing to stretch it, `.acc-link`
   is a chip that sizes to its content. Two declarations on the holders, the inline widths out of
   both markups (an inline style beats any rule a component can write), and `:has(.skline)` so the
   three shipped account screens read **0 moved** rather than «nothing renders differently».

   `.ac-cli` is **radio.css's fifth row-rung name** - picking a client from a list is choosing an
   option out of several - and it gained a hover, a press, a focus ring and the family's chosen
   ground, none of which it had. `.tf-incl li.off` went home beside the tick it is the opposite of,
   and settled the mark 8.29 had got wrong (`\2715`, not an en dash - the dash rule is about dashes
   in TEXT). `.cs-empty` was a sixth private `.emptybox`, the third in this pile.

   After pile 1: 111 local names with no home, 88 multi-component rules, 24 partially new, 2 with no
   class.

   **Pile 2, first block: the ordering session, 138 rules -> 48.** The seven state screens of the
   coach's session carried the largest single block left, and their base carries **no `<style>` at
   all**: the base was migrated and its states never were, the same shape as `coach-tariff` and
   `coach-orders` at four times the size. Three class attributes make the map (`.cc-repeat`,
   `.qadd-field`, `.cl-rm`, plus the field's input), applied by a transform that READS the base
   rather than by six hand edits; fifteen selectors were pure resurrection of buy-bar, client-row,
   price, discount, availability and chip.

   What the states had been drawing instead, at 390: the sticky bar's action **100.50 x 40 against
   243.30 x 64** - under the 44px touch floor, on the control the whole session leads to; «В
   наявності» in **secondary** where availability.css says `--text-success`, on the primary
   audience's trust signal; a struck price in **rgb(170,170,170)**, a grey in no palette; the
   quick-add input drawing **a box inside a box**; goal chips 33.19 tall.

   **And four instruments learned something.** `private.mjs` was reading a NOTE as a selector -
   `withNotes` grows a span over its comment and this repository writes css inside its comments, so
   two rules were misfiled and both notes were the two previous passes' own. `btn-rank.mjs` had never
   asked whether a rank AGREES with the base, and it took three keys to ask it soundly - the slot,
   not the class and not the destination - plus a split that is now a rule: **a size that disagrees
   is a defect, a finish that disagrees is a decision**, and it writes the first and only reports the
   second. Its `--apply` was a string replace that upsized a second control per page, caught by its
   own next run. And `accept.mjs` gained a `dot=` column after a photograph showed **two dots** on
   every availability line: 31 typed marks on seven screens, one of them a buyer screen shipping a
   doubled dot on twelve lines. Third time for that shape, so it is a gate now.

   **Pile 2, second block: the add-client modal, 32 rules -> 0 on the screens.** It is
   `client-dialog.css`'s EDITOR dialog under six other names, and the private copy had built the
   WRONG one of that file's two dialogs from scratch - its own 440px box at radius 14 against
   `.cemodal`'s 460 at `--radius-12`, and a close control that was a bare `<a>` at 19px with no
   hover, no focus ring and no touch target on the modal whose only way out it is. Three moves were
   genuinely new: the picker's scrolling list and its dashed launcher went into the component, and
   `#wf-bar{ z-index: 80 }` went to `design/_stand.css`, where it belongs - every state screen that
   opens a modal needs the bar to stay above a scrim at 55.

   **And the same 27 pixels the tariff dialog measured at 8.7.** «Додати першого клієнта» beside
   «Скасувати» runs 154..387 in a 360 viewport and does NOT scroll the page - the scrim clips it, so
   the right edge of the primary action is unreachable and every gate reads 0. Two dialogs, one
   measurement each, so the stacking rule is the component's now and the tariff copy is gone.

   **The stand page's idle control had been red and nothing was reading it.**
   `kit/client-dialog.html` had been printing «5 named in words, not shown in a demo» for as long as
   it existed - verified against the tree before this pass touched it. `accept.mjs` collects that
   verdict now, using the box's own words rather than a second implementation of them.

   **And the inventory turned out not to be one.** Adding `plan-card.css`'s row showed that
   `inventory.md` lists none of the eight coach components that entered at 7.95 and 8.7, files
   `upsell.css` as a molecule while its own stand page called it level 3, and carries a «Lines»
   column that is a step-5 snapshot - `tabbar.css` reads 25 there and 72 on disk. All three are
   written into the file itself, under the table. The second is the one with a rule behind it: **the
   coach group in `index.css` is a SCOPE group, not a level group**, so any file put there loses the
   one place its level was readable.
3b. **NINETEEN STAND PAGES ARE SHOWING LESS THAN THEIR FILE, and the control that says so had
   never been read.** Opened 2026-08-16 by step 8.31b.

   Every component page ends with an idle control: a box comparing the classes its css file declares
   against the classes its demos actually render, printing «Пройдено» or naming what is missing. The
   box draws in the browser. **No gate had ever collected the verdict**, so
   `kit/client-dialog.html` had been printing «5 named in words, not shown in a demo» for as long as
   it existed - and it was one of nineteen.

   `accept.mjs` reads it now, in the box's own words rather than a second implementation of the same
   check. First run over the corpus: **19 pages red, 179 classes**, and a scratchpad probe sorted
   them **70 JavaScript-written / 109 demos owed**.

   **THE SORTING WAS WRONG, AND IT WAS WRONG IN THE DIRECTION THAT FLATTERS** - step 8.32 refused to
   apply it mechanically and read the evidence line by line instead. The probe asked «does this class
   string appear in a JS file», matching `classList.add` OR `class="x"` inside a script. In this
   repository the second half means nothing: `wireframes/_nav.js` is the BUILDER of the grey
   prototype, so most of the product's markup lives inside JS string literals. Of the 26 classes on
   the seven smallest pages, 23 were `class="x"` in a template string - ordinary markup, which a
   stand can render in repose - and only three were real. The question had quietly become «where does
   this file live».

   `tools/idle.mjs` asks the narrow, falsifiable version: `classList.add|toggle|remove('x')` on an
   element that already exists. Honest census:

   | | first probe | measured |
   |---|---|---|
   | classes a script writes - a state, not a demo | 70 | **9** |
   | classes the page genuinely owes a demo | 109 | **163** |

   **All nine states are closed** (8.32). Each moved into `KIT_STS` with a sentence on its own page
   saying what writes it and why it cannot stand in repose: `dr-lock` and `pdp-stuck` are put on
   `<html>` and `<body>`, so rendering them would dress the STAND rather than the demo;
   `catov-open` lands on the bottom tab, an element that page does not contain; `hidden` is the
   cookie bar's absence; `added` is the trace of an act. `checkout-form` owes **57 of its 93** - the
   stand page for the largest organism in the system shows barely a third of its file.

   **Two more findings came out of building the instrument, both closed:**

   - **The page-side control existed in 74 hand copies, in five editions.** Three differed only in
     where a string wrapped. The fourth - `plan-card` and `upsell` - had the states clause DELETED,
     so those two pages could not have reported an unnamed state if they had one. The fifth was
     `icon.html`, and it held the one rule the other 73 had lost: **measure after the passes have
     run, never at parse time**, because `marks.js` adds `.uiv-trail` on DOMContentLoaded. Now one
     `design/kit/_idle.js`, included once per page, and it also reads into the demo frame - worth
     exactly 4 classes of 162, `toolbar`'s three and `co-wrap`, but a page red for a reason that is
     not true is what this item is about.
   - **`address-card` had two classes parked in `KIT_STS` that are not states.** `.addr-del-row` and
     `.addr-back` are both drawn on that page already; what switches them is the `hidden` ATTRIBUTE
     and `.mode-edit` on the modal, neither of which is these classes. The exemption list now has an
     idle control of its own in `idle.mjs`, narrowed to the only kind that could be parked there: a
     bare identifier the component's own css declares as a class and no script toggles.

   **First five pages closed 2026-08-16 (8.33): `filter-sheet` `overlay` `product-grid` `restock-note`
   `pdp-tabs`, 6 classes.** None of the six was a decoration. `.fsheet-ov` was missing because the
   extractor walked the panel's own tree and the scrim is its SIBLING; `.ceov` had to stand apart
   from `.wf-ov`, since two `position: fixed` scrims on `inset: 0` in one viewport read as one dark
   rectangle; `.oosbtn` had been deliberately taken OFF this page at 7.91 for being drawn under the
   wrong finish, and is now shown where the product puts it, inside `.bb .buyrow`; `.pdp` moved into
   the frame around its own strip, because the strip grows a price at 960 precisely BECAUSE `.pdp`
   moved the buy rail into a second column; and `.ptabs` turned out to be an unrelated tab strip
   sharing a file, which is a stage 09 question.

   **And the first real demo found a live defect on a shipped buyer screen.** `design/listing-list.html`,
   first card: the «★ Популярне» badge overlaps the product title by **10 x 14px at 390 and at 360**,
   clean at 1280. Under 559 `.pcard-l` becomes a `56px 1fr` grid and the photo narrows to 56, but the
   badge inside it is `position: absolute` with no width bound and is 77px wide, so it crosses into a
   text column that starts at 101; `.lph` has no `overflow`. The «✦ Новинка» badge is 13px narrower
   and misses the title BY LUCK, not by a rule. Three remedies were measured before one was chosen -
   clipping the word, ellipsing it, or the photo keeping its 84 - and all three remove the overlap
   at the same card height, 225. **DECIDED 2026-08-16, owner: the photo keeps its 84.** It is the
   only one that keeps the WORD, and it costs 43px of title width and nothing in height: the name
   wraps to two lines either way, so narrowing to 56 was buying nothing visible and paying for it
   with a collision. Written into `product-card.css` with the table.

   **Three more closed 2026-08-16 (8.34): `filter-rail` `auth-dialog` `cat-overlay`, 16 classes,
   and all sixteen were the same shape - a face of the component that does not exist in repose.**
   `.hrail` and its flyout are `display: none` below 960 and `!important`-hidden below 860, so at
   the width the listing rail is shown that whole family draws nothing. `.auth-load` `.auth-spin`
   `.lp` `.auth-note` live only in the dialog's `loading` state and `.auth-alt` only in `error` -
   the panel is rebuilt whole, not re-dressed. `.cback` `.wf-catov-all` `.wf-catov-sub` and
   `.wf-catov-goal` with `.cg` `.gn` live on the overlay's second and third levels, whose body is
   rewritten by a function.

   **None of the five new frames types markup: each one CALLS the product's own builder** -
   `wfHomeRail({open: true})`, `wfAuthGo('loading')`, `wfAuthGo('error')`, `catOverlayCat(0)`,
   `catOverlayGoals()`. A stand that retypes what a builder emits is showing its own copy.

   **And that needed a second instrument extraction.** The frame-fit script was 24 byte-identical
   inline copies, and the copy carried a limit nobody had decided: `querySelector`, singular, so a
   page could hold exactly ONE frame and a second would have sat unfitted at 150px. Now
   `design/kit/_frame.js` with `querySelectorAll`; nothing about the sizing changed. Several of the
   pages left need two or three frames, so this was the blocking piece rather than a tidy-up.

   **Two more closed 2026-08-16 (8.34b): `cookie-banner` and `cart-drawer`, 20 classes.** The
   cookie settings dialog is written by `wfCookie()` together with the bar and stays shut until
   something presses «Налаштувати», so ten classes had no resting form; the frame calls
   `wfCookie(); openCookieSettings()`. The cart's other three faces live on three other screens -
   `.cd-empty` on `cart-empty`, `.cd-oosnote` `.cd-blocked` `.cd-fix` on `cart-oos`, `.cd-group`
   `.cd-note` on `cart-coach` - and **the page BEHIND the drawer belongs to this file too**:
   `.cart-behind` `.cart-ov` `.ph-grid` `.ph-card` are the drawer's siblings, and the whole point
   of `.cart-behind` is that the cart is a drawer over a page rather than a page of its own.

   **Third instrument extraction of the day, and this one found a live class of bug.** The frames'
   boot block - the init list that makes a demo behave rather than pose - was **33 identical
   copies**. Now `design/kit/demo/_boot.js`, with two things the copies did not have:
   - **`FRAME_STATE`, called at one exact point**: after the initialisers, so the builders exist,
     and BEFORE the icon and mark passes, so markup a state builds gets its glyphs. Placed at the
     end instead, a freshly built panel stays in emoji - the defect 7.78 fixed for the toast and
     7.87 for the catalogue overlay, which would have returned by placement alone.
   - **The asset path a builder types by hand.** Four builders in `design/_nav.js` write a
     DOCUMENT-relative `src` into an element they create (`:563`, `:1228`, `:1425`, `:1566`).
     Correct on a screen that sits in `design/`; a frame sits two levels deeper, so the same string
     404s. Found by the empty cart drawing a broken image. `uivFixLinks` solves exactly this for
     `<a href>` and only for those.

   **And an open panel produced a finding bigger than any of these components.** The container of
   every open fixed panel draws **Chrome's default focus ring, `auto 1px rgb(0, 95, 204)`** - a blue
   that is in no palette. 7.85's focus trap sets `tabindex="-1"` on the panel and focuses the
   CONTAINER deliberately, «so it announces the panel's own label and does not preselect an action»;
   nobody ever declared a ring for that container, because the system declares focus per component,
   for controls. Measured on three real coloured screens: `#fsheet` on `listing.html`, `#city-dlg`
   on `index.html`, `#wf-catov` on `product.html`. **DECIDED 2026-08-16, owner: take the ring off the
   container** - `[tabindex="-1"].open:focus{ outline: none }` in `base.css`. Every control inside
   the panel keeps its own `:focus-visible`. **`.open` in that selector is the whole safety of the
   rule:** a bare `[tabindex="-1"]:focus` would have been an accessibility REGRESSION, because
   across 88 screens that attribute is worn by `div.menu-opt` on 87, `label.co-opt` on 15,
   `div.menu-list` on 15, `span.ptab` on 12, `span.vopt` on 10 - roving tabindex, the inactive
   members of composite widgets that arrow keys DO focus and that must keep their ring. None of
   them ever carries `.open`; every panel the trap focuses does.

   **Three more closed 2026-08-16 (8.34c): `header`, `system-page`, `buy-box`, 31 classes.**
   `.mega-pinned` and the scrim's `.pinned` turned out not to be runtime states at all - the only
   things that set them are the four grey `wireframes/megamenu*.html` screens, and `screens.md`
   says why: «`.mega-pinned` тримає відкритим для демо». A class whose purpose IS the demo, shown
   by the demo, is the honest reading. The cabinet menu needed two frames rather than one, because
   `.cab-lvl` is the buyer's loyalty line and `.cab-tier` the coach's plan pill, and one
   `.cab-head` answers both. `system-page` got its 404 and its status page - the first time either
   is seen under `system/index.css`, since none of the four grey screens has a coloured twin.
   `buy-box` got the coach's box from `design/product-coach.html`, nine classes that exist on that
   one screen: retail price and coach price side by side, never one instead of the other.

   **And two of `buy-box`'s eleven were not demos owed but DEAD CODE - list 3, closed by
   deletion.** `.bb .tier` and `.bb .qty` are worn by **nothing**: asked of the rendered DOM on all
   four screens that carry `.bb`, 0 matches, and the grey layer has none either. `.bb .tier` reads
   as an earlier name for the wholesale pill that `.cbtier` now carries, with different
   declarations - a second answer, not a duplicate. Quantity is counted under three other names,
   each in its own file (`.ci-qty`, `.co-qty`, `.oc-qty`), and never inside `.bb`. Deleted with the
   measurement written beside each; `--line-strong` left the file with them and left the page's
   token table the same step, which `roles.mjs` caught.

   **`.tier` is also the string that misled the first sorting probe at 8.32**, because it occurs in
   `wireframes/_nav.js` as `acc-tier` and `cab-tier`. Two instruments misled by one name in a row,
   and both times the fix was to ask the rendered page rather than the source text.

   **The last two pages, 2026-08-16 (8.36): 86 classes -> 11, and `checkout-form` is closed
   entirely.** The biggest file in the system needed eight frames, because it holds eight faces and
   TWO ADDRESSES: the checkout's own stripped chrome (`.co-head` / `.co-logo` / `.co-support` /
   `.co-foot` - no menu, no search, no catalogue, one way out and no way sideways), the logged-in
   and no-address variants, the declined screen with its «order kept» block, the processing screen,
   the profile card `.pfcard` that shares no code with any of it, and two dialogs the profile opens
   (`openProfPhone()` + `profStep('pf-phone','code')` for `.pf-resend`, `openProfDelete()` for
   `.pf-delcheck` - the only place in the product where a destructive act is gated by an explicit
   «I understand»). `account-shell` went 29 -> 11: the shell itself, its loading grid, the coach
   rail's buyer row and the wishlist heading.

   **One class turned out to be in the wrong file.** `.smeths` - the column of social sign-in
   buttons - was declared in `checkout-form.css` and appears in **no markup at all**: the only
   thing that emits it is `wfAuthPanel()`, the auth dialog. Moved to `auth-dialog.css`, both
   declarations byte for byte and each kept in its own block. It sat there unnoticed precisely
   because it has no markup: nothing could ever have pointed at it until this page was asked to
   show a class its own five screens do not contain.

   **AND THE LAST 11 ARE NOT THE ACCOUNT SHELL AT ALL - they are the grey hub's flow map, and this
   is an owner's decision.** `.wt-flow` `.wt-fh` `.wt-fnote` `.wt-screen` `.wt-sname` `.wt-st`
   `.wt-states` with `.base` `.node` `.planned` `.soon` are built by `wfTree()` and called by
   exactly one page, `wireframes/overview.html` - the prototype hub that LISTS the screens rather
   than being one, which `CLAUDE.md` excludes from the component corpus by name. That page loads
   `_wf.css` and never the system, and `_wf.css` declares the family itself. So the copy inside
   `account-shell.css` cannot be reached by anything: **136 of that file's 169 declarations that
   match no element in the product**. The precedent is on the same page - `.only-mobile` and
   `.only-desk` were the identical question and the owner closed it by deletion on 2026-08-14.
   Recommendation: delete, same as then.

   **CLOSED 2026-08-16.** The owner took the recommendation: the twelve `.wt-*` rules are deleted
   from `account-shell.css`, with the three measurements written beside them. The hub was measured
   after, not assumed - `wireframes/overview.html` still draws 6 flows, 50 screens and 141 state
   pills, with the same border, radius and pill shape, all of it from `_wf.css`. Four tokens left
   the file with the rules and left the page's token table in the same step, which `roles.mjs`
   caught: `--bg-inverse`, `--line-inverse`, `--fs-10`, `--fw-medium`.

   **Item 3b is done. 74 stand pages, 0 red; `accept` over 234 screens, 0 failures.** The debt ran
   179 classes on 19 pages -> 0, and along the way it produced three instrument extractions
   (`_idle.js`, `_frame.js`, `_boot.js`), two dead-code deletions (`.bb .tier` / `.bb .qty` and the
   `.wt-*` family), one rule that was living in the wrong file (`.smeths`), one live defect on a
   shipped buyer screen (the badge over the title) and one across the whole panel layer (the blue
   focus ring) - none of which any earlier probe could have pointed at, because they were all
   questions nobody was asking. One page at a time, each
   decided by reading - a mechanical sweep would hide real gaps behind a heuristic, which is what the
   first probe nearly did.

4. ~~**105 dead `class="dark"` in `design/*.html`** - no stylesheet in the coloured layer defines
   `.dark`.~~ **Closed 2026-08-15: 105 removed from 57 screens, and nothing moved.**

   **Measured before it was touched, and the measurement is what made it safe.** All 105 sat on
   controls that already carry a `btn--*` rank, so the word had been superseded rather than
   forgotten. `wireframes/_wf.css:583` declares `.btn.dark`; nothing under `design/` declares
   `.dark` at all - the mentions in `tokens.css`, `button.css` and `filter-sheet.css` are comments
   recording that it used to matter, and `design/kit/_page.css` has `.kp-demo.dark`, which is the
   stand's own demo canvas and not this class on a product control. No JS reads or writes it
   either: every `dark` in `design/_nav.js` and `design/system/theme.js` is the THEME mode string.

   **The fix is in two places, and one of them is the transform.** `clone-to-colour.mjs` reads
   `dark` to decide whether a cloned control starts as `btn--accent` or `btn--outline` and used to
   carry it into the result - so `dark` is INPUT to the transform and is now dropped from its
   output. The sweep of what already shipped lives in `btn-rank.mjs`, which owns button class
   attributes in `design/*.html`, **and it runs after the ranks are written**: a control arriving
   as `btn dark cs-go` is unranked, so a sweep placed before the ranking would leave the dead word
   behind and need a second run to converge.

   **The guard is what makes it a sweep and not a `sed`:** a `dark` is dead only where a rank has
   replaced it. Anything else wearing the word is listed and left alone.

   Proof: `tree-diff HEAD`, 57 pages, 114 comparisons, **0 moved, 630 rows renamed** - which is
   105 controls x 3 rows (element, `::before`, `::after`) x 2 widths, exactly. `accept` at 360 and
   390 over the 57: 0 failures.
5. ~~**`.cline.oos` dims a whole row to `opacity: .5`.**~~ **Decided and built 2026-08-15, the
   owner handing the look back.** It was a bug after all, and the measurement is what settled it.

   **The number in this entry was wrong, and the true one is worse in the place that matters.**
   Composited against the surface rather than multiplied raw, `rgb(28,28,28)` at `opacity: .5` on
   white is **3.30:1**, not 1.47 - measured in the browser on `cart-oos`, `coach-session-oos`,
   `listing` and `listing-list` at 390, all four the same figure. 3.30 clears the 3:1 non-text
   threshold and fails the 4.5:1 one that applies to what this actually is. The placeholder's 1.97
   was right and does not matter: a placeholder box is decoration.

   **One instrument was doing two jobs on two surfaces**, which is the axis this system splits
   roles by, and only one of the two has a floor:

   | | | |
   |---|---|---|
   | the photograph | decoration, no threshold | `opacity: .5` stays |
   | name and price | ink, 4.5:1 | 3.30 -> **6.84:1** via `--text-secondary` |

   > Variable: how an unavailable product is muted. Value: photograph keeps `opacity: .5`; name and
   > price take `--text-secondary`. Why: opacity changes contrast without declaring a role, and the
   > product name is exactly what a coach reads in order to choose a substitute - the last thing
   > that may go faint. `--text-secondary` is the existing muted-ink role, 265 uses, 6.84:1 on white.

   Applied to all four places that share the meaning, because one meaning may not have two
   treatments: `.ci.oos` (cart-row.css), `.pcard.dim` / `.pcard-l.dim` (product-card.css), and
   `.cline.oos`, which **moved out of the private block of `coach-session-oos.html` into
   `coach-session.css`** where it belongs - one rule off item 2's pile as well.

   **The state was never carried by the fade.** `.ci-oostag` and `.pavail.out` say «Немає в
   наявності» in words, which is what states a state; the fade is emphasis.

   **And measuring after the change caught a mistake reasoning had missed.** `color` on `.prow2` is
   INHERITED, and inheritance loses to any declaration of its own however weak - `.pnew` has one,
   so the grid card came back at 17.04:1, not muted at all, the exact opposite of the fix. The role
   had to land on the price element. The same measurement showed the old fade had also been
   dimming `.cartbtn.notify`, «Повідомити про надходження» - **the one action still possible on
   that card**. It now stands at full strength, which is right: the product is unavailable, the
   notification is not.

   Proof: `tree-diff --dir`, 88 pages, 176 comparisons, **10 moved - the 5 affected screens at both
   widths and nothing else**, every movement either `opacity 0.5 -> 1` or `rgb(28,28,28) ->
   rgb(91,91,84)`. `accept` at 360 and 390 on the five: 0.
6. ~~**`cart-coach` wears `.coach` for nothing.**~~ **Closed 2026-08-15: kept, and the check was
   wrong to call it a defect.**

   **Nothing is broken on that screen, and the answer was already written in the code.** The one
   rule that ever used the scope there was `.coach .ci:last-child`, moved to
   `.cd-group .ci:last-child` at step 7.96 with its reason recorded in `cart-drawer.css`: `.coach`
   was **not a guard**, because every coloured coach screen carries it, so the rule reached
   whatever wore `.ci` anywhere in the flow. `.cd-group` is the true guard - 2 instances on
   `cart-coach.html`, 0 on `cart.html` and `cart-oos.html`. The rule that needed a guard got a
   correct one; the namespace simply has nothing to bite on that screen today.

   **Kept, and the reasoning is asymmetric on purpose.** `cart-coach` IS a coach screen - locked
   product decision 1, the cart with per-client tagging. The scope is written from the base by
   rule, not by hand. Stripping it would leave the single coach screen without the namespace, so
   the next `.coach`-scoped selector would silently miss it - **the 23-screen defect this stage has
   already paid for once**. A namespace that catches nothing today costs one class token; a
   namespace missing from one screen costs a class of silent bugs.

   **`scope.mjs` had been failing the gate on it, and now asks the right question instead.** An
   idle namespace is reported and does not fail. What fails is the direction nobody had been
   asking: **a screen wearing a scope its base does not wear** - a screen claiming a flow it is not
   in. Currently 0.

   **AND «ONE SCREEN» IS FOUR AS OF 2026-08-17, WHICH IS THE DECISION WORKING RATHER THAN DRIFTING.**
   `cart-coach`, `cart-coach-empty`, `coach-home-error` and `coach-home-loading` now wear `.coach`
   and catch nothing with it. Nothing was added: the private rules that used the namespace on those
   screens were migrated into their components by the item 3 sweep, and a rule that moves into
   `coach-cabinet.css` no longer needs the screen to say `.coach` for it. Every screen that loses
   its last scoped selector joins this list, so the list GROWS as the work finishes - which is why
   it is reported and does not fail. The number is written here rather than left at «one», because
   a published count that quietly went stale is exactly item 8.
7. ~~**Two stale records**: `design/overview.html` says 50 coloured screens (it is 87 plus the hub)
   and the step-8.19 note says 41 grey-only screens (it is 54).~~ **Closed 2026-08-15, and the
   entry was stale about itself.** Both had already been corrected in their own files -
   `design/overview.html:283` carries «87 екранів + хаб (46 покупця, 41 тренера), перераховано
   2026-08-14 кроком 6» with the old 50 named as the number that froze at stage 07, and
   `docs/decisions.md` marks the 41 as stale beside step 6's 54. The README stage-07 row was right
   all along.

   **A third one was found in their place, and it was on no list.**
   `design/kit/docs/architecture.md` and its page said «the coloured layer is 40 screens; the grey
   prototype is 142. The 42 coach screens have no colour at all», and then put a scope decision to
   the owner on the strength of it. Both numbers were made false by this stage's own work: 87 plus
   the hub are in colour, and the coach flow has been coloured since step 7.95. The record is
   **noted rather than rewritten** - it is the question the owner answered on 11 August, and the
   answer does not read without it - but a reader arriving today would have taken a settled
   decision for an open one.

   **What the pattern is worth saying out loud:** a list of stale records goes stale. Two entries
   were fixed at the point of the fix and the list never heard, while the record that nobody had
   listed kept its numbers for four days. The lesson is the one already written above about
   `.badge` at 8.19 - a later step makes a number false and the page never hears - and the only
   defence that scales is asking the OUTPUT rather than keeping a list: the count of coloured
   screens has one measurable source, and any page stating a different one is stale by
   construction. That check does not exist yet; it is item 8.
8. **No instrument asks whether a published number is still true.** Every count on the stand was
   right when it was written and nothing re-asks. Three have been caught by hand in two days (50,
   41, 40/42), each by someone reading the page for another reason. The shape of the check is known
   from the ones that already work: take the claim from the page, take the number from the corpus,
   and fail when they differ - `grey-vars.mjs` and `vars.mjs` already do exactly this for values.

   **FIRST INSTRUMENT BUILT 2026-08-16: `tools/inventory.mjs`,** and its subject is the registry of
   the component layer, `inventory.md`. Six questions: coverage both ways, `Lines` against disk, the
   level table against the level the component declares about itself, the totals paragraph against
   the tables it summarises, and - behind `--screens` - the `Screens` column, asked of the rendered
   DOM over all 88 coloured screens.

   | first run | |
   |---|---|
   | components on disk / rows in the tables | **84 / 73** |
   | rows pointing at a file that no longer exists | 2 |
   | `Lines` cells drifted | **66 of 73** |
   | `Screens` cells drifted | **58 of 73** |
   | totals paragraph | «70 components: 22/27/21» against 73 (22/28/23) in its own tables |

   **The `Screens` column could never have been right:** it was a markup scan, and a third of this
   product's chrome is written by `wireframes/_nav.js` at load, so it read `footer.css` **1** where
   the footer is on **77** screens and `tabbar.css` **0** against **82**. The `**JS**` annotation it
   carried was a note beside a number saying «this is wrong and here is why». **And the file's own
   note about the gap had drifted too** - it said eight components were missing, the real number was
   thirteen. All of it is measured now and the run exits 0.

   **SECOND SUBJECT WIRED 2026-08-17, AND IT IS THE ONE A PERSON ACTUALLY READS.** The registry is
   one copy of «how big is this component»; the `kp-meta` strip on each `design/kit/<component>.html`
   is a SECOND, and the second copy is the one that drifts. Question **H** in the same instrument,
   over the 75 stand pages that name a component file:

   | | tags | wrong |
   |---|---|---|
   | `рівень` | 75 | **0** |
   | `N рядків` | 73 | **53** |
   | `N селекторів` | 51 | **40** |
   | `N оголошень` | 46 | **19** |
   | `N екранів` | 56 | **37** |
   | **total numeric tags** | **270** | **143** |

   **Level at zero is what makes the other four readable.** A check that finds everything wrong is
   as suspect as one that finds nothing; one family coming back entirely clean says the parser and
   the corpus agree, and the drift is real.

   **The vocabulary was read off the pages rather than chosen.** `loyalty-rung.html` publishes «49
   селекторів · 61 правило», and its file measures 67 selectors and exactly **61** rules - so the
   stand already distinguishes the two words, and the check keeps the distinction: a selector is one
   comma-separated member of a rule head at any depth, a rule is one block. `pdp-tabs.html` pinned
   the other two, shipping 85 lines and 102 declarations both exact against a stale selector count.
   Twenty pages had a correct line count and twenty-seven a correct declaration count, which is how
   the conventions were confirmed instead of invented.

   **Agreement travels with the number, because these tags are render text.** `button.html` shipped
   «461 рядків» where 561 wants «рядок»; `breadcrumb.html` «22 екранів» where the count wants
   «екрани». `--apply` writes the ending the new number takes: 1 -> рядок, 2-4 -> рядки, 5+ ->
   рядків, and 11-14 take the last form against their last digit.

   **AND THE FIRST `--apply` DAMAGED THIRTEEN TAGS BEFORE ANYTHING WAS COMMITTED.** The tag matcher
   anchored at the start of the string, so it read the head of a COMPOSITE claim - «3 екрани +
   значок на 14», «14 екранів, діалог на 5», «291 оголошення без елемента» - as the count it
   recognises, and the rewrite replaced the whole tag with two words. Two of the three were not even
   the same quantity: cookie-banner's 291 is declarations that match no element. Reverted, and the
   matcher now requires the tag to be EXACTLY number + noun; anything longer is a claim of its own
   and goes to «not reached». Same family as `btn-rank.mjs`'s string replace at 8.31, caught the
   same way - by looking at what the tool wrote.

   **44 numeric tags are named as out of reach rather than passed over**: 13 composite claims, and
   31 corpus counts that need a browser and a different question («106 екземплярів», «470
   лічильників», «5 291 входження»). The composite thirteen still carry whatever they carried; they
   need a person who knows what each sentence means, not a regex.

   **AND A DASH READ AS ZERO HAD ALREADY DESTROYED TWO PUBLISHED NUMBERS, 2026-08-17.** The check
   failed `product-thumb.html` within a minute of its being written: «1 екран» against a registry
   cell that reads «–». The registry was right. `inventory.mjs` parsed that cell as
   `sc ? Number(sc[0]) : 0`, deliberately - the comment beside it said «a missing one reads as 0
   rather than as unknown», which was true while every row carried a number. `product-thumb.css` is
   the row that broke it, because it declares no class of its own and the anchor walk cannot count
   its screens at all. **A limit of the instrument had been flattened into a fact about the
   product** - and `--apply` had already used that zero: `counter.html` went from «19 екранів» to
   «0», `icon.html` from «39» to «0». Both restored, both now reported as **not reached**, which is
   the honest state: nothing in this repository can confirm 19 or 39 today. An instrument that
   cannot say «I do not know» says a number instead.

   **QUESTION I, 2026-08-17: the level is written in FOUR places and only two were being checked.**
   The file declares `(level N)`; `inventory.md` puts its row in one of three tables; `index.css`
   imports it into one of three groups; `design/kit/_nav.js` files its page under one of three
   headings. C and D compared the first two. The ladder is the whole architecture of this stage - an
   atom imported after the molecules can be overridden by them, the exact inversion the order exists
   to prevent - and the last two had never been looked at.

   | file | file says | `index.css` | stand registry |
   |---|---|---|---|
   | `product-thumb.css` | 1 | **2** | 1 |
   | `menu.css` | 2 | **3** | **1** |
   | `upsell.css` | 2 | **3** | **3** |

   A mismatch with a reason written above it is not a defect: `upsell.css` carries four lines saying
   why it imports where it does, `product-thumb.css` carried nothing, so the check asks for the
   comment and reports the two kinds apart. `product-thumb.css` moved into the atom group and
   `tree-diff` over five screens at both widths found every differing row inside the hidden
   `.wf-bar` subtree and none outside it.

   **The other two are the owner's, and they are two different questions.** `menu.css` has three
   placements and three answers - file molecule, import organism, registry atom - and nothing in the
   source says which is right. `upsell.css` has two INDEPENDENT placements agreeing against the
   file: both the import and the registry call the Pro panel an organism, and only its own opening
   comment says molecule.

   Still open under this item: every other published count on the stand - the per-page «N власних
   класів», the census tables, the numbers in `architecture.html`. The shape is proven; the subjects
   are not wired yet.

9. **16 selectors in the shipped component layer have never matched anything.** Found 2026-08-17 by
   `tools/dead-sel.mjs`, built for this question because nothing here was asking it: `inert.mjs`
   asks whether a DECLARATION is overridden and is structurally blind to a selector with no element
   - there is no losing declaration to find. Two had already been caught by hand in one file, and
   the second only because the first was: `coach-order.css` lost `.od-back` at 8.7 by counting
   instances, and kept `.od-line:last-child` four lines below it until 8.52.

   2925 selectors in 84 files (2215 distinct), asked of the browser over 263 pages that load
   `system/index.css`: **16 dead**, 365 conditional with a live host, 6 born at an act (the toast,
   declared with the line that builds it), 4 this engine will not parse (`::-moz-range-*`).

   **Seven of the sixteen are one shape - a comma list completed for symmetry where only one member
   has an element.**

   | file | alive | dead beside it |
   |---|---|---|
   | `button.css` | `.btn--outline .uiv-brand` · `.btn--s .uiv-brand` | `.btn--accent` · `.btn--ghost` · `.btn--l` |
   | `field.css` | `.field-grp > .btn--accent` | `> .btn--outline` · `> .btn--ghost` |
   | `empty-state.css` | `.emptybox .et:first-child` · `.emptybox .es:last-child` | `.empty .et:first-child` · `.empty .es:last-child` |

   The last one is the sharpest, because **the file had already written the rule down four lines
   above**: «`.errbox .et` is never `:first-child` anywhere, and a selector added for it would match
   nothing. An exemption that covers nothing fails as loudly as an undeclared case; so does a rule.»
   Then it wrote two.

   The other nine are not one family and are not one decision:

   | selector | file | what it is |
   |---|---|---|
   | `.pcard.dim .pold` · `.pcard.dim .pcut` | `product-card.css` | dimmed cards exist, struck prices exist, **no card is both** |
   | `.pdp-tabs .tprice:not(:has(.told)) .tnew` | `pdp-tabs.css` | a price with no old price - the demo product always has one |
   | `.skcard:not(:has(.skb))` | `skeleton.css` | a skeleton card with no button block |
   | `.pl-hw .pl-ic:empty` | `spec-table.css` | an icon slot with nothing in it |
   | `.wfh-meta .wfh-loc .uiv-ic:last-child svg` | `header.css` | the host is not there either |
   | `.btn--stack .tl .uiv-ic svg` | `stack-action.css` | the host is not there either |
   | `.resend a` | `field.css` | the block is built by a script and holds no anchor |
   | `.coach .upsell p b` | `upsell.css` | the Pro panel's paragraph lost its bold |

   **«Dead» here means «matched nothing on these 263 pages», never «can never match»**, and the two
   kinds are repaired differently - a rule with no possible host is deleted, a combination the corpus
   never shows is a question about the demo data. The instrument reports the fact and refuses to
   guess; the reading is a person's.

   **CLOSED 2026-08-17: sixteen read, twelve deleted, four kept with a reason. `dead-sel` 0.**

   The twelve deletions each carry the reading beside the rule, and every one of them is a sentence
   about the product rather than about the selector: a brand mark names a third-party provider, and a
   provider is never the one action of a region nor a ghost (`button.css`, three); a control welded
   to the edge of a field is the one that submits it, which is an accent by rank (`field.css`, two);
   `.wfh-loc` holds one mark and the caret belongs to the language menu alone (`header.css`); `.tl`
   is the tab bar's caption and was written into a stacked control as if the two were interchangeable
   (`stack-action.css`); every `.empty` carries a glyph above the title and an action below the body,
   which is what the paragraph four lines up had already worked out (`empty-state.css`, two); the
   panel's bold is real in one of its two paragraph shapes (`upsell.css`).

   Two deletions are worth their own line. **`.resend a`** was four declarations for an anchor that
   is not there: `.resend` holds a countdown in a `<span>`, because during the count there is nothing
   to press, and the live resend link is `<a class="pf-resend">` in `.otp-note`, already drawn by
   `checkout-form.css:50`. The dead rule was that control's ninth-edition twin, one token off, **on
   markup that never existed - so no amount of looking at the screen could have found it.**
   **`.skcard:not(:has(.skb))`** was right the day it was written (8.24, for two named coach screens)
   and the corpus moved out from under it: twelve `.skcard` today, not one without a `.skb`, and on
   the two screens it was written for the word survives only in a comment. That is the case for
   sweeping the whole corpus rather than what a step touched.

   The four kept are declared in `tools/dead-sel.mjs` as `KEPT_ON_PURPOSE`, each with its reason, and
   the control there **fails in both directions**: an entry that goes alive means the case arrived
   and the note must go; an entry naming a selector no file declares means the exemption outlived its
   rule. Three are states of a SHOP the demo catalogue does not contain - a full-price product
   (`pdp-tabs.css`), an out-of-stock product that is also discounted (`product-card.css`, two).
   The fourth is the mirror of the toast: `.pl-hw .pl-ic:empty` is **killed by an act**, because the
   markup ships the slot empty and `design/_nav.js:1461` fills all three on every load, so `:empty`
   is false by the time anything is measured.

   **2925 selectors before, 2913 after, and the live count unchanged at 2534** - which is the proof
   that nothing alive was touched. Pixels: **`tree-diff --dir` over all 88 product screens at both
   widths, 176 comparisons, 0 elements moved.**

   **And the reference had to be built, because `tree-diff HEAD` could not answer this.** Asked
   against HEAD it reports 4 comparisons moved, and all four belong to earlier steps still sitting
   uncommitted in the tree - the border-ownership rewrite on `coach-order` (8.52) and the 560 on
   `coach-verify-loading` (item 2). Worse, with no page named it asks git which `design/*.html`
   changed and gets two, **neither of them affected by a stylesheet edit at all**: a component-layer
   change touches every page that loads it and no html file at all. So the reference is the working
   tree with only the ten stylesheets restored from HEAD, and `--dir` compares against that. A
   comparison whose two sides differ in more than the thing being measured is not a proof.

## List 4 - what stage 09 step 1 found and deliberately did not fix

The pattern step is a refactor, and its acceptance is zero pixels moved. Every value below is a
value the move made VISIBLE; snapping any of them is a separate decision, said out loud in the
form this project requires (variable, value, why), and none of them is taken here. Folding a value
change into a move is exactly how a refactor stops being provable.

**1. The action row is written with five gaps for one job.** `tools/pattern.mjs`: one composition,
70 grey screens and 58 coloured ones, under 53 container names. Of the sixteen that carry a rule,
the gap is `--space-12` ten times, `--space-8` four times, a bare **9px** three times and a bare
**10px** twice, `--space-16` once. **9px and 10px are on no rung** - the ladder is 4 / 8 / 12 / 16 -
and `coach-clients.css` had already written the finding into its own comment beside `.cl-acts`:
«the third number on the four action rows». After the move each deviation is one declaration in
one file, written as `.x.actions{ gap }` so that it outweighs the pattern:

| variable | today | candidate | why it is not decided here |
|---|---|---|---|
| `.coach .cl-acts` gap | 9px | `--space-8` | 1px on four coach screens, and the four rows should agree with each other first |
| `.coach .ccard-acts` gap | 9px | `--space-8` | the same 1px, on the client card |
| `.coach .oc-actions` gap | 9px | `--space-8` | the same 1px, on the order card |
| `.coach .cc-cta` gap | 10px | `--space-12` | 2px, and this row is the screen's own call to action |
| `.coach .od-acts` gap | 10px | `--space-12` | 2px on the coach order |
| `.coach .ord-acts` gap | 10px | `--space-12` | 2px on the coach cabinet |
| `.addr-acts` gap | `--space-8` | `--space-12` | on a rung already; the question is whether an address card's actions are a tighter row on purpose |
| `.aord-actions` gap | `--space-8` | `--space-12` | the same question on the order row |
| `.cshelf .cs-act` gap | `--space-8` | `--space-12` | the same question on the trust shelf |
| `.ci-links` gap | `--space-16` | `--space-12` | the only one that is WIDER than the pattern, and it holds two text links rather than two buttons |

**2. Two action rows were not converted, and both are named rather than left to be noticed.**
`.ceact` and `.cedlg .act` - the client dialog's own row, on eight grey screens and two coloured
ones. Its markup is built by `wireframes/_nav.js`, the shared script BOTH corpora load, and this
stage does not edit the grey corpus. `.sys-acts` - the service page's row, zero coloured
occurrences, so there is nothing to prove with pixels. Stage 12 rebuilds both sets of screens and
should take the pattern then.

**3. The product's page container lives in a file that is not part of the system.**
`.wf-canvas` and `.wf-page` stand on 129 and 122 grey screens and are declared in
`design/_stand.css`, which opens by saying it is «prototype and stand chrome, NOT part of the
system». `.wf-page{ max-width: 1200px; margin: 0 auto; padding: 0 16 40 }` is not chrome - it is
the product's content column, and stage 10 will need it. It was left where it is on purpose: the
file's placement is a recorded decision of step 7.26, and moving product layout out of it is an
owner's call, not a side effect of a pattern step.

**4. The system has no class that means «this is the screen's main action».**
**And two documents of this project disagree in writing.** `DESIGN.md` line 44: «One orange per
view. The accent means exactly one thing: where to press. A second filled orange button on a screen
means the screen has not decided which step is the main one». `button.css` line 20: `.btn--accent`
is «the one action of a REGION». `conventions.md` line 130 sides with the region: «Each zone has one
main action; the screen's main action is a real `<a href>` to the next». The product obeys the
region reading on every listing screen, so **the `DESIGN.md` line is the one that is stale** - and
it is the line a new reader meets first, because it sits under «The rules colour obeys».
The coloured layer has no way to check either reading. Visible `.btn--accent`
reaches 13 on one screen because every product card carries an accent cart button; the grey
layer's own primary marker `dark` reaches 5. The finish is the rank, and the rank is being asked
to mean two different things. Whether the system needs a separate `btn--primary` role, or whether
the rule belongs to zones and stays uncheckable by class, is an owner decision. Rule U8 in
`architecture.md` states the measurement; it does not invent the class.

**5. `.acc-main` is declared by no component file at all.** It stands on 43 grey and 31 coloured
screens as the second half of the account shell, beside `.acc` and `.acc-nav`, which
`account-shell.css` does declare. The same holds for `.stack`, `.navlink`, `.ctrl`, `.frange` and
`.wfh-cabbtn`. None of them is dead - `tools/dead-sel.mjs` asks the opposite question and answers
0 - they are classes the markup wears that no stylesheet in the system names.

**6. The roadmap sidebar's own text is under 4.5:1, in BOTH themes, on every page that carries it.**
`tools/theme.mjs`, measured: `a.nav-top` «Дослідження» **4.14 dark / 4.33 light** against
`rgb(242,241,238)`, 14 occurrences; `a.nav-link` «Чому саме так» the same pair; the row label
«дизайн-процес» the same; and `span.nav-badge-soon` **3.79 / 3.79**. The «light» column is what says
whose fault it is, and it says the theme is innocent: **the defect is older than the dark theme and
lives in `/_nav.css`**, the look of the root registry.

Stage 09 step 3 did not create it, but it did widen its reach by one page: giving `why.html` the
roadmap panel - which the pack requires of a roadmap item - brought a second `design/` page into the
same reading. The walk's subject is `design/`, so it reports two pages; the panel is on every stage
hub in the repository, so the real reach is every one of them.

Not fixed here for a reason that is a rule rather than a preference: `/_nav.css` is the look of the
project registry, owned by no design stage, and its ink is a value. Changing it is «variable ->
value -> why», said out loud by the owner. The measurement is what this step owed.

## List 5 - the self-sufficiency test, stage 09 step 5

The next real page of the product, assembled from the finished system and nothing else: **node 2.2,
Ціль-колекція**, base plus its three states, four files. Chosen by the owner from 54 grey screens
that have no coloured twin, on the measure the pack names - it covers **38 of the 84 components**,
more than any other candidate, and it stands in the flow between two screens that are already
coloured (`index` -> `goal` -> `listing`).

**Zero styles were written on the screen.** The four files carry no `<style>` element and no `style`
attribute; they load `system/index.css` and the stand chrome, exactly like every other product page.
Everything below is what the system could NOT do, recorded rather than drawn by hand.

| What is missing | What needed it | Which level of the system closes it | Priority |
|---|---|---|---|
| **`.gnote`** - the note under the grid that explains the default card order («в наявності first, then …») has no component and no rule anywhere in the system | `goal` - a goal collection sorts differently from a plain listing, and the screen has to say so | component (a quiet note under a block), or a variant of `seo-text` | medium - it is one paragraph on one screen today, and every goal collection will carry it |
| **`.actions` has no inline variant** | `goal-empty` and `goal-error` - the empty state's action row is `inline-flex` and centred, so it shrink-wraps under a centred block. The pattern is `flex`, which stretches | pattern variant (`.actions--inline`) | medium - it is why `.eact` was NOT converted at step 1, and it holds for every empty state in the product |
| **`.ctrl` is declared by no stylesheet** | all four screens, and `listing` before them | component or a deletion - the class is markup nobody styles | low - already recorded as item 5 of list 4, confirmed here on a new screen |

**And one thing that is NOT a system gap, named so it is not mistaken for one.** The view toggle's
«Списком» cell on `goal` is a dead `<span>`, because **`goal-list.html` does not exist in the
product at all**. That is a missing SCREEN and therefore an IA decision, recorded on the
`view-toggle` stand page since stage 08 as the single real defect of that control. The coloured copy
inherits it unchanged; drawing a «disabled» look for it here would be inventing a state, which step
7.31 already refused for the chip on the same grounds.

**The backlog is not empty, and that is the answer the step owed.** Three system gaps and one IA
gap, from a screen that a person would otherwise have called finished.

**Two more, found by the critique pass and by a positive control rather than by eye.**

| What is missing | What needed it | Which level | Priority |
|---|---|---|---|
| **`pagination.css` does not draw its own cells.** `.pages a` gets no box, no border and no size from the component; on `listing` every link was hand-dressed at stage 08 as `btn--outline btn--icon btn--s`. The new screen's pagination therefore renders as bare text, and no map can fix it: the links carry no identity class, so there is no key to read a finish from | `goal` - a goal collection is paginated | component (the pagination should style `.pages a` itself, the way `button.css` refuses to style a bare `.btn`) | **high** - it is the only finding here that makes a control invisible, and it reaches six more coloured screens |
| **The «next page» link has no accessible name.** `<a href="?page=2">›</a>` - a chevron and nothing else | `goal`, and the same markup stands in the grey layer of six other coloured screens | microcopy plus markup, owned by the grey layer | medium |

**The keyboard pass was read against a control, not in isolation, and that changed the report.** The
first reading of `goal` said «78 focusable elements with no focus ring» and a list of hit targets
under 44px. The same probe on `listing`, the accepted twin, answers **78 and the same list** - so
both belong to the shared shell and neither was introduced here. What the control DID isolate is one
delta: three unnamed focusables against the twin's two, and that third one is the pagination chevron
above. A finding that both screens share is a corpus finding; only the difference is news.

**And one that reaches far past this screen, found by the critique's positive control.**
`clone-to-colour.mjs` line 138 always appended **`btn--s`** to every control it ranked - a 40px
button - while the hand-built screens write `btn--accent btn` with no size and get `button.css`'s
own 52px. So the main action of an empty state came out **under the 44px touch target on every
screen the transform ever cloned**, and nothing said so: the button is ranked, visible and
clickable, and only a comparison with a hand-built twin shows it.

The split is clean and it is the proof: `listing-empty`, `listing-error`, `cart-empty`,
`product-oos` - built by hand - carry **0**. `account-empty` carries 5 and `coach-home-empty` 3,
both cloned. Across `design/` the invented size stands on **114 controls over 43 screens**.

The instrument is fixed (the rank is read off the grey layer's `dark`; the SIZE is not the grey
layer's to give) and the four screens of this step were corrected to match their twin. **The other
43 were not touched**: undoing a size on 114 shipped controls is a value change across the corpus,
which is «variable -> value -> why» and the owner's call, not a side effect of a step about
patterns. Every one of them is a main action that a thumb has to hit.

### Withdrawn on verification, and both were mine

The critique's design-review pass disagreed with two rows above, and the source settled it against me
both times. **A withdrawn finding stays visible with its reason, or it comes back next time in the
same words.**

**«`pagination.css` does not draw its own cells» - WITHDRAWN.** The component declares the row, the
disabled state and the fill of the current cell, and draws no box **on purpose**: the cell IS a
button, composed on the screen as `btn--outline btn--icon btn--s`, which is exactly what the accepted
twin `listing.html:184` writes. Nothing was missing from the system; classes were missing from my
screen, together with `aria-current` and a name on the chevron. Measured before the fix: cells of
9.6 x 25.6px of bare text against 40 x 40 on the twin, and Tab drawing Chrome's default blue outline
on a warm neutral ground. Fixed by a pass in `clone-to-colour.mjs`, because the finish map cannot
reach links that carry no identity class.

**«`.gnote` needs a component» - WITHDRAWN, and the opposite is true: the markup had to go.**
`badge.css` records the decision in writing - `.gnote` is a GREY-PROTOTYPE ANNOTATION, «the coloured
clone dropped it, correctly, which is why it renders on 0 of 39 coloured screens» - and the class
left the system at step 8.10. The twin keeps the same rule as an HTML comment, «kept out of the
visible UI». My screen was the fortieth, and the one that rendered it: 16px full-ink text, the
loudest block in the lower half of the page, louder than the SEO body under it. **Ordering a
component for it would have put back into the system exactly what the system had deliberately
removed.** The drop had been done by hand at 8.10, so the transform never learned it; it is a pass
now.

**And one sharpened rather than withdrawn.** The view toggle is worse than «the «Списком» cell is
dead»: on `goal` BOTH cells are `<span>` - the grey original writes them that way while
`wireframes/listing.html` writes both as `<a href>` - so the whole control is out of the Tab order
while still advertising `cursor: pointer`. It is a defect of the frozen grey layer, and it needs the
owner rather than a coloured patch.

## ~~List 6 - the one hole the dark theme opened~~ - CLOSED by the owner, stage 10 debts pass

`tools/theme.mjs` over 276 of 280 coloured pages, ink against its own ground, threshold 4.5:1.
**35 forms sit under it. 34 of them fail in the LIGHT theme too**, which means they are older than
the theme and belong to A10, the accent-on-pale decision already recorded: `#FF5A00` on a pale ground
is 3.13:1 and every `btn--accent` label inherits it.

**Exactly one form was broken BY the theme, and that is the whole value of running this check:**

| what | light | dark | where |
|---|---|---|---|
| `span.hptag` - the eyebrow of the first home promo tile, «Акція тижня» | **5.45** | **2.97** | `home-buyer` · `home-cart`, 6 instances |

The tile paints its ground with `var(--bg-page)` behind a photo (`banner.css:108`), and the eyebrow
takes `--text-action` on the first tile only (`hero.css:29`). The light pair works because the pale
ground sits under a light photo; in dark the ground inverts and the photo does not, so the ink loses
its footing while the role itself is perfectly paired. **A role can have both halves and still fail:
the pair is about the token, the contrast is about the SURFACE it lands on**, which is the third axis
stage 08 wrote down and this is its first real catch.

Not fixed there. It was a value decision in the form the project requires - variable, value, why - and
it was the owner's: either the eyebrow stops reading `--text-action` on a photo tile, or the photo
tile gets a ground of its own.

**CHOSEN: the tile gets its own ground, and the diagnosis above was wrong about the mechanism.** It
said `--bg-page` in `banner.css:108`. Measured: it is `--bg-inverse` in `hero.css:29`. «Inverse»
means «opposite of the page», so the whole family flips coherently - charcoal in light, warm-50 in
dark, with `--text-oninverse` flipping to match. The one thing that does NOT flip is
`--text-action`, the brand's orange, so the same ink met a dark ground and then a pale one.

A new semantic family, **whose two halves are deliberately equal**: `--bg-media`, `--text-onmedia`,
`--text-onmedia-muted`, `--line-media`. That is not a missing pair - both halves are written and the
equality is the decision. The argument is that a promo panel is a PICTURE surface, and the two tiles
beside this one carry actual photographs, which do not learn a theme; the strip was already half
theme-independent and the first tile was the odd one out.

Measured after: **5.45 in both themes**, ground identical, heading 17.04 in both. `theme.mjs` had
found exactly one form broken BY the theme across 276 pages, and it is now zero.


## ~~The header search collapses between the shell point and ~1010~~ - the owner chose, and it is HALF closed

**Measured on `design/index.html`, the search input's own width:**

| viewport | `.wfh-search` | the input inside it |
|---|---|---|
| 860 | 121 | **44.5** |
| 900 | 161 | 84.5 |
| 907 | 168 | 91.5 |
| 960 | 221 | 144.5 |
| 1020 | 281 | 204.5 |
| 1280 | 325 | 248.5 |

The placeholder is «Пошук товарів, брендів...» and at 44px the field shows «Пс». The row at 907 holds
logo 65 + nav 198 + search 168 + actions 408 with four gaps, 839 of the 875 available: the search is
the only item with `flex: 1` and no floor, so it absorbs every shortfall alone. It is the same shape
as the restock row's 26px e-mail field, one level up.

**Not fixed in step 4, and the reason is that both repairs are SHELL decisions.**

- **Give the search a floor and let the row wrap** (`flex: 1 1 18rem` + `flex-wrap` on `.wfh-in`).
  Measured consequence: below ~1010 the search takes its own full-width line, which grows the header
  by one row - and three sticky offsets read the header's height as a literal (`.pmini` `top: 88`,
  `.acc-nav` `top: 120`, `.mtoolbar` `top: 57`). They would all need to follow, which is what
  `--shell-top` exists for.
- **Let the action labels go and keep one line.** The four actions take 408px with their words; at
  859 they are already icons only. Deciding where the words come back is a decision about the
  action row, not about the search.

The shell's form was chosen by the owner at step 3 and its height is what three components measure
themselves against, so neither repair belongs inside a component round.

**CHOSEN: let the action labels go, keep one line.** Implemented as a CONTAINER question, not a
point, so no third breakpoint was needed: the row is what runs out of room and its width is not the
window's (capped at 1200 and padded), so a media query would have measured the wrong box.
`container-type: inline-size` on `.wfh-main`, `flex: 1 1 18rem` on the search, and at
`@container (max-width: 63rem)` the words go. The threshold is the sum the row must hold with them
in: `65 (logo) + 198 (nav) + 18rem (search floor) + 408 (actions with words) + 4 gaps`.

**The words are hidden from the EYE, not from the reader.** `display: none` would have stripped the
accessible name off four controls whose only text this is - «Увійти» is the whole name of that
button - trading a cramped field for four nameless icons. The five declarations are the classic
visually-hidden pattern.

**HALF closed, and the estimate above was wrong about the other half.** It said the search would get
those 408px. Measured after: the actions give back **44**, not 408 (408 -> 364), because the buttons
keep their icons, padding and badges. The field goes 44 -> 88 at the point, 84 -> 128 at 900,
204 -> 238 at 1010, and the words return at 1040. So it doubled and it is still cramped between 860
and ~960, and what eats the rest is `.wfh-nav` (198) plus the four action boxes (364) - neither of
which is the search's to spend.

**The cheap next step is microcopy, not layout.** At the same container threshold the placeholder
could shorten from «Пошук товарів, брендів...» to «Пошук» - at 88px the long one shows «Пс» and the
short one is fully legible. That is a product string, so it belongs to `microcopy.md` and to a
decision, not to a component rule.

## ~~The system has five visually-hidden declarations written twice~~ - CLOSED, and asking the question found five more

**Closed at step 10.6b, and the shape of the closing matters more than the item.** The five
declarations had FOUR editions, not two: `.vh` in `base.css`, `.menu-src` in `menu.css`,
`header.css` inside its container query, and an inline `style=` on `checkout.html:52` that was not
even a correct copy - `clip: rect(0 0 0 0)`, the deprecated form, with `padding`, `margin` and
`border` missing. Two of the four are gone: `menu.js` now puts `vh` on the element beside its
`menu-src` marker, and the checkout label takes the class. `header.css` KEEPS its copy, and the
reason is mechanical rather than lazy: the hiding there is conditional - it happens inside
`@container (max-width: 63rem)` and nowhere else - and plain CSS cannot apply a class conditionally,
so no markup class can express it.

**The header's comment used to say it «composes them rather than repeating them», which was simply
not true of the code under it.** It repeats them. What changed is that the equality is now CHECKED
rather than promised.

## Withdrawn on verification at step 6, and left visible

A withdrawn finding stays here, or it comes back next time in the same words.

**The three phone header actions are 21x32, under the WCAG 2.2 floor** - withdrawn. The audit measured
`getBoundingClientRect` of the anchor, which does not include an absolutely-positioned pseudo-element.
`header.css` carries `.wfh-mi a::after{ inset: -6px -8px }`, and a hit test at 360 confirms it: the box
is 21x32, the **hit area is 35x42**, and all four corners of that area return the link from
`elementFromPoint`. 35 and 42 both clear the 24x24 minimum. Both are under the 44x44 AAA target, which
is a smaller point and one the spacing exception already softens.

**An em dash in `design/kit/docs/consolidation.md:8794`** - withdrawn. The line is ABOUT the em dash:
it quotes the table-cell form as the thing being forbidden. All five files in the tree that hold one
are quoting the rule. **The hole underneath it was real and is closed:** the rule was enforced on
rendered screens only, and `tools/typo.mjs` now asks the whole tree, with each quotation declared by
COUNT so a real one added beside it fails the run.

**Twenty inline `style="width:82%"` on rating bars and `width:62%` on the loyalty bar** - withdrawn as
adaptation defects. A bar's percentage is a value, and a static prototype has no server to compute it.

**`--bp-grid-2col` and `--bp-shell-wide` are read by no `var()`** - withdrawn, and written into the
registry so it is not re-found. `@media` cannot read a custom property, so a point token can never
have a `var()` reader; if it had one the rule carrying it would be silently dead. The reader is
`tools/bp.mjs`, which is a stricter one than `var()` would be.

**Six items the browser audit withdrew on its own re-measurement**, and they are worth keeping because
four of the six are the same mistake: reading a value before it settled or without its ancestors.
`.pbrand` «clipped» (it is an ellipsis); `span.lbl` collapsed to 1px (the deliberate clip pattern);
the coach search input's height as a touch target (the target is the wrapping 44x328 label); the
header search having no focus ring (the first read sampled mid-transition); 342 focusables including
the closed mega menu (own-element `display`, ignoring ancestors - `checkVisibility` gives 111); and
`design/content-loyalty.html`, which did not exist that day. **Stage 12 built it at batch 4**, so the
sixth withdrawal is now closed by the page arriving rather than by a re-measurement - and the entry
excusing that path in `tools/paths.mjs` came off at 12.10 when its own idle control went red.

## What step 6 measured and did NOT take, with the numbers instead of the adjectives

Three things came out of the step-6 critique that are real, measured, and not stage 10's work.
Each carries the number that would let somebody decide, rather than a word like «large».

### 1. Eighty inline style declarations on the coloured screens

`design/*.html` holds **108** `style="…"` attributes. The classification matters, because most of a
raw count like that is not a defect:

| kind | count | verdict |
|---|---:|---|
| a bar percentage (`width:82%` on a rating bar, `width:62%` on the loyalty bar) | 20 | **not a defect.** A percentage is a VALUE, and a static prototype has no server to compute it |
| skeleton heights | 7 | borderline - they are the shape of the placeholder, and the placeholder is the component |
| the visually-hidden pattern, inline | 1 | **closed at 10.6b** - it takes `.vh` now, and it was not even a correct copy |
| **style declared on a screen** | **80** | **the defect.** Colour, weight, margin, flex, and `font-size:13.5px` / `12.5px`, which are not values on the ramp at all |

**Why it is not taken here.** The ban - «a screen declares no styles of its own» - is stage 08's, and
closing it means deciding, for each of the 80, whether the screen wanted something the system lacks
(then it is an order for the system) or repeated something the system has (then it is a class swap).
That is the same three-list sweep stage 08 ran on the sample and stage 12 will run on the corpus, and
doing it here would do it twice. **Nothing measures it today** - `private-css.mjs` counts rules in
`<style>` blocks and has never looked at the attribute - so the count above is the first one taken.

### 2. Twenty-nine typography switches at a point, and nothing can tell the three kinds apart

`font-size`, `line-height` or `letter-spacing` inside a width query, in 12 files. The pack calls this
a defect by class, and by class it is - but reading the 29 shows three different things:

- the element EXISTS only in that place (`cat-overlay` is the mobile overlay, its 14 declarations are
  its only ones) - not a switch at all;
- a genuine switch (`product-card` drops the name from `--fs-14` below 620);
- `base.css:86`, `font-size: var(--fs-16)` on `textarea` and `select` below 859, which is the iOS
  zoom guard and must not be touched.

**Telling them apart needs the OUTPUT, not the source:** does the computed `font-size` of that element
actually change across the point? That is a browser question and the instrument for it does not exist.
Recorded rather than guessed, because converting the second kind to `clamp()` and the first kind by
mistake would flatten a component that never had a ramp.

### 3. The visual assets are 2048x2048 PNGs rendered at thumbnail size

Measured over the wire at 1280, cache cleared:

| page | images | transferred |
|---|---:|---:|
| `index` | 21 | **9.34 MB** |
| `checkout` | 4 | **6.11 MB** |
| `product` | 10 | 1.00 MB |

Four files are almost all of it: `product-creatine.png` 2 446 389 B, `product-whey.png` 2 386 308 B,
`mascot-face-reassure.png` 1 736 820 B, `product-preworkout.png` 1 571 458 B. The three product shots
are **2048x2048**; they are painted as `background-image` on cards that are never wider than a few
hundred pixels.

**What step 6 did take:** the 2.28 MB PNG that was being injected into the CLOSED mega menu at boot on
every page that has one - measured `rendered "0x0"`, `natural "2048x2048"` - now loads on first open.
And all 28 `<img>` in the folder got `loading="lazy"`, `decoding="async"` and their intrinsic
`width`/`height`, so none of them reflows the page as it arrives. **What it did not take:** re-encoding
the owner's brand assets. A `background-image` cannot be lazy-loaded declaratively, so the fix is
smaller files or a `image-set()` ladder, and both change what stage 06 produced. That is an owner
decision and a build step, which is stage 13.

## Six blocks the system writes twice, and the new instrument that found five of them

`tools/dupe.mjs` reduces every rule in `design/system/` to its declarations, normalised and sorted,
and asks which blocks appear in two different files. It reports from four declarations up and fails
the run from six up - because `color; font-size; line-height; margin` is a sentence in the language,
while six identical declarations are the same OBJECT written twice.

**The two counts this check prints - groups in the idiom range, and groups above the failing floor -
are NOT repeated here.** They were, as «30 groups over 71 places», and by 12.10 the run says 39 over
91 while the sentence still said 30: the corpus grew by 137 blocks and eight files between stage 10
and the end of the rollout, and nothing compared the sentence with the run. Run it.

**12.10: THE CAPS-LABEL PAIR BECAME A TRIO, and that is the entry earning its keep.** `.wff-col h4`
(footer column) and `.wfh-mega .mgt` (mega-menu column) were declared as a pair when they were a
pair. `info-page.css` arrived at batch 3 with `.info-toc .tt` - the label over the numbered index of
a service page - byte-identical to both, and the group failed the run instead of staying green. Three
identical blocks is where a caps-label atom stops being a preference. **A declared group is a set of
claims under test, not a mute button**, and this is what that sentence buys.

All groups above the floor are declared with the reason CSS could not share them, and one of them is
genuinely finished (the `.vh` pair above). The rest are real work and they are NOT stage 10's:

| the block | where | what it is |
|---|---|---|
| square photo frame, 10 declarations | `.oc-ph` (`coach-clients.css`) = `.qa-ph` (`coach-session.css`) | one object under two names - the shape rule of step 7.68, «a box that frames a product photograph is square» |
| the same frame, larger | `.od-ph` (`coach-order.css`) = `.cl-ph` (`coach-session.css`) | the same object under two more names, so four in total |
| panel section label, 6 | `.city-lbl` (`city-dialog.css`) = `.dr-lbl` (`nav-drawer.css`) | two mobile panels, one label shape |
| link-list column heading, 6 | `.wff-col h4` (`footer.css`) = `.wfh-mega .mgt` (`header.css`) | the footer column and the mega-menu column |
| the wordmark, 6 | `.co-logo::before` (`checkout-form.css`) = `.wfh-logo::before` (`header.css`) | **this one is probably correct as it stands** - the checkout header is a REDUCED header with no nav, no search and no actions, and reaching into `header.css` for one glyph would make the reduced header depend on the full one |

**Why it is not taken here.** Extracting a shared photo frame or a shared label is adding a COMPONENT,
and this repository charges five things for that: a css file, a stand page, a registry row, an
inventory row and an `@import` in its own level group. That is stage 12's work on a corpus that is
half grey, not stage 10's on the adaptive layer. What stage 10 owes is the instrument, and it is
built: if any of the four names drifts from its twin, the run now fails and says which.


`menu.css` carries them on `.menu-src` and `header.css` now carries the same five on the action
labels. It is the classic pattern and it is correct in both places, but a second copy is how a third
one starts. The system wants ONE utility - a class in `base.css`, or a `%`-style placeholder - and
both sites point at it. Small, mechanical, and it belongs to whoever next opens `base.css`.

## ~~The bonus action has no bonus icon~~ - CLOSED, the owner chose a new coin

«БОНУСИ / Отримати» in the header action row stands beside «Увійти» (a person), «Обране» (a heart)
and «Кошик» (a cart), and wears a generic container glyph. A set of four marks where three name
their job and one does not reads as a gap rather than as a choice. This is an addition to the icon
set, which is stage 08's territory and `design/kit/icons.html`'s page: a new glyph, its row in the
set, and the same 30-size / 6-weight discipline every other mark in the set carries.

**CLOSED.** The set had 67 marks and not one of them named MONEY, so «Бонуси» wore `star` while the
three actions beside it named their job. The new `coin` is two concentric circles on the set's own
24x24 grid at stroke 1.9 - no sparkle and no plus, because the voice forbids celebrating a balance.
It needed no row anywhere: `icons.html` renders the set live from `design/system/icons.js` and says
so on the page, «другої копії набору немає ніде», so the glyph appeared on the stand the moment it
existed. Verified in the browser. The `★` in the header markup became `◉`, mapped in both mark maps.

## ~~The split turns on by SCREEN and the frame needs PLACE~~ - CLOSED by the critique repair

**Measured on `design/coach-clients.html`, the split frame and its two columns:**

| viewport | `.clsplit` | rail (fixed `17.5rem`) | detail |
|---|---|---|---|
| 860 | 528 | 280 | **224** |
| 960 | 628 | 280 | 324 |
| 1024 | 692 | 280 | 388 |
| 1280 | 732 | 280 | 428 |
| 1440 and up | 868 | 280 | 564 |

From 860 to about 1010 **the detail is narrower than the list**, and at the point it is 224px -
narrower than a phone. It is not broken: `.cdetails` carries a container threshold at `22rem`, so the
label-and-value rows fold to one column and the panel reads. But the cause is the same one that set
`--grid-col-min-panel` to 19rem instead of 22rem at step 4: the account shell takes its own 268px nav
column, so `.acc-main` at a 860 viewport is a 528px box.

**`@media` asks about the SCREEN; this frame needs an answer about the PLACE.** The ladder's own
answer is `container-type: inline-size` on `.acc-main` and `@container` on `.clsplit`, which would
turn the split on when there is room for it rather than when the window is wide. Two reasons it was
not taken inside step 5, and both are the owner's to weigh:

1. It changes WHEN the split appears, which is a decision said out loud rather than a refactor.
2. The coloured corpus cannot measure it honestly. The stand's own roadmap rail appears at 1076 and
   drops `.acc-main` from 692 back to 528, so a container query would flicker off and on while
   browsing the design copies and be right in the product. The measurement would have to be taken
   with the rail suppressed, which is a change to how the copies are viewed.

Cost if taken: one `container-type` on `.acc-main`, one query moved in `coach-clients.css`, and
`wfClientSplit()` stops reading `--bp-shell-wide` through `matchMedia` and asks the OUTPUT instead -
`getComputedStyle(pane).display !== 'none'` - which is what this repository asks everywhere else.
Check first that nothing `position: fixed` lives inside `.acc-main`: `contain: layout` would
re-anchor it, which is exactly why the page frame could not become a container at round 4.

**CLOSED, and the cost was one line more than the estimate above.** Measured first: zero
`position: fixed` descendants inside `.acc-main` on all three clients screens, so the container was
safe. `.acc-main` had no rule of its own at all - it was a bare grid cell - so `container-type:
inline-size` is the whole of its rule. The estimate was wrong in one place: a BARE `@container` is
not enough, because the place is not monotonic in the viewport. It turned the split on below 860
(no nav column there, so the box is the full 828) and off between 860 and 960 (the shell takes its
268 and the box drops to 528) - the split appeared, vanished and came back, and the sweep caught it
inside one run. The rule is now `@media (min-width: 860px)` with `@container (min-width: 41rem)`
nested inside: the shell first, because a two-pane workspace belongs to the desktop shell, then the
room. The split opens at a 990 viewport with a 354px detail pane, and the 224px is gone.

## The stand's roadmap rail reflows the product page, and now it changes WHICH layout you see

Measured on `design/coach-clients.html`, swept at 10px from 320 to 1600: the split opens at 990
(`.acc-main` 658), **closes at 1080** (`.acc-main` 532) and opens again at 1210 (662). The 1080 drop
is the stand's own roadmap rail appearing at 1076 and pushing the page: the coloured copies are
~216px narrower than production above that width.

**This was always true and it never mattered until now.** Every width this stage measured was
measured with the rail in the page, but with `@media` the rail only shifted pixel numbers. With
`@container` it decides which LAYOUT the page gets, so a viewing aid now has a vote on the product's
composition. In the product there is no rail and the sweep is a single transition at 990.

Three ways out, none of them free:
1. **The rail overlays instead of reflowing.** Correct in principle - it is chrome for looking at the
   product, not part of it - but it would cover content at exactly the widths where it appears.
2. **The rail is suppressed for measurement**, and every instrument opens pages with it off. Cheap
   for the instruments, but then nobody ever LOOKS at what the instruments measure.
3. **Leave it and keep printing it.** What is in place today: `tools/split.mjs` prints every
   transition with the box that caused it, so the flicker is in the output of every run rather than
   in somebody's memory.

## 20 stand pages describe a width the component no longer has - stage 10, owed to step 6

Measured across all 84 component stand pages, comparing the `(min|max-width: Npx)` numbers written on
the page against the numbers the component file actually holds after comments are stripped: **22
pages named a width that is not in their own file, and step 5 repaired the two it touched, leaving
20.**

| page | says | the file holds |
|---|---|---|
| `account-shell` | 640, 959, 960 | 620, 859, 860 |
| `auth-dialog` | 719, 720, 899, 900 | 859, 860 |
| `checkout-form` | 479, 480, 559 | 619, 620, 860 |
| `coach-cabinet` | 520, 640, 720 | 619, 620 |
| `coach-landing` | 559, 980 | 619, 620, 860 |
| `coach-verify` | 520, 760 | 620, 860 |
| `footer` | 479, 720 | 619, 860 |
| `menu` | 859 | 619 |
| `pdp-tabs` | 1180 | 860 |
| `system-page` | 720 | 620, 859 |
| `trust-strip` | 479 | 619, 859, 860 |
| `buy-box`, `city-dialog` | 479 | 619 |
| `hero` | 720 | 860 |
| `address-card`, `button`, `field`, `loyalty-rung`, `product-grid`, `restock-note` | various | the file holds none at all |

**Not all 20 are defects, and that is why this is a list rather than a fix.** A stand page is allowed
to name history - «було 720, стало 640» is a record, and a record may keep the old number. What is not
allowed is a RULE that names it: `coach-clients` held `(min-width: 640px) -> .clist у дві колонки` in
its «Межі» table, presented as the current boundary, four rounds after that number stopped existing.
Separating the two needs a reading of each page, not a regex.

**Nothing checks this today, and the gap has a shape.** `tools/bp.mjs` deliberately excludes the stand
from its subject - «у стенді, і це не предмет: 17» - because a stand page legitimately shows CSS that
is not the product's. So the numbers the stand writes in PROSE are checked by nobody, and stage 10
moved 27 of them. The check is cheap (the comparison above is fifteen lines) and belongs either as a
fifth class in `bp.mjs` with the stand as an explicit second subject, or in `inventory.mjs`, which
already reads every stand page and every component file and already fails on meta drift.


## ~~20 stand pages describe a width the component no longer has~~ - CLOSED, and 11 of the 20 were legal

Read page by page rather than by regex, because the backlog's own note said a regex could not tell a
record from a rule. The 49 ghost numbers across 20 pages split three ways:

- **9 pages carried a STALE CURRENT CLAIM** and were rebuilt. Twelve of them were the «У файлі N
  медіа-умов» section, which enumerates the file's conditions as fact - `account-shell` listed
  640/959/960 against the file's 620/859/860, `auth-dialog` 719/720/899/900 against 859/860. Those
  lists are now GENERATED from the file rather than typed. Three more were «Межі» tables presented as
  current rules: `coach-cabinet`, `coach-landing`, `coach-verify`. `product-grid`'s list went to
  zero, and a «У файлі 0 медіа-умов» reads as a bug, so that one is written in words.
- **8 pages name HISTORY**, which is legal and stays: «Було `@media (min-width: 720px)`», «Крок 7.26
  вийняв звідси», «обидва блоки структурної половини, які не грають ніколи».
- **3 pages style their own demo tables** with their own queries - `button`, `field`, `menu`. Those
  are the STAND's layout, not the component's, and the first measurement counted them wrongly.

**And it is checked now, which was the actual finding.** `inventory.mjs` gained class H2: it already
reads every stand page and every component file, so the comparison is fifteen lines. Both legal
classes are declared as lists and both are idle-controlled - a page in either list that no longer
carries a ghost fails the run as loudly as an undeclared one. Both failure classes were proved by
being introduced on purpose and reverted.

## The hover that was added to fix a missing hover state drew nothing

Caught by `tools/vars.mjs` in the same pass that introduced it. The new `.ccard:hover` reached for
`--bg-hover` - the name stage 08's own prose uses for this role - and **that token is declared
nowhere.** An undefined custom property is the quietest failure CSS has: the declaration becomes
invalid at computed-value time, the background lands on its initial value, and nothing is raised.

Fixed by reuse rather than by declaring: `--bg-sunken` is what every other hovered row in this system
already takes - `.menu-opt`, `.wfh-langmenu a`, `.wfh-cabmenu a` - and a card in a rail is a row in a
list. **The open question is the prose, not the code:** stage 08's documentation names a state token
that has never existed, so the next reader will reach for it again. Either the token gets declared
with both themes, or the prose stops naming it.

## The two spinners write the same rule twice - stage 11, found by Codex at step 6

`auth-dialog.css:205` and `checkout-form.css:364` carry a byte-identical declaration:

    .auth-spin{ border-top-color: var(--line-action); animation: uivspin var(--dur-cycle) var(--ease-cycle) infinite; }
    .co-spin{   border-top-color: var(--line-action); animation: uivspin var(--dur-cycle) var(--ease-cycle) infinite; }

plus a duplicated `@media (prefers-reduced-motion: reduce)` block beside each. **Half of this was
already fixed at step 4**: `@keyframes uivspin` was declared inside `auth-dialog.css` and read from
`checkout-form.css`, which worked only because keyframe names are global, and it now stands in
`base.css`. What is left is the rule itself.

**Why it is not closed here.** Merging them needs ONE class on both elements, and the markup of one of
the two is written by `wireframes/_nav.js` - the frozen grey corpus this stage may not edit. The
coloured screens would take a new class today and the grey ones would not, which is two editions of
one control, and that is a worse defect than the duplicate. **Stage 12 rebuilds both screens anyway**,
so the merge costs one class there and nothing anywhere else.

The same shape as `action-row`'s forty unconverted container names: a conversion that is correct and
whose only honest moment is the rebuild.

## Every «немає» in the motion column carries a reason - stage 11, step 6

Not a debt, a rule, written here because the next stage will add rows: **a component whose `Рух`
column says «немає» must say WHY in the same cell** - «стану немає» (nothing to answer) or «рух
шкодить» (movement would lie about what happened). A bare «немає» is a gap wearing the clothes of a
verdict, and `tools/motion.mjs --states` cannot tell the two apart.

## The view transition carries nothing - stage 11, step 6, found by the critique

`@view-transition { navigation: auto }` crossfades one whole-page snapshot into another. The moment
it could actually be doing - the listing card's photo becoming the product page's photo, which is the
CONNECTION job in its purest form - needs `view-transition-name` on both elements in both documents,
and there are **0 declarations** of it in the tree.

**Why it is not closed here, and the reason is measured rather than preferred.** A
`view-transition-name` must be UNIQUE per document. A listing renders 12 product cards, so a single
declaration in `product-card.css` produces 12 duplicates, and Chrome's response to a duplicate name is
to skip the entire transition, not the offending pair. Unique per-card names therefore cannot come
from a component stylesheet at all, and they cannot come from a screen file either: **stage 11 bans
`transition`, `animation` and `@keyframes` in `design/*.html`**, and a per-element motion identity
written into a screen is the same defect wearing a different property name.

**What it would take.** Either the names are generated where the markup is generated - which is
`wireframes/_nav.js` for the grey corpus and the screen builder of stage 12 for the coloured one - or
the system grows a documented way to stamp them. Both are stage-12 work, on screens stage 12 rebuilds
anyway. The same shape as the two spinners above: a conversion that is correct and whose only honest
moment is the rebuild.

**And it is not free even then.** The transition currently fires identically on `listing -> product`
(a real move), on `listing -> ?page=2` (the same document), and on related links that point at the
page they are already on. Scoping it to the pairs that mean something is the other half of the order.

## The result plate stands in the product under SEVEN names - stage 12, batch 1

Found by the subagent that stopped on node 6.2 rather than draw it, and measured before it was
believed. The shape is the same every time: a bordered plate, centred, a disc with a glyph, a
heading, a sentence under it.

| the name | file | what it announces |
|---|---|---|
| `.cv-ok` | `coach-verify.css` | success - and it is scoped to `.coach`, so nothing else can wear it |
| `.co-err-box` `.co-err-mark` `.co-err-h` `.co-err-s` | `checkout-form.css` | failure - payment declined |
| `.co-proc-box` | `checkout-form.css` | waiting - payment in flight |
| `.sys-code` `.sys-h` `.sys-s` | `system-page.css` | 404 / 500 / 503 |
| `.emptybox` | `empty-state.css` | there is nothing here |
| `.errbox` | `empty-state.css` | the data did not arrive |
| `.auth-visual .vmid .vi` | `auth-dialog.css` | the disc alone, without the plate |
| `.op-hero` `.op-check` | `order-placed.css` | success - added at 12.3 |

**The rule for a pattern is three named screens. This is seven families.** `design/system/CLAUDE.md`
rule 9 could not be more clearly met, and the reason it was never met before is that no two of the
seven were ever built in the same step: each arrived with the flow that needed it, and each was
correct about its own screen.

**Why it is NOT taken at 12.3, and the owner said so out loud.** Extracting it means touching seven
accepted files and the screens under them - `checkout-declined`, `checkout-loading`, `coach-verify`,
`404`, `500`, `maintenance`, every empty state - and batch 2 of the rollout would wait for it. Node
6.2 got its own level-3 file instead, which is the cheap branch and closes the screen today.

What the number buys is that the question cannot quietly disappear: **seven families, one shape**,
and the extraction is a stage-13 handoff row rather than a hypothesis somebody may raise again.

**One thing to check first if it is ever taken:** the seven differ in a way that may be the whole
point. `.cv-ok` and `.op-hero` are SOFT-ground plates, `.co-err-*` is a failure and reads danger,
`.sys-*` has no plate at all (it is the page), and `.emptybox` explicitly refuses to be `.errbox`
- the stand page for `empty-state` says «`.errbox` is when the data did not arrive, not when there
is none». A pattern that flattens those distinctions would be worse than seven files.


## Stage 12, step 7 - what the critique named and did NOT take

Each of these is a DECISION, and a rollout decides nothing: it multiplies. An unresolved line arriving
at a fan-out is invented afresh by every agent separately, so each of these carries its number and
waits for the owner rather than being patched on one screen out of twenty.

| What | Measured | Why it is the owner's |
|---|---|---|
| **The emoji map, exhaustively** | 20 pictographs on 12 product screens with no row in `UIV_EMOJI`, 30 occurrences, almost all on the 8.x family. **12 already have a drawing in `icons.js`** (🕘/⏱ `clock`, 💳 `card`, 💬 `chat`, ↩ `ret`, ✅ `check`, ✉ `mail`, 🚚 `truck`, 📄 `doc`, 🔄 `refresh`, 🔗 `link`, 🔒 `lock`); **8 need one** (🗓 🛠 🤝 📞 🗺 📱 💵 💰) | And the pass itself is the second half: `uivIcons()` is called on six chrome ids, the drawers, the overlays and the home rail, and **never on `.wf-page`**. Turning it on before the map is complete makes the rows LESS uniform, not more - eight raw emoji standing beside twelve icons |
| **The illustration payload** | 11.3MB of PNG. Three mascots at 1024×1024 render at 118-126px - 8.7× oversample - and three product shots at 2048×2048 | Assets are the owner's, and the right answer (resize, or a second size, or a different format) is a decision about the source files, not about css |
| **The touch rung** | `.chip--letter` 36×40 on the A-Z index of `brands`, and the 40 rung on `.btn--s` / `.field--s`, against `field.css`'s own sentence «44 is the touch minimum» | Raising the S rung moves every screen in the product. Either the rung changes or the sentence does |
| **Heading rank and level** | One rank drawn four ways across the content family (18/700 Inter · 20/700 Inter · 24/600 Oswald); six screens skip a level (H1→H3, or H2 after H3) | A rank ladder is a stage-08 decision, and correcting it screen by screen would give a seventh spelling |
| **The service-page mark** | Four materials for one job: a numeral (`404`, `500`), an emoji (`maintenance` 🛠), a text glyph (`catalog-page-error` ⚠, and `account-error` uses ⚠️ with the variation selector), an illustration (`brands-error`) | Which of the four is the product's answer is a visual-language call |
| **The rank of a lone action** | `maintenance` has one action and draws it `btn--outline`; there is no accent anywhere on the screen, against principle 2. The grey never had a primary either, so the rollout's rank rule read it correctly | Named at batch 5, still open |
| **Empty states that end** | `brands-empty` stops at its two buttons; `search-empty`, the same moment in the same flow, offers six goal tiles and a chip row underneath | Two answers to one question, and only the owner picks which |
| **Skeletons that under-reserve** | `.skcard` is one shape doing duty for four different real cards. Measured at 360: `catalog-page-loading` 70px against a real 116-138 goal tile and 244 against a real 367 product card; `brands-loading` omits the A-Z index entirely, so everything below it shifts ~218px when data lands; `coach-home-loading` has no CTA skeleton at all and its `.acc-main` sits 96px too high | A skeleton per real card is a component decision with four new files behind it |
| **Two labels, one action** | `coach-order` carries «Повторити замовлення» twice, ~700px apart: one repeats ONE client's part, one repeats the whole order. And across the coach flow the same job is «↻ Повторити» on one screen and «Повторити замовлення» on three | Interface strings belong to `microcopy.md`; both editions are already registered there as separate rows rather than resolved |
| **The Pro plan, five editions** | `coach-tariff` prints it twice on one screen with different contents (4 items vs 3, every shared item reworded, one benefit silently dropped); `coach-verify-tier`, `coach-clients-cap` and `coach-landing` each carry a fourth and fifth | One canonical list, one owner |
| **The accent decision reached one screen of four** | `coach-clients` documents it in its own head («ONE ACCENT ON THE SCREEN») and carries 2 accent grounds in `<main>`; `coach-orders` carries 5, `coach-home` 5, `coach-client` 3 | The decision exists and was applied once; extending it is a per-screen ranking call |
| **Two notice components, one job** | `.cs-warn` (title + body + action + `role="alert"`) on `coach-session-priceblock` and `.cs-banner` (a plain box, no role, no title, no action) on `coach-session-oos`. One instance each in the whole tree, and neither is `banner.css` | The session needs one, and which one is a component decision |
| **`meta description`** | 0 of 140 coloured heads, 0 of 141 grey. It stays absent by decision (every `design/` page is `noindex`, and SEO copy has one owner). **But `description` exists in only 6 of 18 `ia/docs/pages/*.md` and 4 of 22 `ia/*.html`** - twelve nodes have that string nowhere | A debt of stage 03. A rollout may not close it by inventing copy |
| **The orders pair has no split** | `coach-orders` → `coach-order` is the same list-and-record shape as `coach-clients` → `coach-client`, which got a split at stage 10. At 1440 the orders list is one narrow column with ~500px of empty rail beside it, and `split.mjs` passes it **by construction** - the registry declares two frames, so a third is an undeclared case rather than a failing one | Whether the pair earns a frame is a stage-10 question re-opened by stage 12's corpus |


## The one gate this stage leaves RED, named rather than hidden

`node tools/idle.mjs` exits 1: **29 of 93 component pages, 67 declarations behind their file**. Every
one of them is the same shape - a class exists in the component's css and the page's own `KIT_CLS`
list does not name it, so the stand's idle control cannot ask whether that class is demonstrated.

**It is not new to step 7 and it is not closed by step 7, and both halves matter.** Batches 3, 4 and
5 wrote nine component files and extended eight more; every class they added arrived in the css and
not in the page's declaration. Three of the 67 are this step's own repairs (`.info-grid:has()`,
`.cmp-cell.pro .yes`, `.chips--sticky`'s new `top`).

**Why it was not auto-filled the way `roles.mjs` was.** The token table is DERIVED - `var(--x)` in the
css minus what the file declares for itself - so writing it is arithmetic. `KIT_CLS` is not: naming a
class there is a promise that the page SHOWS it, and the run currently reports `винне демо: 0`.
Filling 67 names mechanically would move 67 findings out of «declaration behind the file» and into
«owes a demo», which is the same debt wearing a better number. **A green counter bought by moving the
red is exactly what this stage spent itself proving against.**

The work is 67 demo blocks on 29 pages, and it belongs to whoever owns the showcase.

### 12.11 - CLOSED, AND TEN OF THE 67 WERE NEVER REAL

`node tools/idle.mjs` now exits 0: **94 pages, 0 red, 0 declarations behind their file.** The debt was
paid, but the shape of the payment is the finding, and it is this stage's signature class one more
time - **a green counter that could not see its own class, wearing red instead of green.**

Three of the 67 were the instrument being wrong, and each is a separate defect:

- **`.html` is not a class.** The class extractor stripped comments and nothing else, so
  `a[href="index.html"]` in `header.css` read as a declaration of `.html`; one file owned it, so it
  became an anchor, so `header.html` was red for a class that does not exist and no demo could ever
  have shown. Attribute values, `url()` arguments and quoted strings are now stripped: 1137 anchors
  become 1136, and the one that left is that.
- **A class that lands on `html` or `body` cannot be put in a demo box.** `_idle.js` reads
  `.kp-demo *`, so the only way to satisfy a demand for `html.dr-lock` or `body.pdp-stuck` is to hang
  the class on a `div` inside the box, which renders nothing and states something untrue. The test is
  read off the css, so it discriminates: on `pdp-tabs.css`, `body.pdp-stuck` is excused in the same
  run that still demands `.pdp-tabs.stuck`.
- **It asked only ONE of the page's two lists.** The reverse question was written against `KIT_CLS`
  alone, so a class the page had properly declared as a STATE - `drawer-open`, `mega-open`,
  `wfh-menuopen`, all three named in prose on `header.html` since 8.31 - still came back as
  «declaration behind the file». **That is 7 of the 67**, and 9 anchors rely on it today. The pages
  had done the right thing and the gate could not see it. `parkedIn` is what stops `KIT_STS`
  becoming a place to hide, and it still runs.

The books close exactly: **1 + 2 + 7 = 10 were the instrument, and the other 57 were real.** Those
57 were paid the expensive way - 57 class names demonstrated live across 26 stand pages, not one of
them declared without being shown. `винне демо` stayed **0** through every intermediate run, which
was the whole condition for refusing to do it mechanically in the first place.

Batch 6 then ADDED work of its own and paid that too, so the counter is honest rather than merely
zero: `kit/quiz.html` declares and shows 43 classes of a component that did not exist this morning,
`section-head.html` gained the brow's bold rung (2), and `badge.html` the third badge form (1).

---

## Four dead tokens, found by the map of stage 13 - and the walk that found them needed opening first

**Stage 13, step 3.** `tools/map.mjs` inverts the same data the map is built from - component reads
`var()`, role reads primitive - and asks the question backwards: which token does nobody read. The
answer is four, and none of them can be closed by stage 13, because `design/system/**` is untouchable
after the roll-out was accepted. They belong to the owner of stage 08.

| Token | Kind | Why it is dead | What the owner decides |
|---|---|---|---|
| `--bg-success` | role | `status-pill.css` documents the split in its own comment: the SOFT ground is what every reader takes, and the calm success PLATE this role names is taken by nobody. Three files mention `--bg-success-soft`; none reads `--bg-success` | remove the role, or give it the reader its comment promises |
| `--text-price-was` | role | the struck-through old price. `price.css` draws that line with another role, so this one names a job the product already does elsewhere | remove, or unify the two |
| `--scrim-white-70` | primitive | no role reads it and no component reads it directly. The scrim family survives on its ink half | remove |
| `--tint-red-d-10` | primitive | the dark-theme red tint. Its light twin has readers; this one does not, which is the shape of a pair half-finished | remove, or finish the pair |

**And the instrument that found them had to be taught to open a door first.** Its first run also
reported `cat-overlay.css` as «on no screen at all», which is false: it is the mobile catalogue
overlay and it renders on every page. The walk was reading the document AT REST, and a component
that only appears after a click is invisible there - the same class `steps.mjs` was written for at
12.11. The walk now calls every opener the page declares (3679 calls over 141 screens, one dropped
because it navigates away) and `cat-overlay` moved from «dead» to «global». **A dead-token list taken
without opening the panels would have been wrong in the other direction too**, and nothing in the
output would have said so.

**Two primitives are dead-looking and are NOT on this list, by a declared exception:**
`--bp-grid-2col` and `--bp-shell-wide` have no reader BY CONSTRUCTION, because `@media` cannot read a
custom property. The exception is asked both ways - if either ever gains a reader, the run fails,
because the exception would then be excusing something it no longer describes.

---

## `quiz.css` reads a colour primitive directly - the first component in the system to do it

**Stage 13, step 4.** `theme.mjs` question 2 answered «none - every colour in the components goes
through a role» for the whole of stages 08 to 11. It does not any more:

```
quiz.css:12   --scrim-ink-10   background: var(--scrim-ink-10)
```

`.q-backdrop` is the dim behind the quiz card from the two-column point up. Every other scrim in the
system - the search overlay, the drawer, the cart, the filter rail and sheet, the header, the menu,
the auth dialog - reads the ROLE `--scrim-overlay`. This one reads the primitive.

**It is my own, from batch 6 at 12.11, and nothing caught it because the gate of stage 12 was not
re-run after that batch** - the same sentence as `inventory.mjs`, on the same commit, found four
steps apart. «A repair stales its own neighbours» was already written down; what was missing is that
an ADDITION stales them too.

**Why it is a debt and not a repair.** `--scrim-overlay` is the modal dim and is far heavier than
this backdrop wants; there is no semantic role at this weight today. Giving it one is a NEW ROLE in
`tokens.css`, in both themes, which is a decision of the system's owner and belongs to stage 08's
five-part rule, not to a stage that documents. Two ways out, both the owner's:

1. a semantic role for «the light dim behind a card that is not a modal», paired in both themes;
2. or `.q-backdrop` takes `--scrim-overlay` like every other scrim, and the card stops being a
   half-modal.

**The map already carries it.** `handoff/docs/map.md` section C2 prints `--scrim-ink-10` as reached
through `--elevation-2`, `--bg-inverse-tint` and *component quiz.css* - the direct read is marked
rather than hidden, which is why the two-knee inversion has a third case at all.

---

## The accessibility debts of stage 13, step 4 - eight rows, each with the number that has to reach zero

`handoff/docs/a11y.md` holds 26 rows, 18 confirmed by a run on 2026-08-25 and eight open. The eight
are here because the checklist may not repair anything: the product was accepted at stage 12 and
`design/system/**` belongs to the owner of stage 08. Every row below names the command that produced
its number, so nothing has to be taken on trust and nothing has to be re-derived.

| # | Debt | Today | The command | Zero looks like |
|---|---|---|---|---|
| C5 | **text-only zoom at 200%** | **142 of 343 screens** overflow horizontally at 1280, **284 of 343** at 360 | `node tools/accept.mjs 1280 --text200` | no screen scrolls sideways with the root font size doubled |
| E2 | the heading ladder skips a rung | **97 of 141**, and it is ONE cause: the footer's newsletter block is an `h4` directly after an `h2` | `node tools/headings.mjs` | one heading level changed in one component |
| B2 | ink fails its own ground | **12 shapes**, after the ruling of 25.08.2026 carved out the 25 that are the Signal Orange pair. 8 of the 12 are stand pages, 4 are product screens, 2 of those 4 are the word «фото» in an image placeholder | `node tools/theme.mjs` | the two real shapes on `search-suggest` take ink that passes; the 25 ruled ones never reach zero and are no longer counted here |
| E1 | no reachable `h1` | **35 of 141**: 17 dialog states, 16 loading and error states, 2 checkout states with no heading in the markup at all | `node tools/headings.mjs` | a heading inside each dialog, and two on the checkout states |
| F1 | a mark still drawn by the font | **11 marks the map knows** (18 occurrences, 7 screens) + **19 with no map row** (29 occurrences, 11 screens); **11 product screens** in all. Corrected 25.08.2026 - this cell used to read «5, every one on a stand page; on the 141 product screens 0», and the run named beside it had already contradicted that on the day it was written | `node tools/glyphs.mjs` | every mark on a product screen comes from the set: about 20 map rows, 8 of which need a drawing that does not exist |
| C6 | a line past the reading measure | **2**: `system` at 128.5ch and `content-reviews` at 88.4ch against a measure of 68 | `node tools/width-sweep.mjs` | both blocks take the text container |
| B3 | a component reading a colour primitive | **1** - `quiz.css`, written up on its own above | `node tools/theme.mjs --source` | a semantic role for the light backdrop |
| G2 | one entry in two navigation carriers at 360 | **2** - «Кошик» by decision, «Обране» not in the specification | `node tools/tab-walk.mjs index` | an IA decision at stage 03a, not a css one |

**C5 is the largest and the least expected.** Stage 10 moved the type ramp from px to rem value for
value, and the reader that change exists for is exactly the one who raises the default font size.
Nothing had ever asked whether it worked, because browser zoom - which every earlier check did
cover - only halves the viewport and leaves the type alone. The two are different questions and only
one of them had an instrument.

**E1 and E2 come from an instrument that did not exist before this step.** `tools/headings.mjs` was
written because the roll-out's class 8 named two screens in prose and nothing could re-ask it. It
finds 17 where prose found 2, and it found E2 - 97 screens, one cause - which nobody had named at all.

---

## `motion-row.mjs` writes by default, and every other repair in `tools/` is behind a flag

**Stage 13, step 6, found by accident and worth keeping.** After the clean-clone test caught the
author's home directory typed into line 6 of `tools/motion-row.mjs`, the fix was smoke-tested by
running the tool - and the run **rewrote 21 stand pages** without being asked. It has no `--apply`
guard: reading and repairing are the same invocation.

Every other repair in this folder asks first. `inventory.mjs`, `rollout-table.mjs`, `scope.mjs`,
`btn-rank.mjs`, `inert.mjs`, `grey-vars.mjs`, `links.mjs`, `screen-css.mjs`, `map.mjs` and
`coverage.mjs` all read by default and write only behind `--apply` or `--write`. One tool out of
eleven does the opposite, and the cost is exactly what happened: a person who wanted to know whether
it still RUNS changed 21 files.

The 21 writes were reverted; the path fix stands and is confirmed by the clone test's second run.
**The cure is one line and it belongs to the owner of stage 11**, because a flag changes how the
tool is called from every step that calls it: put the write behind `--apply`, and print what it
WOULD do without one.


---

## «Мої стейпли» - eight orders from the first screen built after the handoff

**Written by the agent that assembled `design/account-staples*.html` from `handoff/docs/one-shot.md`,
with no context beyond the documents that prompt names.** The feature is post-launch by decision
(`ia/docs/sitemap.md` Open items, `research/docs/cjm-to-be.md` Backlog P2), so none of the rows below
is urgent - they are here because each one stopped the build for a moment and the next person will
be stopped in the same place. Nothing on this list was drawn in the screen file instead.

| # | What is missing | Where it bites | Whose call |
|---|---|---|---|
| S1 | **the account rail has no row for the section** | `WF_ACC_LINKS` in `wireframes/_nav.js` is the ONE source of the buyer rail, and that folder is frozen from stage 05. The four screens call `wfAccountNav('staples', false)` with a key the list does not hold, so the rail renders correctly and marks nothing as current. A person inside the cabinet has no way to reach the section, and a person on it is not told where they stand | the owner of the grey runtime. Adding a row by hand in four screen files would make a second edition of a rail whose whole rule is «one source» |
| S2 | **a screen with no grey twin is invisible to the coverage map** | `tools/coverage.mjs` derives its rows from `WF_FLOWS` and uses `DESIGN_NAV` only to decide whether a row is coloured. `one-shot.md` section 2 says the registry row «is also what puts the screen on the map» and that `--check` «says whether it did» - measured, it does neither: after the four rows were added the check still prints «сторінок у реєстрі: 141 · на карті всі 141» and exits 0. The same derivation drives `uivBar()`, so the stand rail does not list the screen either, and `wfBar()` finds nothing to draw. **A green counter that cannot see the class**, which is the failure this repository has now met six times | the owner of `tools/`. The cure is one question asked the other way: a name in `DESIGN_NAV` that no flow knows is either on the map with its reason or it fails the run |
| S3 | **`skeleton.css` has no promise for the restock-note row** | the file writes skeleton rules for `.oc-item`, `.qa-row` and `.od-grp`, and none for `.rk-item`. So the loading state falls back to a stack of bars in a `.skcardbox`: measured, 410 promised against 468 drawn at 1280 and 656 at 390, where a product name wraps to three lines. The 8.38 rule «the skeleton wears the component it promises» has no way to be obeyed here | the owner of stage 08 |
| S4 | **`microcopy.md` has no cluster for this screen** | interface strings belong to `voice/docs/microcopy.md` and the inventory ends at the 25 `account*.html` files that existed at stage 05. Six strings were needed and none of them is in any owner document: the H1 «Мої стейпли» (taken from `ia/docs/sitemap.md`, which is the only place the product ever writes it), the subtitle, the list lead, and the empty and error titles and bodies. They are written strictly to `voice.md` - plain words, no urgency, a reason and an exit in every state - and they are **proposals awaiting the owner of `microcopy.md`**, not canon. Everything else on the four screens is quoted: «У кошик», «Спробувати ще раз», «До каталогу», «Мої замовлення», «Повідомити про надходження», «Повідомимо, коли з'явиться», «Додано в кошик», «В наявності / Залишилось мало / Під замовлення», and the two consumption sentences taken word for word from `design/account.html` | the owner of `microcopy.md` |
| S5 | **the IA node has no number** | 7.0-7.7 are taken and `ia/docs/sitemap.md` names the feature under «Open items» without a node. The four titles carry `(wireframe 7.x)`, the same form `quiz.html` uses for node 4.x, and the breadcrumb has a parent it can point at. Assigning the number is stage 03b's | the owner of `ia/` |
| S6 | **a staple row has no route to its product** | `restock-note` renders `.rk-nm` as a plain div and the row's only control is the cart button. On the dashboard nudge that is right - it is a nudge. On a page whose whole subject is those products, «open this one» is a missing exit, and it is a component change (a link finish for `.rk-nm`, with hover and focus in both themes), not a screen one | the owner of stage 08 |
| S7 | **the consumption cycle is one of the numbers that has to stay `[?]`** | root `CLAUDE.md` names «consumption cycles» in the same breath as the coach tier and the loyalty thresholds. `design/account.html` nevertheless ships «Купували 34 дні тому · зазвичай вистачає на ~30 днів» as canonical sample data. The two rows that carry a cycle here are that exact string, quoted; the other three carry none, and the lead says out loud that the rhythm is computed from real orders `[?]`. **Which of the two readings is right is not this screen's to settle** | the owner, with real purchase data |
| S8 | **one feature, three names, two job numbers** | `ia/docs/sitemap.md`, `ia/sitemap.html`, `ia/concept-map.html` and `research/cjm-to-be.html` say **«Мої стейпли»**; `research/docs/jtbd.md`, `strategy.md`, `research.md`, `personas.md`, `ux-patterns.md` and `aarrr.md` say **«My Staples»**; `handoff/handoff.html` section 10 says **«Мій набір»** and files it under «out of MVP scope from stage 01», which is a different verdict from «post-launch P2». And the job differs too: `research/docs/cjm-to-be.md` files it under **job 3** twice, while `jtbd.md` puts «My Staples» inside **Job 4, reorder without effort**, and job 3 there is «verify product safety before buying». The screens follow «Мої стейпли» and Job 4, because those are the two the render-text document and the job document say. Both disagreements are recorded, not patched | the owner |

**S1 and S2 are the same shape and it is the shape this stage keeps finding: a declared list asked
in one direction only.** The registry knows the screen, the map does not, and nothing fails. Every
other class of defect in this file was caught by an instrument; these two were caught by adding a
screen the corpus had never been asked to hold.

## «Знову в наявності» - nine orders from the second feature built after the handoff

**Post-handoff, built from `handoff/docs/one-shot.md` alone.** Four screens landed flat in `design/`
and four rows in `design/_nav.js`: `product-oos-notify` (the request form on a sold-out product),
`product-oos-notify-done` (the request accepted), `account-waitlist` (the products being waited for)
and `account-waitlist-empty`. The feature is post-launch by decision - `ia/docs/flows.md` «Backlog»
(the stockout reminder, Decision 4) and `research/docs/cjm-to-be.md` Backlog P2 - so nothing above
`design/` carries it, and every row below is a thing the screens could NOT do for themselves.

| # | Order | Why the feature could not close it | Whose |
|---|---|---|---|
| R1 | **A door into the request form from `design/product-oos.html`.** That screen already carries the compact `.notifyrow`; its accent button focuses the field and goes nowhere. One `href` would join it to the new form state | the screen belongs to another author and is read-only for a feature pass. A hand-edit there is exactly the class this backlog exists to stop | owner |
| R2 | **A row in the buyer account rail.** `WF_ACC_LINKS` lives in the frozen `wireframes/_nav.js`, and «the sidebar has one source». Both new cabinet screens call `wfAccountNav('waitlist', …)` with a key the list does not hold: the rail draws correctly and marks **nothing** current - measured, `aria-current` count 0 on both. **Until the row exists the section is unreachable from inside the cabinet** and is entered only from the product page | unfreezing one line of the grey runtime is a product decision, not a visual one | owner |
| R3 | **A confirmation plate a BUYER screen can use.** The system has exactly one «accepted» plate, `.coach .cv-ok` in `coach-verify.css`, and it is locked to the `.coach` scope; `.nl-card` / `.nl-check` are newsletter-scoped; every status pill name is page-scoped (`.oh-status`, `.aord-status`, `.ci-oostag`, `.rbadge`). A screen that has to say «прийнято» anywhere else has nothing to reach for, so `product-oos-notify-done` carries its confirmation as `.oosnote` with `role="status"` - true, calm, and thinner than the decision deserves | a new component (or an unscoping of `cv-ok`) is the system owner's call, and a screen may not draw one | stage 08 |
| R4 | **`restock-note.css` holds two jobs under one name, and this feature made the second one the bigger.** Its own stand page prints the split: `.restock .rk-*` is «ваш запас» (the reorder nudge) and `.notifyrow .oosbtn .oosnote .oosback` is «наш склад» (out of stock, we will write). The second family was on ONE screen; it is now on three, and the `.rk-item` row is used by a list that is not a reorder nudge at all. Either the file splits, or section I gains the rule of use for the row: **«a `.rk-item` row names a rhythm OR a wait, one action per row, never a product's properties»** | naming and the level ladder | stage 08 / stage 09 |
| R5 | **No cluster in `microcopy.md`, so eleven strings are PROPOSALS, not canon** (listed below the table). The inventory was taken from the 142 grey pages and this surface did not exist at stage 05 | a cluster is `voice/docs/microcopy.md`'s to open | voice, stage 05 |
| R6 | **`ia/docs/flows.md` and `ia/docs/concept-map.md` disagree about this feature, in writing.** `flows.md` line 460 calls the back-in-stock notify «Decision 4, **entity E10**»; `concept-map.md` says E10 is **My Staples List** and that the stockout reminder is «**a notification rather than a screen**». Both cannot be true, and the second is now also stale, because the reminder HAS screens | an IA correction, and it decides whether this feature gets a node number at all - the four screens carry `7.x` and `3.0` because no node exists | stage 03 / owner |
| R7 | **The notification CHANNEL is `[?]` and three files answer it differently.** `cjm-to-be.md` P2 says «back-in-stock **e-mail**»; `concept-map.md` E10 says «**Email** reminder preference»; `design/product-oos.html` has said «**E-mail або телефон**» since stage 08; `ia/docs/pages/account.md` Open questions keeps «channels (SMS/e-mail) … `[?]`». The screens follow the corpus (contact = e-mail or phone) because that is what the product already renders | a real-data decision, like every other `[?]` | owner, with real data |
| R8 | **A request DATE has no canonical value.** `rollout.md` section C fixes names, prices, the order number and the promo deadlines, and nothing else. «Чекаєте з 12 серпня / 5 серпня / 29 липня» are sample content in the same class as «Купували 34 дні тому» on `account.html`, chosen to sit before the corpus's own clock (bonuses burn 20.09.2026). They are NOT `[?]`: `[?]` marks a number that needs real data to be correct, and a sample date needs a real ORDER, not a datum | a row in section C if the feature ships | stage 12 owner |
| R9 | **`voice.md` asks for a non-breaking space and the product has none.** «₴ після суми з нерозривним пробілом: `1 740 ₴`» - measured across `design/*.html`: **0 files** carry `&nbsp;` or U+00A0, and no instrument in `tools/` asks. It shows: in `.rk-nm` at 360 the unit «г» drops to a line of its own, the same widow class the one-shot prompt warns about for the currency sign. Not repaired inside the feature, because a nbsp inside «Gold Standard 100% Whey 2270 г» would make a second edition of a string 70+ files already spell | typography, and it needs an instrument before it needs a fix |

**The eleven proposed strings**, written to `voice.md`'s rules (plain words, one action one name, no
urgency, no celebration, states name a fact and an exit) and **not canon until the cluster exists**:

| Where | String | Note |
|---|---|---|
| section H1 · breadcrumb · rail label | **Знову в наявності** | the feature's own name; the dictionary's availability words are «В наявності / Залишилось мало / Під замовлення / Немає в наявності», and this is the section where the fourth turns back into the first |
| section sub | Товари, про надходження яких ви просили повідомити. | |
| card lead | Напишемо на {контакт} - один раз на товар, щойно він буде на складі. | |
| row meta | {бренд} · чекаєте з **{дата}** | |
| row action · PDP action | **Вимкнути** | ONE name in both places (voice §Повторювані дії). Full accessible name «Вимкнути сповіщення про надходження», which contains the visible label |
| toast | Сповіщення вимкнено | same verb as the button |
| field label | E-mail або телефон | |
| field hint | +380 __ ___ __ __ або name@mail.com | the label says WHAT, the hint says HOW (voice §Поле форми) |
| form note | Напишемо один раз - коли товар знову буде в наявності. Сповіщення можна вимкнути будь-коли. | |
| confirmation | **Повідомимо, коли з'явиться.** Напишемо на {контакт} - один раз, щойно товар буде на складі. | the first sentence IS canon - `voice.md` §Наявність fixes it as the OOS confirmation. Only the second is a proposal |
| empty state | Ви поки нічого не чекаєте · Коли товару немає на складі, натисніть «Повідомити про надходження» на його сторінці - і він з'явиться тут. | «Повідомити про надходження» quoted from canon |

**What the four screens deliberately did NOT do.** No new component, no new token, no `@media`, no
`transition`, no class the system does not declare - `node tools/screen-css.mjs` reads «чисто» on all
four. The heading ladder was matched to the nearest neighbour rather than repaired: `product-oos-notify*`
report «H4 після H2 - Розсилка» exactly like `product-oos`, and `account-waitlist*` report «H4 після H1»
exactly like `account-wishlist` - the footer's newsletter `h4` is debt E2 and adding a screen inherits
it without adding a cause. **And the coverage map does not show any of the four**, which is the open
order already written above for `account-staples`, not a second one.

---

## The contrast ruling of 25.08.2026, and what it does NOT cover

**Debt B2 above shrank from 37 to 12 without a line of css changing**, which is the whole point of
writing a debt as «the number that has to reach zero» rather than as «shapes that fail». 25 of the 37
were one pair read from both ends - `#FF5A00` against white - and the owner ruled on 25.08.2026 that
it stays: charcoal ink on Signal Orange is rejected as a look, so `--text-onaction` keeps
`var(--white)` and `.btn--accent` keeps 3.13. A10 had already taken the same pair in the pale-ground
direction on 12.08.2026; the two halves are now one closed decision, written up in `docs/decisions.md`.

**The 25 are not deleted from the record, they are moved out of the target.** A target nobody intends
to hit is how a register starts lying, and a debt that quietly drops its largest group is how the
same register starts flattering itself. The count stays printed beside B2 so the cost stays visible.

**What is left is smaller and much more specific than the old number suggested.** Of the 12: eight are
stand pages - the word «фото» in a placeholder, `code` on `kit/why`, `u` on `kit/geometry`, `span.btn`
on `kit/census`, `p.kp-p` on `kit/overlay`, `button.btn--text` on `kit/button`, `span.ar` on
`kit/pixel-proof`, `a.ph` on `kit/product-card` - documentation rather than product. Four are on
product screens, and two of those are `div.cl-ph` and `div.ms-ph`, the media placeholder, whose ink is
the word «фото» behind a photograph that does not exist yet.

**So the real residue is two shapes on one screen:** `a` («Головна») at 2.20 and `p.lintro` («Усе
спортивне харчування Stack») at 2.53, both on `search-suggest`, both failing in the light theme too,
so neither is the theme's doing. One screen, two lines of ink, and it belongs to the owner of stage 08
like every other change under `design/system/**`.

**And one shape is the theme's own, still:** `a.ph` on `kit/product-card` reads 8.88 in light and 3.17
in dark. It is the only one of the 37 the dark theme broke, and it is on a stand page.

---

## The quiz's emoji, the sixth time this shape arrived, and the blind zero underneath it

**Owner, 25.08.2026, with a screenshot of quiz step 1: «а що на квізі роблять іконки емодзі».** Six
goal marks were being drawn by whatever face the machine happens to have, next to a product that
draws every other mark from one 63-glyph set.

**All six already had a map row.** `UIV_EMOJI` has carried `💪 trending`, `🔥 flame`, `🌿 leaf`,
`⚡ bolt`, `🛡️ shield`, `🏃 pulse` since the set was drawn. What was missing is the PASS:
`uivChrome()` names the regions it walks - six chrome ids, then a short list of scoped selectors -
and `.q-opt .ic` was never one of them. This is the sixth time this exact shape has been reported,
and the fifth was reported the same way, by the owner, with a screenshot.

**Closed for the quiz by rule and not by hand:** one scoped call added to the pass, two map rows
added (`🌱 leaf`, `✅ check`), six mark slots removed from the two ORDINAL ladders because a trophy
for «досвідчений» is a rank badge and loyalty gamification is out of MVP scope. **Zero new drawings;
the set stays at 63.** Verified with `glyphs.mjs` on `quiz` and `kit/quiz` - none on both questions -
plus `accept.mjs` at 360 and 1280, both themes, and every mark now an `aria-hidden` svg rather than
an emoji a screen reader reads aloud.

**What the same run said about everything else is the expensive half.** `a11y.md` F1 read «5 marks
left, every one on a stand page. On the 141 product screens: 0». The measurement after the quiz
repair: **11 marks the map knows, still letters, 18 occurrences on 7 product screens**, and **19
pictographs with no row at all, 29 occurrences on 11**.

**And F1 was never true.** The saved output of the step-4 run - 24.08 at 18:16, the run the register
was written from - already prints ten product rows under question 1. Two runs of 23.08 print «none»
there. The number in F1 looks inherited from one of those and was never re-asked after the corpus
grew. The register's own sentence is «a zero from an instrument that cannot see the class is not a
zero»; this instrument saw the class, printed it, and the row recorded a different section of the
same output. **A named instrument, run on time, read in the wrong place - that fails more quietly
than one that was never run.**

**The eleven screens, and what closing them costs.** `content-about`, `content-contacts`,
`content-delivery`, `content-faq`, `content-guarantee`, `content-promo`, `content-returns`,
`content-reviews`, `maintenance`, `search-suggest`, `system`. Eleven of the nineteen unmapped
pictographs land on drawings that already exist - `💳 card`, `🕘 clock`, `⏱ clock`, `✉ mail`,
`🔗 link`, `📄 doc`, `🔄 refresh`, `↩ ret`, `🔒 lock`, `🚚 truck`, `💰 coin`. Eight do not: `🤝`, `🛠`,
`🗺`, `💵`, `📱`, `📞`, `🗓`, `💬` - each is a drawing decision rather than a lookup, and they belong
to the owner of stage 08 with the rest of `design/system/**`.

---

## `.q-opt` checkboxes never ticked - closed 25.08.2026, and it was one missing selector

**Owner, with a screenshot of quiz step 4.** Three rows orange, every square empty. The row carried
`.on` and `aria-checked="true"`; `span.cb` carried neither fill nor tick.

Two idioms, both legitimate, and this component had wired only one of them for `.q-opt`: the frozen
runtime puts `.on` on the SQUARE (`wireframes/_nav.js:1996`, every filter option since stage 04),
`.q-opt` puts it on the ROW - and `radio.css:178` already grants the row idiom to `.q-opt`'s OTHER
control. `checkbox.css` now carries `.q-opt.on .cb` and its `::after` twin, and the pressed pair
moves its `.on` from square to row. Verified both ways: the quiz ticks in both themes, and
`listing`'s three pre-checked filter boxes are still painted.

**Worth keeping for whoever writes the next state file.** All five hand-written `span.cb` in the
product are on the quiz, so this selector had no reader until 12.11 - and the batch that built the
screen could not have caught it, because a row with `.on` LOOKS chosen. The missing state is one
nobody photographs: a control mid-interaction is not a state FILE, so no screen walk ever reaches it.

---

## What building two product empty-states found in the components under them - 25.08.2026

Three defects, none of them in the new screens, and one of them was predicted in writing by the
component that carries it.

| # | Where | What | Fixed |
|---|---|---|---|
| 1 | `gallery.css` `<=859` block | `.gal` is a grid item and `margin-inline:auto` defeats `stretch`, so the frame sized to its CONTENT. Every screen so far had a thumbnail rail as its widest content, so it looked full width **by coincidence of the rail being there**. First product with no photographs: **96px inside a 358px column** at 390 | yes - `width:100%`, the declaration the `>=860` twin already carries. Measured both ways with only that line changed: three existing product screens identical to the pixel |
| 2 | `_nav.js` `uivPdp()` | the pass does `main.textContent=''` and appends the product shot unconditionally, painting a photograph over a frame that says it has none | yes - the guard sits on the gallery STEP (refusing the whole pass, as `product-loading` does, would cost the tabs, stars, trust icons, seal and ₴) and asks the question `gallery.css` already asks: `.loadnote` inside the frame |
| 3 | `empty-state.css` | `.empty` has no `:first-child` / `:last-child` pair, so a box with neither icon nor action charges margin for both absent neighbours - the defect 8.15 fixed for `.emptybox` and 8.39 measured as matching nothing on `.empty` | avoided, not fixed: the three exposed boxes take `.emptybox.mini`. **If a `.empty` box without an icon is ever built, this is the line it joins** |

**Defect 2 has a first draft worth keeping.** The markup was written as a bare text node inside
`.gmain`, and the words were invisible before the photograph even landed - `.gal .gmain` declares
`font-size: 0` so whitespace around an `<img>` cannot push it. That is exactly what step 8.16 wrote
down («in the COLOURED layer the placeholder is not faint, it is INVISIBLE») and answered by choosing
`.loadnote`, whose own `--fs-14` beats the inherited zero. **The component had recorded both the trap
and the way out, and the screen still had to be built wrong once to read it.**

## `inventory.mjs --apply` converges in two passes, and the first one looks like success

**Reproduced 25.08.2026** after two component files changed size. `--apply` repairs the `Lines`
column and writes the per-level summary lines from the rows it read at the START of the run, so the
summaries are rewritten from pre-repair numbers. First run left «рівень 1: сказано 4763, у таблиці
4783»; the second closed it. The tool's own comment claims the repair - «`--apply` rewrites them from
the tables, the same way it rewrites the `Lines` column» - and it does, one pass late.

**Zero looks like:** rows recomputed after the `Lines` rewrite, so one `--apply` is enough. Until
then anyone running it once sees a tool that printed its repairs and left the file wrong, which is
harder to notice than a tool that did nothing.

---

## The roadmap panel did not mark the current page - closed 25.08.2026

**Owner's report, and it was one line in `/_nav.js`.** `URL().pathname` keeps percent escapes,
`location.pathname` was being decoded, so a checkout folder with a SPACE in its name never matched
its own prefix: no active stage anywhere, and **zero of the declared `NAV_SECTIONS` rendered** on any
of the 37 pages that carry the panel.

**It is invisible over http**, which is why it survived every gate: the site is served from
`/stack/`, nothing to escape. Every browser instrument in `tools/` calls `serve()`. `clone-test.mjs`
opens these pages from `file://` and passed, because it asks whether a page OPENS.

`tools/nav.mjs` now asks the missing question over `file://`, with its wrong versions recorded.
**Proven red before being believed**: 33 findings on the pre-fix file, 0 after.

**Open, and it belongs to whoever owns `tools/`:** `clone-test.mjs` and `nav.mjs` now stand on the
same protocol asking different halves of one question. If a third `file://` question ever appears,
they should be one walk rather than three.

---

## Neither panel held your place across a click - closed 25.08.2026

**Owner's second report on the same panel**, and it was two panels with two different causes. The
roadmap (`/_nav.js`) had no memory at all - measured at 1280x680, four of the seven registry pages
overflow their box, `design/overview` drawing 805px of panel into 596. The stand's rail
(`design/kit/_nav.js`) had `scrollIntoView({block:'center'})`, which RE-CENTRES rather than
preserves: 4160px of rail in a 900px box, and for the first ten items of a hundred the centre is
literally zero.

**The rule already existed and had no check under it.** `design/_nav.js` (`uivRailCurrent`) states
it in a comment: «`scrollLeft` ON THE RAIL, NOT `scrollIntoView()` - the latter scrolls every
scrollable ancestor». Stand pages load `base.css`, where `html` carries `scroll-behavior: smooth`.
The prose was right, it was argued for, and the file next door kept the forbidden call for weeks.

Closed by one mechanism in both registries: the offset is remembered per tab and restored before the
first paint, then corrected by a NUDGE only when the current row would fall outside. `tools/nav.mjs`
grew questions **D** (the row is inside its box, 150 pages) and **E** (the place survives a click,
asked as a hot-versus-cold difference). **6 findings before, 0 after, same eight pages.**

**And then a third report found the panel that was actually meant.** `.uiv-side .us-nav`, the
product's own screen navigator - 143 screens on every coloured screen - had no memory and not even a
centring: on `coach-home` the row saying where you are sat at **1779..1813 inside a box of 717**. It
is INJECTED by `uivBar()`, so no `id=` exists for it in any markup, and a subject grepped from html
could not see it through two passes of fixing the wrong panels. It is discovered from `DESIGN_NAV`
now. **10 findings before, 0 after.**

**Open, and it is a debt of the SHAPE rather than of the code:** the mechanism exists in THREE files,
because no file can be reached by all three registries - the root pages do not load the design
system, the product screens load nothing from the root. The bodies are identical to the character
and `tools/nav.mjs` question F compares them, proven red on a one-character change. A drift will be
caught rather than shipped, but one edition would still be better than a good check over three.


## The footer trust cards are not links - open, owner's call

`ia/docs/pages/footer.md` section A specifies four trust cards, each «a crawlable `<a>` to the
matching info page»: Delivery 8.4, Guarantee 8.8, Payment 8.4, Returns 8.5. The grey runtime built
them as `<div>` and the colour layer repaints on top, so on **129 screens** the strip is four
statements that lead nowhere.

**Not folded into the size change of 26.08.2026**, and deliberately: this is tab order, the landmark
register and four new links per page, on a block that appears on nearly every screen. A size is a
size.

**Zero looks like:** four `<a class="wff-tc">` with the node's own destinations, `focus-visible` from
the system, and `a11y.md` gaining the row. **Measured today:** `document.querySelectorAll('.wff-tc a')`
returns 0 on every coloured screen.


## Six glyphs put paint outside the safe area - open, and none of them is new

`design/kit/icons.html` measures the live set on open and names them: `spark`, `bell`, `card`,
`cart`, `sun`, `cap`. Paint runs past `2..22` - `spark` and `sun` by a full module on both axes,
`bell` and `cart` on one.

**Found on 26.08.2026 while adding six drawings**, by asking that page whether the NEW ones cross the
line. They do not; these six already did. Nothing was changed about them in that step, because a
glyph's geometry is a drawing decision and six of them is not a side effect of adding a seventh.

**Zero looks like:** `n-safe` reads 0 on `design/kit/icons.html`. **Today it reads 6**, and the page
prints the exact overrun per axis for each.

## `content-legal` keeps a third carrier for its index - open, owner's call

**Measured 01.09.2026, when the owner asked why the table of contents on `content-delivery` sits as
a box at the top of the page instead of beside the text.** The honest answer was that nobody had
decided it: the grey layer drew that box at stage 04, the rollout renders the grey and invents
nothing, and stage 10's width audit gave the screen one line about its BODY - the furniture around
the body was never a row in that table. Three service screens now grow a sticky rail at 860 and one
pass marks the section you are reading on every carrier in the product.

**What was NOT taken.** `content-legal` keeps `.chips--sticky` - a pinned horizontal strip of pills -
on every width, so on a wide screen the product answers «where am I in this document» in two shapes:
a vertical rail on three service pages and a horizontal strip on the fourth. That is not an oversight.
The strip is a decision of 12.10 with measured arithmetic under it (the header publishes its height,
and eight pixels of every pill used to sit behind it), and swapping a carrier that works is the
owner's call rather than a side effect of a step about a different page.

**The numbers if it is taken.** Three sections, the longest label «Політика конфіденційності» at 190px,
so it fits `--container-index` (256) with room. The strip is 65px tall and stands at y=319; the rail
would return those 65px to the prose and put the index beside 2814px of document instead of above it.
The cost is the phone: the strip is the only in-page index in this product that stays with you while
you scroll, and a vertical box of three entries is about 116px, which sticks acceptably where
`content-delivery`'s five entries at 221px do not.

**Zero looks like:** `tools/page-index.mjs --census` prints one carrier shape for every service page.
**Today it prints two** - `Зміст сторінки` on three pages and `Розділи` on `content-legal`.

## The in-page index does not stay with you on a phone - open, and it is the same question

Below 860 every carrier except `content-legal`'s strip scrolls away with the page. On
`content-delivery` that is 221px of index over 4236px of document: it is on screen for the first
eighth of the page and absent for the rest, which is where «where am I» is actually asked. A vertical
list of five entries cannot stick - it would take a quarter of a phone screen - so the answer, if
there is one, is the shape `content-legal` already uses, and that makes this entry and the one above
it two halves of one decision.

**Zero looks like:** a carrier that is pinned on narrow widths on every service page. **Today one of
five is.**


## The six service pages now sit in two frames - open, and it is the tail of one decision

**01.09.2026.** `content-about`, `content-delivery` and `content-returns` carry an index rail, so
`.wf-page` narrows to the pair - 1014px - and the whole column centres. `content-contacts`,
`content-guarantee` and `content-legal` carry no rail, so they keep the 1200 frame with their prose
against the left edge and about 480px of air beside it at 1600.

**This is not a defect of either rule and it is not invisible either.** Two service pages of the same
product, opened one after the other, put their first line in two different places. The three fixes
are all real decisions and none of them is free: give the other three a rail (needs an index each,
and `content-contacts` has no sections at all); narrow every service page to the document frame
whether or not it has a rail; or accept that a page with a rail is a different shape from a page
without one. **The third is what stands today, unchosen.**

**Zero looks like:** one frame width across the six. **Today it is two** - 1014 on three pages and
1200 on three.

## The form-row rung is written twice, and the copy ends in the markup - open, owner's call

**02.09.2026.** `menu.css` has carried the sentence since stage 07: «a menu built FROM a field keeps
the FIELD's rung, not the button's - a form row must not change height because the kind of control
changed: 44, 16px, medium». It was right, and it had no NAME, so when the city picker on
`coach-verify` needed the same rung a stage later it could not find it and stood there at the
button's 52.

`button.css` names it `.btn--field` now, and the four declarations exist in both files. Sharing one
selector across them was tried and withdrawn inside the hour: `tools/idle.mjs` failed `button.html`,
because a class named in a file has to be shown on that file's stand page and the button page cannot
show a menu. **A selector list is not a free way to share a decision - it moves ownership too.**

**What actually removes the copy is a markup change:** `uivMenuFromSelect` emits
`btn--outline menu-trig`, and emitting `btn--outline btn--field menu-trig` would leave `menu.css`
with nothing to say about the rung. It also moves the trigger's corner from 8 to 12 and its content
from centred to the left on every screen a select-menu stands on. That is a visible change, so it is
the owner's and not a side effect of a step about a different screen.

**Zero looks like:** `tools/dupe.mjs` finding no group that is `min-height + padding + font-size +
font-weight` in two files. **Today it is one group of four declarations**, which sits in the idiom
range and does not fail the run - which is exactly why it needs a line here instead.

## `.ccard` still pads at 15px - open, and it is the last of the coach flow's off-ladder three

`coach-clients.css` carried three numbers on no rung, each with its own `[?]`: `.cl-tools` at
`gap: 9px; margin: 18px 0 4px`, `.clist` at `margin-top: 14px`, and `.ccard` at `padding: 15px 16px`.
**The first two were closed on 02.09.2026 because they were causing the defect the owner pointed
at** - the 14 sat on one column of a two-column split, so the panel beside the list began 14px higher
than the first card. The 15 causes nothing: it is a card's inner padding, and moving it to 16 changes
the height of every client card on four screens for no reason anybody has asked for yet.

**Zero looks like:** no literal length in `coach-clients.css` outside a query. **Today it is one.**

## The two city pickers now agree on everything but one weight - open, owner's call

**02.09.2026.** `checkout`'s `.co-city` and `coach-verify`'s `.city-chip` are the same control on two
screens, and after 12.14 they measure the same: 44 tall, a 12px corner, 8/12 of padding, content read
from the left, a 16px value, a 20px place mark, and a `--fs-12` muted change word pushed to the far
end of the row. **One declaration is still different: the change word's weight.**
`.city-chip .ch` is `--fw-bold`, `.co-city .note` is `--fw-semibold`. A third instance,
`.co-saved .ch` in this same file, is `--fw-semibold` - so it is two against one and the odd one out
is the coach's.

**It was not changed here on purpose.** The step was about `checkout`, and moving `coach-verify`'s
weight is an edit to a screen the owner has already looked at and accepted, for a difference of one
step at 12px in muted grey. Measured, the two words come out 66.23 and 67.16 wide - under a pixel
apart - which is why nothing on either screen looks wrong today and why this is a line here rather
than an edit.

**Zero looks like:** one weight for «the change word beside a city value», in one place. **Today it
is two weights over three instances.**

## A media query's unit: 86 in px against 9 in rem - open, owner's call

**02.09.2026.** Counted across `design/system/`: **86 `min-width` queries written in px and 9 written
in rem.** Both forms pass `tools/bp.mjs`, because it asks whether the number is in the registry and
`53.75rem` resolves to the registered 860. Neither form is wrong; having both is.

**The argument for rem is not taste.** A px query does not move when a person sets a larger default
text size; a rem one does, so the layout changes shape at the same POINT IN THE TEXT rather than at
the same point in the glass. Stage 10 already moved the type ramp from px to rem value for value for
exactly this reason, and `tools/accept.mjs --text200` exists to ask the question. Nothing has ever
asked it of the queries.

**The argument against changing is the count.** 86 rules across 95 files, each of which would need
its own measurement at 200% text before anyone could say the move improved anything - and a bulk
sed over that set is the hand fix this project forbids, wearing a script.

**What is settled here and now:** `coach-landing.css` writes the 860 boundary FOUR times and all four
say `860px`. One file stating one boundary in two units is the same defect 10.4 round 3 found in this
very file (479 against 480) and it does not ship, whatever the system-wide answer turns out to be.

**Zero looks like:** one unit for every `min-width` in `design/system/`, and a measured reason for
which one. **Today it is two units, 86 against 9.**

## `microcopy.md` has no membership check against the corpus - open, and it is an INSTRUMENT gap

**02.09.2026.** Every declared list in this project is asked BOTH ways: membership («is each thing on
the list really in the product?») and existence («is each thing in the product really on the list?»).
`tools/inventory.mjs` asks both of a component. `tools/dead-sel.mjs` asks both of a selector.
`design/visuals/README.md` states the rule for images in one sentence - «an image with no row here, or
a row with no screen, is the same defect in two directions» - and `tools/glyphs.mjs` asks both of a
mark.

**Nothing asks it of a product string.** `voice/docs/microcopy.md` is 3220 rows naming what every
screen says, and there is no check that a row still has a screen, or that a screen's string still has
a row.

**It cost a defect the same afternoon it was noticed.** 12.15 deleted the hero's hatch and the label
printed inside it; the row `| Hero | підпис поля | Мультиклієнтська сесія замовлення | art
placeholder |` stayed. One hour, one file away, and the only reason it was found is that 12.16 edited
the five rows directly beneath it. A row nobody deletes reads as a string the product still says.

**What the check would ask.** For each row, does its text appear on any screen of its zone's page
(after the icon and mark passes have run, since a leading emoji is a mark and not a word)? And for
each visible product string on a screen, is there a row? Both counts printed, both zeros named. The
hard part is not the walk, it is the MATCH: strings carry `{placeholders}`, `[?]` markers, `[UGC]`
values and dynamic numbers, so a naive equality test would report hundreds of false rows and be
switched off within a day. The wrong versions have to be written beside it.

**Zero looks like:** a row in `tools/` that reads `microcopy N rows · без екрана: 0 · рядок без
екрана: 0`. **Today there is no such row, and the number is unknown** - which is worse than a number
that is wrong, because nobody is arguing with it.

## `fluid` means two things in the Width column - open, and it is an INSTRUMENT question

**02.09.2026.** `tools/inventory.mjs` labels a component `fluid` when its css matches
`auto-fit|auto-fill|minmax\(|flex-wrap`. Three of those four say the same thing - **the browser
chooses how many tracks there are**. The fourth does not: `minmax(0, 1fr)` is the min-width guard this
project writes everywhere, because *a grid item's automatic minimum size is its CONTENT, not zero*,
and a single long word in a `1fr` track blows the whole row.

`coach-landing.css` demonstrated both inside one afternoon. At 12.16 the row lost `fluid` because the
five value labels stopped being a wrapping chip row - a true reading. At 12.19 it regained `fluid`
because the closing band's grid is `minmax(0, 1fr) 15rem` - a guard, not a choice. **The same word,
the same column, two different facts, and nothing in the output distinguishes them.**

**Why it is not fixed here.** Splitting the label costs a second word in a column that 94 rows carry,
and every row would have to be re-read to know which of the two it meant all along - some files hold
both. That is a corpus pass, not an edit, and the wrong version is obvious enough to write down now:
**matching `minmax\(` and calling the result «not fluid» would be wrong too**, because
`repeat(auto-fit, minmax(8rem, 1fr))` contains it and is the fluid case exactly.

**Zero looks like:** two words - say `fluid` for a browser-chosen track count and `guard` for a
min-width floor - and a row that carries whichever it actually has. **Today it is one word over 94
rows, and nobody knows how many of them mean which.**

---

## OPEN 12.20 - our own tub is labelled with another company's product name

**Where.** `design/_nav.js:783`. The mega-menu's product placeholder injects
`visuals/product-whey.png` with `alt="Gold Standard 100% Whey"`.

**Why it is a defect now and was only a smell before.** That render is the **Stack** pack: black tub,
white Stack wordmark, orange S. The alt text names a product of Optimum Nutrition. Until 03.09.2026 it
was possible to read the file as a placeholder standing in for a catalogue item; on that date the
owner ruled that **Stack has its own line**, and a placeholder cannot stand in for a competitor's
product while wearing our mark. The string also reaches a screen reader, which is the one reader who
gets the wrong brand and nothing else.

**Why it is not fixed here.** The alt is one line, but the question under it is not: if the shop
carries its own line AND the brands `brands/` shows, then the mega-menu placeholder has to decide
which it is showing, and that decision belongs with whoever writes the catalogue. Renaming the alt
without that decision just moves the guess.

**Zero looks like:** the placeholder names what the picture actually is, and no product string in the
repository names a brand the picture does not carry. **Today it is one line, and it says the opposite
of the pack it points at.**

**Related, and outside this repository:** the UGC voiceover for the same shop says «Тут немає власної
марки. Тільки бренди, які ти й так знаєш із залу». After the ruling that sentence is false. It lives
in the Ad AI project, not here, and it is written down here only because the contradiction was found
here and would otherwise be found by an audience.

---

## CLOSED 12.21 - the sheet is one image, and one image is one point of failure

**Closed by the rebuild rather than by a repair, and the difference is worth the line.** The item
below was written against a build in which the claims and the film shared a fate: the beats' resting
state came on with `is-live`, `is-live` came on with the section, and only the film could lift them
again. The 12.21 rewrite separated the two by accident of a better structure - `run()` builds the
layer painter and the reels independently, and the painter runs every frame whether or not a single
sheet ever decodes. So a sheet that 404s now leaves exactly what the item asked for: the poster on
the stage, the title card and every claim arriving and leaving normally, and nothing on screen that
looks broken. Measured, not assumed: both sheet URLs blocked at the network layer
(`Network.setBlockedURLs`), page reloaded, and the section read back `is-live` on, both canvases at
opacity 0, the poster loaded at its natural 1280 and painted, and the layers still arriving one at a
time - the title card alone at p=0, the fourth claim alone at p=0.6.

**What is still true:** there is no `error` handler and no second attempt, so a person on a bad
connection sees a still photograph where a film was meant to be and is never told. That is a smaller
defect than the one below and it is the same owner's call - a poster is a finished page, and saying
«video failed» on a page that sells may be worse than saying nothing.

## OPEN 12.21 - a stylesheet waiting on a value nobody in `design/` supplies

**Where.** `design/system/components/nav-drawer.css:33` and `search-overlay.css:42` and `:44`, all
three reading `var(--dr-top, 56px)`.

**Who found it.** `tools/roles.mjs`, at 12.21, on the run that taught it a class it could not see
before: a `var()` name that is in NEITHER half of `tokens.css`. Splitting those out immediately
answered a question nobody had asked - who writes them - and two came back with no writer at all.

**What is true today.** `wfDrawerTop()` exists and publishes `--dr-top`, and it lives in
`wireframes/_nav.js`. The coloured product loads `design/_nav.js`, which has no such function, so on
every coloured page the drawer and the search panel take the literal fallback instead of the measured
header. 56px against the 57px the shell actually publishes as `--shell-header-h`: one pixel, and the
point is not the pixel. The channel has nothing on the other end, and the fallback is what hid it.

**Why it is not fixed here.** It is the shell, not this component, and 12.21 is a step about one
page's film. Touching `design/_nav.js` puts every screen back through acceptance.

**Zero looks like:** `roles.mjs` reports «змінна без письменника: 0». **Today it reports 2.**

## OPEN 12.20 - superseded, see CLOSED 12.21 above

**Where.** `design/system/scrollstory.js`, and `visuals/coach-box-fill.jpg` behind it.

**What is true today.** The film loads as a single 1006 KB jpg, and the canvas fades in only in that
image's own `load` handler - so a failed or slow sheet leaves a real poster on screen and the page is
finished rather than broken. That half is done and measured.

**What is not.** There is no `error` handler and no second attempt. A sheet that 404s or dies mid-
transfer leaves `is-live` on the section for ever, which means the five claims sit at 35% opacity
with nothing ever raising them - the beats' observer is independent of the film, so they do get
`is-in`, but a person on a bad connection watches a poster and a dimmed list and has no idea anything
was meant to happen.

**Why it is not fixed here.** The honest fix is not a retry, it is a decision about what the section
IS when its film cannot arrive: a poster with an undimmed list, or no story at all. That is the
owner's call about a page that sells, not a script's.

**Zero looks like:** a sheet that fails takes `is-live` off with it, and the section falls back to
exactly what a phone gets. **Today a failed sheet leaves a state that only success can clear.**
