# Document layout


## Contents

- [Runtime UI chrome and toolbar](#runtime-ui-chrome-and-toolbar)
- [Theme (light by default)](#theme-light-by-default)
- [Reusable brand profiles](#reusable-brand-profiles)
- [Density (optional)](#density-optional)
- [Table of contents (multi-section documents)](#table-of-contents-multi-section-documents)
- [Document overview strip (report/plan)](#document-overview-strip-reportplan)
- [Sections and document layout](#sections-and-document-layout)
- [Tables](#tables)
- [Layout recipes](#layout-recipes)
  - [Recipe A: centered max-width layout (most pages)](#recipe-a-centered-max-width-layout-most-pages)
  - [Recipe B: full-bleed dashboard layout](#recipe-b-full-bleed-dashboard-layout)
  - [Default sidebar state](#default-sidebar-state)
- [Per-document configuration example](#per-document-configuration-example)

## Runtime UI chrome and toolbar

The runtime chrome is generated by the layer, not authored in report content. The runtime toolbar is intentionally minimal:
**Copy all**, the open-comment count bubble, **Hide** / **Show**, and a **... more** menu with document state,
export actions, and Help. The sidebar header pins a meta row (document-type bubble, layer version, and the
right-aligned **Help & About** / **Hide** pair), the comments count with **Copy all**, sort arrows, one compact row
for **Generated on** and **Last comment**, then icon buttons for Portable, Offline, Plain HTML, Markdown, and Clear.

The runtime also adds a `cm-skip` attribution footer with the brand icon, version, generated timestamp, and a
**Help & about** link. It is absent from Plain HTML export and recenters inside the visible page area while the
comments panel is open. The brand icon is reused as the favicon, sidebar meta-row icon, and Help About mark.

Timestamps use an unambiguous 24-hour local format with a month name, such as `Jul 9, 2026, 13:07`, on cards and in
Copy bundles. `Generated on` reads `data-generated` from `#commentRoot` when present and otherwise falls back to file
modified time. Comment cards show reader-facing anchor info only; internal pinpoint detail stays in the Copy bundle.

The sidebar sort arrows switch comments between oldest-first and newest-first order. Clicking the active arrow again
returns to document order, and the selected order persists for the document.

The document-type bubble reports **Portable**, **Offline**, or **Not portable**. Its tooltip explains whether the file
is self-contained, offline-ready, referencing companion assets, carrying live comments that are not embedded, or
holding embedded comments that were deleted this session and need re-export.

When a user deletes an embedded comment, the runtime records a tombstone so the deletion is durable across reloads.
The file remains **Not portable** until **Export as Portable** writes a fresh copy without that embedded comment.

The Help button opens a modal with collapsible topics, a focused live-search box, an empty state, focus trapping,
Escape / backdrop / X close behavior, and focus restoration. Topics cover leaving a comment, managing comments, the
panel and toolbar, Portable / Not portable, exporting and sharing, sending comments to an agent, navigation, reading
aids, keyboard and accessibility, self-contained and privacy, and About links.

**Clear Comments** keeps a danger color and opens a confirm dialog whose default action is **Cancel**. Enter, Escape,
or backdrop cancel unless the user explicitly chooses OK.

## Theme (light by default)

The document defaults to a **light** theme. The generated template sets `data-theme="light"` on `<html>` and keeps light unless overridden:

- `?clawpilotTheme=dark` forces the dark theme.
- `?clawpilotTheme=auto` follows the OS `prefers-color-scheme`.
- no parameter is always light.

Default to a light document unless the request is explicitly dark.

## Reusable brand profiles

When a reusable brand should stamp report/deck colours or local fonts, pass `--brand brand.json` to
`tools/authoring/new_document.py`, `tools/authoring/retrofit.py`, or `tools/deck/deck_scaffold.py`.
Brand profiles are stateless and opt-in. They may set validated `--cp-*` tokens and local
`data:font/woff2` font faces only; unsafe values are rejected, and low-contrast token pairs print an
advisory so the author can fix them before handoff. Do not pass `--brand` to `upgrade.py` or
`recommend_kind.py`.

## Density (optional)

Leave density unspecified unless the author asks for a different chrome scale. Add
`data-cm-density="compact"` to `#commentRoot` for tighter toolbar, sidebar, footer, Help, TOC, card,
composer, toast, and menu spacing. Add `data-cm-density="comfortable"` for a roomier chrome scale.

The runtime mirrors a valid preset to `document.body` and applies it through shared `--cp-chrome-*` spacing
and font-size tokens, so authored content stays on the same theme variables and documents without the
attribute keep the default measurements.

## Table of contents (multi-section documents)

For a document with roughly **4+ top-level sections**, add a table of contents so the reader can jump between sections. The layer ships `.cm-toc` styling, so this markup needs no host CSS:

```html
<nav class="cm-toc" aria-label="Table of contents">
 <div class="cm-toc-title">Contents</div>
 <ol>
 <li><a href="#section-id">Section title</a></li>
 </ol>
</nav>
```

- Give each heading a stable, kebab-case `id` derived from its text.
- Place the `nav.cm-toc` at the top of `#commentRoot`, after any intro paragraph.
- If your section headings are numbered (for example `1. Goals`, `2. Scope`), do NOT number the TOC entries yourself: the ordered list supplies the number, and `generate_toc.py` (and `finalize.py` / `retrofit.py` for an existing `<ol>` `.cm-toc`) strip the redundant leading number from each entry so it is never double-numbered. Keep the `<ol>`.
- Keep it inside `#commentRoot` as normal content. Anchor links still work because highlighted links can be opened through the hover comment bubble.
- The runtime side menu appears on wide screens. It uses author `.cm-toc` links when present, otherwise `h2`/`h3` ids. Its runtime heading is **Navigation** (the author-provided `.cm-toc-title` can still say **Contents**), it numbers entries, scroll-spies the active section, collapses to `Navigation >>`, expands with `<<`, and adds **Scroll to Top** / **Scroll to Bottom**.
- If collapsible sections exist, the side menu also adds **Expand All** and **Collapse All**.
- A runtime `cm-skip` scroll-progress bubble appears at bottom-right and shows the percent scrolled. It moves left when the comments panel is open.

## Document overview strip (report/plan)

A `report` or `plan` automatically gets a small `cm-skip` overview strip (`div.cmh-doc-stats`) directly under the `<h1>` title, showing the section count, the word count, and an approximate reading time (words / 200 wpm). `new_document.py`, `finalize.py`, and `retrofit.py` bake it by DEFAULT (opt out with `--no-stats`). It is idempotent (the counts refresh in place on the next finalize), excluded from its own word count, and baked into the content so it survives Plain / Standalone export. Do not author it by hand.

## Sections and document layout

Structure a document as a sequence of `<section>` blocks, each led by a heading. The layer renders top-level sections as cards (surface background, 1px border, 16px radius, padding, and soft shadow) in both light and dark.

`tools/authoring/wrap_sections.py` wraps each bare top-level `<h2>` block in a `<section>` so the
document renders as boxed cards. `new_document.py` (report/plan fragments) and `finalize.py` run it by
default; hand-wrapping is only needed for externally produced HTML or unusual layouts. The validator
warns when top-level report/plan content is not sectioned.

- Wrap each top-level topic in its own `<section>` with an `<h2>` and use `<h3>` for sub-topics.
- Every authored `<section>` with a direct heading gets a caret. The caret collapses or expands the section body without removing or reordering nodes, so comment offsets stay valid.
- Jumping to a comment auto-expands collapsed ancestor sections before scrolling.
- Headings get stable ids when missing. A plain click deep-links to the heading, and a hover Add Comment affordance comments the whole heading text.
- For status labels use `<span class="badge">`, `badge ok`, `badge warn`, or `badge danger` rather than ad-hoc inline styles.
- Standalone code blocks and `figure.chart` render as boxed figures. This keeps logs, code, and charts readable without host CSS.
- For boxed asides, use `cmh-callout` plus `cmh-callout-info`, `cmh-callout-success`, `cmh-callout-warning`, or `cmh-callout-danger`. Use `cmh-lede` for a lead paragraph. Never hardcode report colors; use the `--cp-*` variables so authored content stays readable in light and dark themes.
- Long content `<pre>` and `<pre><code>` blocks wrap with `white-space: pre-wrap`. Diff and mermaid blocks keep their own whitespace behavior.

## Tables

The layer ships default `#commentRoot table` styling (collapsed borders, full width, subtle header background, and zebra-striped body rows). Emit plain `<table>` / `<thead>` / `<tbody>` markup inside `#commentRoot`.

Rectangular tables are sortable automatically:

- A table opts in only when it has a header row and every body row has the same cell count with no `colspan` or `rowspan`.
- Each header cell gets up/down chevrons. Click cycles `asc`, `desc`, then original order.
- Numeric-looking columns sort numerically; empty cells sort first in ascending order, and the comparator avoids `NaN` arithmetic.
- Sort state persists in `localStorage` and re-applies before comment restore. After a sort, text-comment offsets are recomputed from live marks. During export, comments are canonicalized back to original row order so a recipient without the sort state gets valid anchors.

For a before/after of code, prefer a [code review diff block](code-review-diffs.md), not a table.

## Layout recipes

The sidebar is `position: fixed` and resizable. The runtime stores the chosen width in localStorage, exposes it as `--cm-sidebar-w`, and clamps it to the viewport. When `body.sidebar-open` is set, the page should reserve room for the current width on wide screens and let the panel overlay on narrow screens.

### Recipe A: centered max-width layout (most pages)

```css
.app { max-width: 1480px; margin: 0 auto; padding: 2.5rem 1.5rem 4rem; }

body.sidebar-open .app { padding-right: 1.5rem; }

@media (min-width: 1100px) {
 body.sidebar-open .app {
 max-width: none;
 margin: 0;
 padding-left: 2rem;
 padding-right: calc(var(--cm-sidebar-w, 400px) + 2rem);
 }
}

@media (min-width: 1900px) {
 body.sidebar-open .app {
 padding-left: max(2rem, calc((100vw - var(--cm-sidebar-w, 400px) - 1480px) / 2));
 padding-right: calc(var(--cm-sidebar-w, 400px) + max(2rem, calc((100vw - var(--cm-sidebar-w, 400px) - 1480px) / 2)));
 }
}
```

### Recipe B: full-bleed dashboard layout

```css
body.sidebar-open main { padding-right: calc(var(--cm-sidebar-w, 400px) + 20px); }
```

### Default sidebar state

The sidebar state is transient and managed by the runtime: on load the layer opens the panel when the
document has restored comments and leaves it closed otherwise, and it toggles the `sidebar-open` class on
`<body>` as the user shows or hides the panel. Do not bake `sidebar-open` (or any other transient state
class) into the `<body>` open tag of a saved file - a persisted `sidebar-open` renders the content full
width with an empty sidebar gutter (the `body.sidebar-open .app` rule) for a panel that is not shown, and
`tools/validate/validate.py` rejects it. Ship a plain `<body>` and let the runtime derive the state.

## Per-document configuration example

The document-level attributes live on the content root; no JavaScript edits are required:

```html
<main id="commentRoot"
 data-cmh-content-root
 data-comment-key="migration-plan-comments"
 data-doc-label="Migration Plan review"
 data-doc-source="Migration-Plan.html"
 data-generated="2026-07-13T06:58:02+03:00">
 ...content...
</main>
```

Use `data-generated` only when you need a deterministic generated timestamp. Otherwise the runtime uses the file
modified time.
