diff --git a/docs/docs.json b/docs/docs.json
index dd4703b7c6..85b47012ff 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -1009,7 +1009,8 @@
"reference/html-schema",
"reference/color-grading",
"reference/audio-effects",
- "reference/speed-ramps"
+ "reference/speed-ramps",
+ "reference/lint-rules"
]
},
{
diff --git a/docs/packages/lint.mdx b/docs/packages/lint.mdx
index 414f9b3fd9..39cdc3ee9e 100644
--- a/docs/packages/lint.mdx
+++ b/docs/packages/lint.mdx
@@ -100,6 +100,8 @@ The browser entry exposes `lintHyperframeHtml`, `lintMediaUrls`, and `shouldBloc
## What the Linter Catches
+Every finding code — its severity, what it catches, and how to fix it — is documented on the [Lint rules](/reference/lint-rules) reference page.
+
Detected issues include:
- Missing timeline registration (`window.__timelines`)
diff --git a/docs/reference/lint-rules.mdx b/docs/reference/lint-rules.mdx
new file mode 100644
index 0000000000..4a099df1a4
--- /dev/null
+++ b/docs/reference/lint-rules.mdx
@@ -0,0 +1,877 @@
+---
+title: "Lint rules"
+sidebarTitle: "Lint rules"
+description: "Every lint finding code with its severity, what it catches, and how to fix it."
+---
+
+`hyperframes lint`, `hyperframes check` and the `@hyperframes/lint` package report findings with a stable code. This page lists every code with its severity, what it catches, and how to fix it. The codes come straight from the rule set in `packages/lint/src/rules/` plus the project-level probes that run as part of a project lint.
+
+Severities:
+
+- **Error** — the composition is broken or the render will fail or visibly diverge. Fails `hyperframes check` by default.
+- **Warning** — a quality, determinism or maintainability hazard. Does not fail `hyperframes check` unless you pass `--strict`; in `render`, warnings abort only under `--strict-all`.
+- **Info** — a heads-up that needs no action. Shown by `hyperframes lint --verbose`.
+
+
+ Looking for a code from a render or check log? Codes appear in the output of `npx hyperframes lint [dir]` — add `--json` to script over them. `check` additionally runs runtime, layout and motion audits beyond these lint rules; see [Troubleshooting](/guides/troubleshooting).
+
+
+## Structure and timeline
+
+Source: `packages/lint/src/rules/structure.ts`.
+
+The four codes marked **Warning or error** are warnings when the CLI lints a project, and errors when Studio lints the file it is editing.
+
+### `nested_structure_needs_subcomposition`
+
+**Warning or error.** A top-level timeline element contains another nested structure instead of a sub-composition mount. The timeline draws one row per top-level element, and the root composition is built only from sub-compositions.
+
+**Fix:** Move the element and its contents into a sub-composition file and mount it with `data-composition-src`.
+
+### `timeline_element_missing_timing`
+
+**Warning or error.** A timeline element has no `data-duration`, so the timeline cannot draw where it ends.
+
+**Fix:** Add `data-duration` (in seconds) to the element.
+
+### `caption_track_kind_missing`
+
+**Warning or error.** An element looks like a caption row but is not marked as one, so the timeline cannot group it on the caption track.
+
+**Fix:** Add `data-track-kind="captions"` to the element.
+
+### `multiple_caption_tracks`
+
+**Warning or error.** Captions are spread across several timeline tracks instead of one caption lane.
+
+**Fix:** Give every caption element the same `data-track-index`.
+
+### `clip_ends_past_root_duration`
+
+**Warning.** A clip runs past the root composition's `data-duration`, so it is cut off in previews, posters and renders.
+
+**Fix:** Extend the root `data-duration`, or make the clip end at or before it.
+
+## Composition contract
+
+Source: `packages/lint/src/rules/composition.ts`.
+
+### `invalid_composition_variables_declaration`
+
+**Error.** `data-composition-variables` is not valid JSON or not an array of variable declarations.
+
+**Fix:** Provide a JSON array such as `data-composition-variables='[{"id":"title","type":"string","label":"Title","default":"Hello"}]'` — single-quote the attribute, double-quote the JSON.
+
+### `invalid_variable_values_json`
+
+**Error.** `data-variable-values` is not valid JSON.
+
+**Fix:** Single-quote the attribute and double-quote the JSON keys and values, e.g. `data-variable-values='{"title":"Hello"}'`.
+
+### `unknown_variable_binding`
+
+**Warning.** An element binds a variable that is not declared in `data-composition-variables`, so the binding silently keeps the authored fallback.
+
+**Fix:** Declare the variable on the composition root (``, or the `data-composition-id` root of a template or fragment composition).
+
+### `unloadable_media_variable_default`
+
+**Error.** A variable's default is a URL the runtime refuses to load, so bound elements render their authored fallback src while the render still exits 0.
+
+**Fix:** Use a relative path, `http(s)`, `blob:`, or a `data:image/*` URI. Copy the file into the project and reference it relatively.
+
+### `deprecated_data_end`
+
+**Error.** A legacy `data-end` is used without `data-duration`, or a stale `data-end` disagrees with the `data-duration` next to it.
+
+**Fix:** Replace `data-end` with `data-duration`. The compiler generates `data-end` from `data-duration` automatically.
+
+### `deprecated_data_layer`
+
+**Error.** The legacy `data-layer` attribute is used.
+
+**Fix:** Replace it with `data-track-index`, the canonical name Studio and the linter read.
+
+### `duplicate_composition_id`
+
+**Error.** The same `data-composition-id` appears on several elements in one file — often a `` tag carrying the same id as the root `
`.
+
+**Fix:** Keep `data-composition-id` on exactly one element: the composition root. Remove it from metadata or duplicate hosts.
+
+### `html_dir_attribute_breaks_render`
+
+**Error.** `` or `` renders correctly in preview and snapshot but produces a fully blank or black video from render — a confirmed, silent failure. `dir="ltr"` is not flagged.
+
+**Fix:** Remove `dir` from ``, keep `lang`, and scope the direction to individual text-containing elements instead — `direction: rtl` in CSS, or `dir="auto"` on the element.
+
+### `invalid_parent_traversal_in_asset_path`
+
+**Error.** Asset paths use `../` to traverse above the project root; render and preview clamp or misresolve them.
+
+**Fix:** Point the path at a file inside the project — from `compositions/scene.html`, both `../assets/x.png` and `assets/x.png` reach the project's `assets` folder.
+
+### `composition_exceeds_inspection_viewport_cap`
+
+**Warning.** The composition is larger than the 4096px viewport cap that `check`, `validate`, `snapshot`, `compare` and layout audits clamp to. `render` is unaffected, but a clean inspection result no longer covers the whole canvas.
+
+**Fix:** Keep the authored size if the output needs it, and verify the region past the cap from a render rather than from inspection commands.
+
+### `composition_file_too_large`
+
+**Warning.** The HTML composition file has grown past a size where it stays easy to read, iterate on, and diff.
+
+**Fix:** Split it into sub-compositions and mount them from the parent with `data-composition-src`.
+
+### `composition_heavy_overlay_count_high`
+
+**Warning.** Many elements carry heavy overlay CSS (`filter: blur`, `radial-gradient`, `clip-path`), which can cap capture performance — including hidden ones.
+
+**Fix:** Split the composition and concat the pieces so each capture sees a small subset of heavy overlays; remove overlays that are not visible anyway.
+
+### `missing_data_no_timeline`
+
+**Warning.** A `data-composition-id` element (or the root) has neither a matching `window.__timelines` registration nor `data-no-timeline`, so the producer polls up to 45 seconds before timing out.
+
+**Fix:** Register the timeline, mount the composition with `data-composition-src`, or add `data-no-timeline` for genuinely static sections.
+
+### `negative_z_index`
+
+**Warning.** `z-index` below 0 paints the element behind its nearest stacking context's content; with no stacking-context ancestor that is the composition root, so an opaque background there hides the element entirely.
+
+**Fix:** Give the element's parent a stacking context (`isolation: isolate` is the cheapest; `transform`, `filter`, `opacity` below 1, `contain: paint`, `will-change` also work).
+
+### `requestanimationframe_in_composition`
+
+**Error.** `requestAnimationFrame` runs on wall-clock time, not the GSAP timeline, so it desyncs from frame capture and can flicker or drop frames in renders.
+
+**Fix:** Use GSAP tweens or timeline `onUpdate` callbacks for animation logic instead.
+
+### `root_composition_duration_derived`
+
+**Warning.** The root has no `data-duration` and no GSAP timeline, so its length is inferred from its clips.
+
+**Fix:** Add `data-duration=""` to the root element to set the length yourself.
+
+### `root_composition_missing_duration_source`
+
+**Error.** The root has no `data-duration`, no GSAP timeline, and no CSS, WAAPI, Lottie or Three.js animation to infer a duration from, so the render engine cannot determine how long to capture and fails.
+
+**Fix:** Add `data-duration=""` to the root, or add a paused GSAP timeline registered on `window.__timelines`.
+
+### `root_composition_missing_html_wrapper`
+
+**Error.** An `index.html` carrying `data-composition-id` is a bare fragment without ``, `` or ``, so browsers quirks-mode it and the preview server mis-serves it.
+
+**Fix:** Wrap the composition in `...`.
+
+### `root_zoom_rescales_a_fixed_canvas`
+
+**Error.** CSS `zoom` on the canvas rescales the painted composition inside a capture frame that does not rescale with it — the frame is sized from the root's `data-width` and `data-height` in layout pixels.
+
+**Fix:** Author at the real size: set `data-width` and `data-height` (and the root's width and height) and drop the canvas-level `zoom`. To scale the output without changing layout, pass `--output-resolution` to render. `zoom` on descendants is fine.
+
+### `split_data_attribute_selector`
+
+**Error.** A CSS selector combines two attributes inside one `[...]` block, which browsers reject — GSAP timelines or `querySelector` calls fail before they register.
+
+**Fix:** Use separate attribute selectors: `[data-composition-id="x"][data-track-index="1"]`.
+
+### `standalone_composition_wrapped_in_template`
+
+**Error.** The root `index.html` is wrapped in a `` tag, so the runtime cannot play it — only sub-compositions loaded via `data-composition-src` use `` wrappers.
+
+**Fix:** Remove the `` wrapper and serve the composition as a real document.
+
+### `subcomposition_blanks_before_host`
+
+**Warning.** A mounted sub-composition is blank for part of its host's window because its own `data-duration` is shorter than the slot it fills.
+
+**Fix:** Set the sub-composition's `data-duration` to fill the host window, or cover the gap with another clip.
+
+### `subcomposition_root_styled_by_class`
+
+**Error.** A sub-composition's root element is styled through class selectors, but at render every rule is scoped to `[data-composition-id=...]`, so the class-keyed styles stop applying.
+
+**Fix:** Give the root `id="root"` and style it with `#root` plus plain descendant selectors.
+
+### `template_literal_selector`
+
+**Error.** `querySelector` is called with a template-literal variable, which the bundler's CSS parser crashes on.
+
+**Fix:** Use hardcoded string selectors instead of interpolated ones.
+
+### `timed_element_missing_clip_class`
+
+**Warning.** An element has timing attributes but no `class="clip"`. The runtime still times it, but Studio and the clip-ownership rules use `.clip` to recognise it, so it is harder to edit and to lint.
+
+**Fix:** Add `class="clip"` to the element.
+
+### `timeline_track_too_dense`
+
+**Warning.** One track in this file carries more than 3 timed elements (audio, video, caption cues and sub-composition mounts are not counted), which makes the timeline harder to read, iterate on, and diff.
+
+**Fix:** Split the elements into sub-compositions and mount them from the parent with `data-composition-src`.
+
+## Document, CSS and scripts
+
+Source: `packages/lint/src/rules/core.ts`.
+
+### `host_missing_composition_id`
+
+**Error.** A `data-composition-src` host element has no `data-composition-id` of its own.
+
+**Fix:** Set `data-composition-id` on every `data-composition-src` host element.
+
+### `css_parse_error`
+
+**Error.** A `` closes the stylesheet early and dumps CSS into the body as visible text.
+
+**Fix:** Keep `` paired.
+
+### `unclosed_tag_swallowed_element`
+
+**Error.** A tag is missing its closing `>` before the next `<`, so the following element is swallowed as bogus attribute text and never becomes a real node.
+
+**Fix:** Close the previous tag's `>` before opening the next element.
+
+### `visible_markup_comment`
+
+**Error.** CSS or JS block-comment syntax (`/* ... */`) appears in visible HTML markup, where only `` is a comment — so it renders as on-screen text.
+
+**Fix:** Remove the text or convert it to a real HTML comment; keep CSS comments inside `