# Transfer a finished site into a Vizen shop

> **Резюме по-русски.** Готовый сайт переносится **как есть**: папка = html-виджет
> уровня 3 в настоящем DOM страницы (скрипты работают, `position: fixed` работает,
> Shadow DOM и iframe нет); лейаут = папка общих стилей, шрифтов и фавикона;
> шапка и футер = html-виджеты в группах хрома; страница товара или раздела =
> шаблон в слоте `body`. Перепривязываются только данные магазина: меню, цены,
> корзина, формы. Здесь — что платформа делает с твоим файлом (§7), что едет 1:1,
> что перепривязывается и что не едет (§8), заметки по React/Vite, Tailwind и
> экспортам из дизайн-инструментов (§9). Итерации — дев-ключом: прод-ключ пишет
> в живой сайт сразу.

**Status:** draft · **Serves:** `GET /docs/transfer` · **Verified:** 2026-09-12, live shop vezu-vezu (company 14), storefront code, stand :3970 · **Owner:** tokens and web-coding docs line

## 1. What you can do here

A site that already exists — an HTML/CSS/JS folder, a static React/Vite export,
a Figma, Webflow or Tilda export — goes into the shop **as it is**. One folder
per page becomes one level-3 widget; one folder becomes the site skeleton (the
layout); header and footer folders become chrome widgets; product and category
folders become templates. Nothing is cut into sections, no path is rewritten,
nothing is wrapped in Shadow DOM or an iframe. What the platform adds is the
page around your file (header, footer, the site's own stylesheet) and the live
data you re-bind. §7 says exactly what happens to the file; §8 what cannot
travel.

The one-command path is `backend-3D/tools/html-transfer/transfer.mjs`; every
step below names the raw API next to it.

## 2. Decide first (forks)

| If you want | Take this path | Cost |
|---|---|---|
| a site that already exists in a browser | **transfer** — this document, skill `vizen-transfer` | data that the shop owns (menu, prices, cart, forms) is re-bound, not copied |
| a page that does not exist yet | build it — `vizen-start` and its three questions | not this document |
| one page inside the shop's existing frame | one folder, `--page <slug>`; the shop's header and footer stay | none |
| the whole site: your header, footer, fonts, product pages | layout folder + two chrome groups + templates + N page folders, in the order of §4 | the order matters: chrome after pages means restyling twice |
| static content (text, images, prices typed in) | copy as is | prices typed in lie the moment the catalogue changes |
| live content (menu, product lists, prices, cart, forms) | re-bind to the shop's data with `vz-` keys, or keep a platform widget | the feed in own markup is ≤ 48 rows without filters or paging; filters mean the platform `productListing` |
| your own header | an html widget in a chrome group; menu and cart counter from `vz-for menu` and `vz-cart-count` | in guarded zones it renders without its scripts |
| the platform header restyled | `siteHeader` with `props.wrapperClass` — `vizen-chrome` §4a | the look is bounded by its props |
| iterations while the site is being assembled | a **dev** key: a draft at `<slug>--dev.<zone>` under the owner's session | the owner publishes; nothing is visible to shoppers |
| the final publish | a **prod** key, or the owner's publish of the draft | every release on a prod key is live at once — one shop spent 16 header releases as 16 live edits |

## 3. Objects and where they live

Everything you upload is a **release** of a document; everything you place is a
**binding**.

| Object | Create | Read | Write path |
|---|---|---|---|
| html document (the project) | `POST /html-documents {"item":{"name":"…","level":3}}` → `result.id` | `GET /html-documents/{id}` → `source.url` = the active release's `index.html`, already clean | name and description only; content goes through releases (`HTML_PROJECT_USE_RELEASE` otherwise) |
| release (a snapshot of the folder) | `POST /html-documents/{id}/releases {"base":"active"\|"none","files":[{"path","sha256","size"}],"delete":[],"note":"…"}` → `{release_id, upload[], reused[], inherited[]}` → `PUT` each `upload[].upload_url` (no `Authorization`) → `POST …/releases/{rid}/publish` → `{url, path, embed_prefix, seo, report}` | `GET …/releases`, `GET …/releases/{rid}` (each carries `preview_base`) | `422 RELEASE_INVALID` carries `report.errors`; `409 FILES_PENDING` lists what is not uploaded; `POST …/rollback` moves the pointer |
| release from a ZIP | `POST /html-documents/{id}/releases/zip?publish=1`, multipart `file` — **the document must already exist**; nothing is mounted | same | ≤ 60 MB per request |
| html block (the widget on a surface) | `POST /content-blocks {"item":{"name":"…","sections":[{"type":"text","payload":{"v":2,"kind":"html","props":{"html":{"html_document_id":<doc>,"level":3}}}}]}}` | `GET /content-blocks` | `PUT /content-blocks/{id}` replaces `item` whole; `props.html.boxed: true` asks for the platform box |
| page | `POST /categories {"item":{"name":"…","type":"page","is_published":true,"seo":{"slug":"…"}}}`; the slug must be free (`GET /categories/by-slug/{slug}?company_id=…` → 404) | `GET /categories/by-slug/{slug}?company_id=…` → `content_blocks[]` | `PUT /categories/{id}/content-blocks {"id":<page>,"items":[{"block_id","sort_order"}]}` — a replace-set |
| group (an ordered list of widgets) | `POST /content-blocks {"item":{"name":"…","group_type":"chrome"\|"product"\|"category","sections":[{"type":"text","payload":{"v":2,"kind":"group","props":{"widgets":[<block ids>]}}}]}}` | `GET /content-blocks` | `PUT /content-blocks/{id}` |
| binding (which group, where) | `PUT /design/bindings/{shop\|category\|category_children\|product\|article\|theme\|system}/{id} {"slots":{"header":<G>,"footer":<G>,"body":<G>}}` — `shop` takes id `0`; `slots` is a replace-set | `GET /design/bindings/{type}/{id}` → `resolved` with the source of every slot; `GET /v1/storefronts/resolve?slug=…` → `chrome` | `{"slots":{}}` unbinds |
| layout (site skeleton and shared files) | exists: `GET /content-blocks`, rows with `group_type:"theme"`, the earliest is the shop's default | `GET /themes/{id}/site` → `base`, `scope_class`, `files[]`, `icons_generated[]` | `transfer.mjs ./theme --theme <id>`, or `POST /themes/{id}/site/releases` → `PUT` → `…/publish`; `--theme` and `--page` never together |
| page SEO | taken from `<title>` and `<meta name="description">` on publish | `GET /categories/by-slug/…` → `seo` | `transfer.mjs` writes `seo.meta_title` / `seo.meta_description` when empty (`--seo-overwrite` to replace); by hand: `PUT /categories/{id} {"item":{"seo":{…}}}` |

Scopes: `catalog:read`, `catalog:write`, `storage:write`; the role on the shop
must be owner or admin for level 3 (`HTML_SCRIPT_ROLE_REQUIRED` otherwise). The
server flag for authored markup is on in production.

## 4. Recipes — in the order of a real transfer

Each recipe ends with the check that proves it. "200" proves nothing here.

### 4.1 Inventory

Write one table before the first call:

| Source | Target | Data to re-bind | Does not move |
|---|---|---|---|
| `index.html` (home) | page widget, `--page home` | product strip → `vz-for="p in products"` or `productListing` | — |
| `partials/header.html` | chrome group, slot `header` | menu → `vz-for="m in menu"`, cart → `vz-cart-count` | — |
| `css/`, `fonts/`, `favicon` | layout folder, `--theme` | — | — |
| `product.html` (mock-up) | `product` group, slot `body` | `{{ product.* }}`, `vz-add-to-cart` | own fetch of `/api/products` |
| `app.jsx` router, state | — | — | SPA runtime: export routes statically first |

### 4.2 The key

`GET /v1/account/token`. `contour: "dev"` and `writes_to_live: false` — iterate
freely. `contour: "prod"` — every publish is live: say so to the owner and ask
for a dev key before the first release. `store.storefront_url` is the address
you will open; `warnings[]` go to the owner verbatim.

### 4.3 Layout folder — site-wide CSS, fonts, favicon, scripts

```bash
curl -s -H "Authorization: Bearer $VZ" $API/content-blocks \
  | jq '[.result[] | select(.group_type=="theme") | {id,name}] | sort_by(.id)'   # earliest = default
node backend-3D/tools/html-transfer/transfer.mjs ./theme --theme <id>
```

`index.html` here is the **skeleton**: `<head>` nodes land in the head of every
page, the `<body>` class on the page root, body content before `</body>`;
`*.css` has bare `html`/`body`/`:root` scoped to `.vz-theme-{doc}`; `icon.png`
generates the favicon set. Scripts read `window.VZ_THEME_BASE`. Full folder
rules: `/docs/own-markup` §7.2.

**Check:** `GET /themes/<id>/site` answers `base` and `scope_class`; open any
page and find `class="… vz-theme-{doc}"` on the page root and your stylesheet in
`<head>`.

### 4.4 Header and footer

```bash
node backend-3D/tools/html-transfer/transfer.mjs ./header --name "Header"   # prints: создан HTML-проект #<doc>
node backend-3D/tools/html-transfer/transfer.mjs ./footer --name "Footer"
```

Without `--page` the command creates the document and publishes the release;
nothing is mounted. Then three calls per part: the html block (§3), the group
(`group_type:"chrome"`, `props.widgets:[<block>]`), the binding:

```bash
curl -s -X PUT -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' \
  $API/design/bindings/shop/0 -d '{"slots":{"header":<G_header>,"footer":<G_footer>}}'
# one section only: /design/bindings/category/<id>; one layout: /design/bindings/theme/<id>
```

A sticky header is `vz-sticky` on your root node — it reserves its height;
`position: fixed` does not, so it is not the header tool (it works, and it is
fine for a side rail or a modal).

⚠️ **Guarded zones strip more than scripts.** On `/cart`, `/checkout`,
`/account` and the other guarded pages the widget is served in static mode: the
`<script>` nodes go, and a relative `<link rel="stylesheet" href="header.css">`
goes with them — the header renders unstyled (measured on the stand,
2026-09-12). Inline `<style>` survives, and the layout folder's stylesheet is
linked in the page `<head>` everywhere, guarded zones included. So put the
header's CSS in the layout folder (§4.3) or inline it in `<style>`; keep the
`<link>` form for pages only.

**Check:** `GET /v1/storefronts/resolve?slug=<shop>` → `chrome` names your
documents; open the home page, a product page and `/cart` — the header is
there on all three, on `/cart` without its scripts **and still styled**.

### 4.5 Pages

```bash
curl -s -H "Authorization: Bearer $VZ" "$API/categories/by-slug/home?company_id=<N>"   # 404 = free
curl -s -X POST -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' $API/categories \
  -d '{"item":{"name":"Home","type":"page","is_published":true,"seo":{"slug":"home"}}}'
node backend-3D/tools/html-transfer/transfer.mjs ./home --page home --validate-only
node backend-3D/tools/html-transfer/transfer.mjs ./home --page home
```

The first run creates the document and mounts the block **after** a successful
publish; a validator error leaves the page untouched. A rerun is a new release
and uploads only the files whose sha256 changed (`index.html` changes on every
run — the converter rewrites it). `--doc <id> --list` shows the history,
`--doc <id> --rollback` moves the pointer back. Older html blocks on that page
are unmounted (`--keep-blocks` keeps them).

The page keeps the platform's **technical layer** above your widget —
breadcrumbs and the page heading — because a page is a category and draws
its bundled blocks. For a 1:1 page switch that layer off with the page flags
on the zone block `__page:top` (`/docs/webcoding` §16.1): create or update it
with `{"kind":"zone","refs":[<your block id>],"page":{"hideSystemBlock":true}}`
and bind it with your block (`PUT /categories/{id}/content-blocks` is a
replace-set). `hideSiteHeader` / `hideSiteFooter` do the same for the chrome
when the page brings its own. `transfer.mjs` mounts the block and leaves the
layers as they are.

**Check:** open the `url` the command prints — on a dev key it is the
`<slug>--dev` host, under the owner's session — and confirm nothing of the
platform shows above your first section.

### 4.6 Product and category templates

```bash
node backend-3D/tools/html-transfer/transfer.mjs ./product --name "Product template"
# html block → group group_type:"product" with props.widgets:[<block>] → binding:
curl -s -X PUT -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' \
  $API/design/bindings/category_children/<root category id> -d '{"slots":{"body":<G_product>}}'
```

`category_children` reaches every product of the subtree; `/product/<id>` is
one product. A category template is a group `category` in the slot `body`;
keep the platform `productListing` inside the group when filters and paging
are needed — the own-markup feed is ≤ 48 rows. Inside the template the page's
product is `product` (`{{ product.name }}`, `{{ product.price_text }}`,
`product.gallery[].url`, `product.attributes[]`), the section is `category`,
`crumbs` and `menu` are everywhere.

**Check:** open two products — one card each; the printed price equals
`GET /products/{id}`; `vz-add-to-cart` puts the item in the cart.

### 4.7 Re-bind data

| In the source | On the platform |
|---|---|
| a hand-written menu | `<li vz-for="m in menu"><a href="{{ m.url }}">{{ m.name }}</a></li>` (children: `c in m.children`) |
| a product strip with typed prices | `<article vz-for="p in products"> … {{ p.price_text }} … <button vz-add-to-cart="{{ p.id }}">` |
| a product page mock-up | `{{ product.* }}`, `vz-gallery`, `vz-variants`, `vz-add-to-cart`, `vz-fav` |
| a contact form | `<a href="form:<id>">` (forms: skill `forms`, `/forms`) |
| internal links | `/path` on the site, or `vz:page/<id>` — never the export host |
| a cart icon with a counter | `<span vz-cart-count>` |

`vz-for` and `vz-if` go on different nodes. Names and scope fields:
`GET /docs/vz-keys.json` — an unknown `vz-` attribute is removed silently.

**Check:** `curl <page> | grep '<a real product name>'` finds it in the raw
HTML; adding an item changes the counter.

### 4.8 Check and report

- open by direct URL **and** by a click from another page of the site — two
  code paths;
- screenshots at 1440 and 390;
- console: no errors, no 404s under `/_html/`;
- `/cart` shows your header and footer;
- `--doc <id> --list` shows the active release.

The report: the links, both screenshots, what was re-bound, what was dropped
and why, the rollback command. The token appears nowhere.

## 5. Silently ignored

| You wrote | What happens | Do instead |
|---|---|---|
| `document.addEventListener('DOMContentLoaded', init)` | fires on a direct hit; after an in-site click the document is already loaded and `init` never runs — page two looks dead | call `init()` immediately and on `document`'s `vz:navigate` |
| `<meta property="og:*">`, `<base>`, `<link rel="manifest">`, `<meta http-equiv>` | dropped on publish (`head.stripped`) | og-images: the page's SEO fields; a manifest: not supported |
| `body { overflow: hidden }` from a modal script | applies to `.vz-body-{doc}`, not to the real body — no scroll lock | lock scroll on your own root element |
| `fetch('data/x.json')`, `img.src = 'assets/a.jpg'` in a `.js` file | resolves from the page URL → 404 on the site (`js.assets` warning) | prefix with the widget root: `dataset.vzBase` / `window.VZ_ASSET_BASE` |
| `<title>` and `<meta name="description">` when the page already has SEO | kept as they were | `--seo-overwrite` |
| `props.wrapperClass`, `blockWidth`, `paddingSides` on a level-3 block without `boxed` | there is no box to land on; nothing changes | your own CSS on your root; or `props.html.boxed: true` and then the axes |
| a second `.html` in the folder | not served (`page.extra`) | one page = one folder |
| a header, footer or template folder without `<title>` | a `page.noindex` warning — normal for a fragment that is not a page | ignore it for chrome and templates; pages carry a `<title>` |
| a file referenced only from a `.js` (`img.src = base + 'assets/a.svg'`) | a `file.unused` warning — the validator reads HTML and CSS references, not scripts; the file is uploaded and served all the same | keep the `VZ_ASSET_BASE` fallback in the script and ignore the warning |
| a path inside a CSS comment (`/* url("fonts/x.woff2") */`) | the validator reads `url()` inside comments too — `ref.missing` **error** for a file that is not in the folder | delete the example path or add the file |
| `<script>` in the header on `/cart`, `/checkout`, `/account` | not executed, no error | markup and styles only there |
| `<form action="…">` without a handler | a `form.nohandler` warning; the browser posts to the export host | `href="form:<id>"` |
| a `.vizenignore` with one line | **replaces** the defaults (`.*`, `node_modules/`, `previews*/`, `originals/`, `README*`, `*.md`, `qa.mjs`, `*.log`) | repeat the defaults you rely on |
| `--theme <id> --page <slug>` | refused: a layout is not mounted on a page | two commands |

## 6. Limits

| Limit | Value |
|---|---|
| files per release | 500 |
| `index.html` | 2 MB |
| any file | 50 MB |
| release / ZIP body | 60 MB |
| own-markup feed `products[]` | 48 rows |
| block `sections` JSON | 64 KB — never inline HTML there |
| `wrapperClass` | ≤ 3 names, `^[a-zA-Z_][a-zA-Z0-9_-]*$`, none starting with `vz-` |
| rate limits per company per minute | read 600, write 240, storage 180 |
| image file (advice, not a refusal) | ≤ 400 KB, `width`/`height` set (`img.large`, `img.nosize`) |

Validator codes — errors: `ref.outside`, `ref.missing`, `ref.http`,
`file.forbidden`, `sw.register`, `hash.mismatch`, `html.parse`; warnings:
`page.extra`, `js.assets`, `head.stripped`, `form.nohandler`; info:
`img.large`, `img.nosize`. Source: `internal/api/htmlrelease/validate.go`.

## 7. The runtime contract — what the platform does to your file

1. **Wrapper.** `<body class="dark" data-x="1">` becomes
   `<div class="vz-body vz-body-{doc} dark" data-x="1" data-vz-base="/_html/{doc}/{rid}/">`.
   Clean markup gets the same wrapper. There is no `.vz-box`/`.vz-inner` around
   it unless `props.html.boxed: true`. The div sits in the page `<main>` between
   the header and the footer.
2. **Head.** `link`, `style`, `script`, `noscript` are kept in a hidden
   `div.vz-head-{doc}` before the body — stylesheets load, scripts run; `title`
   and `meta description` go to the page SEO; everything else is dropped.
3. **CSS scoping.** Bare `body`, `html`, `:root` in `<style>` and in the
   release's `.css` files are rewritten to `.vz-body-{doc}`; compound selectors
   (`body.dark`) are left alone. `src`, `href`, `srcset`, `url()`, `@import`
   are rewritten to the widget root on output — the files themselves are not
   touched.
4. **The cascade is shared.** Level 3 is real page DOM without style isolation.
   The storefront's stylesheet applies to your markup, and it ships Tailwind
   v4 **preflight** in `@layer base` (measured on the live CSS, 2026-09-12):
   `*{margin:0;padding:0;border:0 solid}` · `h1,…,h6{font-size:inherit;font-weight:inherit}` ·
   `a{color:inherit;text-decoration:inherit}` · `ol,ul,menu{list-style:none}` ·
   `img,svg,video,canvas{display:block;vertical-align:middle}` · `img,video{max-width:100%;height:auto}` ·
   `button,input,select,textarea{font:inherit;background:transparent;border-radius:0}` ·
   `button{appearance:button}`. Your CSS is unlayered and wins on equal
   specificity, so everything you styled explicitly survives; what you left to
   browser defaults — heading sizes, bullets, link underlines, button chrome —
   is reset. Ship your own base styles under your root class. The page font is
   `ui-sans-serif, system-ui` at 16 px; your `body { font-family }` lands on
   `.vz-body-{doc}` only. A stylesheet linked from any level-3 widget reaches
   the **whole page** (the product card of vezu-vezu takes its font from the
   header's `global.css`); the editor canvas, however, loads only the layout's
   `<head>` — site-wide CSS belongs in the layout folder (§4.3).
5. **Scripts.** `<script>` and `<link>` run when the browser parses the page.
   After an in-site click the platform re-creates every `<script>` node of the
   widget so it runs again; a `CustomEvent('vz:navigate', {detail:{path}})` fires
   on `document` after every navigation and after the first load. Write code
   that works when executed right now, and re-init on `vz:navigate`. External
   and `defer` scripts have no `document.currentScript`: read the root from
   `.vz-body-<id>` or `window.VZ_ASSETS[<id>]`; two widgets on one page
   overwrite the bare `window.VZ_ASSET_BASE` for each other.
6. **Guarded zones.** On `/cart`, `/checkout`, `/account`, `/wishlist`,
   `/orders`, `/deals`, `/dashboard` your markup and styles render and your
   scripts are stripped server-side; the layout's `head_html`/`foot_html` are
   not emitted there either. Moving into such a page is a full reload.
7. **Paths.** `/_html/{doc}/{rid}/…` is immutable and served byte-for-byte —
   no resizing of your own images, so size them yourself. Shop images written
   as `<img src>` are rewritten to the resizer with `srcset`; set `sizes`.
   Only the active release is served; old rids answer 404 by design.
8. **Geometry.** The page `<main>` drops CSS layout containment when it holds
   an unboxed level-3 block, so `position: fixed`, `100vw` and `100dvh`
   resolve against the viewport (a fixed side rail measured 900 px tall in a
   900 px window; 844 in 844). The price: `.vz-vis-*` visibility ranges of
   other blocks do not work on such a page.
9. **Layout folder deltas.** Scope class `.vz-theme-{doc}` on the page root,
   real `<head>` tags, `window.VZ_THEME_BASE`, favicons from `icon.png` —
   `/docs/own-markup` §7.2.

Anchors: `packages/sections/src/sections/SectionHtmlAuthored.tsx`,
`storefront/src/widgets/html/Slot.tsx` (`hasUnboxedAuthored`),
`storefront/src/lib/render/PageShell.tsx` and `storefront/src/app/globals.css`
(`vz-has-authored`, `@import 'tailwindcss'`), `pkg/htmlrefs/fragment.go`
(`SetBodyBase`, scoping), `internal/api/catalog/get_html_document_content.go`.

## 8. copy · remap · drop

| In the finished site | Verb | Where it lands |
|---|---|---|
| page markup, CSS, JS, fonts, images | **copy** | page folder → level-3 widget on a `type:"page"` page |
| header, footer | **copy** + **remap** menu and cart counter | chrome group, bound to the shop, a section or a layout |
| shared CSS, fonts, favicon, a site-wide script | **copy** | the layout folder |
| product page, category page | **copy** the markup, **remap** the data | `product`/`category` group in the slot `body`; `{{ product.* }}`, `{{ category.* }}`, `products[]` |
| menu, product lists, prices, add-to-cart, wishlist | **remap** | `vz-for`, `price_text`, `vz-add-to-cart`, `vz-fav` — or the platform widgets |
| forms | **remap** | `href="form:<id>"`, the `forms` skill |
| internal links | **remap** | `/path`, `vz:page/<id>` |
| `<title>`, `<meta name="description">` | **remap** (automatic) | page SEO |
| browser-default styling (unstyled headings, bullets, links) | **remap** | your own base styles under the root class (§7.4) |
| cart, checkout, account, orders | **drop** | system pages of the platform; yours are the header, footer and layout only |
| React router, state, own fetch, SPA shell | **drop** | export statically, one route = one folder (§9) |
| other `<head>` nodes, service worker, server code, `http://` resources | **drop** | — |

## 9. Framework notes

- **React / Vite / Next.** A client-rendered SPA ships an empty `index.html`
  and draws everything by script: no text for search engines and a page that
  looks dead until the bundle runs. Export statically (SSG or prerender), one
  route = one folder = one widget, `base: './'` so the asset paths are
  relative. The router, global state and data fetching do not move — the
  shop's data comes through `vz-` keys or platform widgets. Scripts must start
  immediately and re-init on `vz:navigate`. CORS is open for `localhost`, so a
  local dev server may call the API with the token to preview real data; the
  published widget does not fetch content on its own.
- **Tailwind in the project.** Compile it into the folder; do not load the CDN
  runtime. Your compiled CSS sits above the platform's `@layer base` and wins.
- **Figma-to-code, Webflow, Tilda exports.** Ordinary folders. Typical fixes:
  assets that point at the export host (download them into the folder —
  `http://` is refused), a `<base href>` (dropped anyway), jQuery
  `$(document).ready` and `window.onload` (run once; re-init on `vz:navigate`).
- **Fonts.** `woff2` in the page folder or in the layout folder with a relative
  `url()` in `@font-face`; `<link rel="preload">` in the layout's `<head>`.
- **Images.** Release files are served as they are — resize before upload.
  Catalogue images arrive pre-sliced through the scope fields (`p.preview`).

## 10. How this was verified

- 2026-09-12, live shop vezu-vezu (company 14), prod key of the owner:
  `GET /v1/account/token`; `GET /html-documents` — 15 documents in three
  generations (level 1 → level 1 → level 3), 31 releases in total; the header
  document's `global.css` styles the product page (`h1` computed font
  Montserrat 26 px 700 from the header's stylesheet); a `position: fixed` rail
  measured 900 × 161 px in a 1440 × 900 viewport and 844 px tall at 390 × 844
  (playwright-core, Chromium).
- Live storefront CSS (`/_next/static/chunks/*.css`) grepped for the preflight
  rules listed in §7.4; Tailwind 4.3.2 in `vizen-market/node_modules`.
- Code read: `SectionHtmlAuthored.tsx`, `Slot.tsx`, `PageShell.tsx`,
  `globals.css`, `htmlrefs/fragment.go`, `htmlrelease/validate.go`,
  `tools/html-transfer/transfer.mjs`.
- 2026-09-12, stand `local-stand.sh` (:3970/:5621), shop `proverka-limita-1`,
  starter kit `tools/html-transfer/examples/`: layout folder (favicons
  generated), header + footer bound to the shop, page `demo`, product
  template bound to `category_children`; measured with playwright-core:
  fixed rail 900 px in a 900 px window, `h1` 36 px from `base.css`, menu from
  `vz-for`, the widget root `/_html/48/207/` in the script's note, `/cart` with
  header and footer markup but without the header's relative stylesheet, page
  breadcrumbs and heading above the widget until `hideSystemBlock`. The
  document stays `draft` until the owner's first external transfer passes.

## 11. Negations in this document (rule §3.9)

| The claim | Kept true by |
|---|---|
| level 3 is not sanitised and not wrapped in Shadow DOM or an iframe | `SectionHtmlAuthored.tsx` renders light DOM; `get_html_document_content.go` skips the sanitiser for level 3 |
| `position: fixed` is not contained by the page on an unboxed level-3 block | `globals.css` rule `main.vz-visibility-scope.vz-has-authored { container-type: normal }`; measured rail (§10) |
| a ZIP release does not create the document | `htmlrelease/zip.go` resolves an existing document id; only `transfer.mjs` creates documents and mounts blocks |
| a header widget's CSS is not confined to the header at level 3 | measured on vezu-vezu (§10); no gate — after-wave pass |
| scripts do not run in the guarded zones — and a relative `<link rel="stylesheet">` of the widget is not emitted there either | `storefront/src/lib/forbiddenZones.ts`, static mode in `Slot.tsx`; measured on the stand 2026-09-12 (`/cart`: header markup present, `header.css` absent, layout `global.css` present) |
| release files are not resized | `htmlgw` serves bytes as stored; no gate — after-wave pass |
| `npx serve .` does not show what the site shows | by construction: no page, no cascade, no substitutions locally; no gate |
