diff --git a/plugins/simulator/docs/user-flows/cdu-dom-tree-reference.md b/plugins/simulator/docs/user-flows/cdu-dom-tree-reference.md
index 86eda7b..34f3f90 100644
--- a/plugins/simulator/docs/user-flows/cdu-dom-tree-reference.md
+++ b/plugins/simulator/docs/user-flows/cdu-dom-tree-reference.md
@@ -446,6 +446,9 @@ Nesting depth → `data-depth` + the `--depth` CSS variable. Active path → `.a
```
> Render path is chosen by mime `value.type`: images → `.file__item__ img`; PDFs/docs →
> `.pg-viewer`/`.pdf-viewer`. Needs a complete File `value` (see §9).
+> **`align` works from config on a *standalone* `file`/`image`** — `align:"center"|"right"` puts a
+> `.center__`/`.right__` modifier on the root (default `.left__`), no CSS needed. A **`file` cell inside
+> a table has no `align`** field, so it stays `left__` and can only be re-aligned via CSS.
### upload (default / webcam)
```
@@ -532,6 +535,19 @@ Common wrapper (all `type`):
- Group title row:
`
→ group.title`
- Then the usual ` ` rows of that group.
+- **Numbered pagination** (`extra.page`/`extra.totalPages`) renders as a **separate block, sibling of
+ `[class*="table__wrap"]`** inside the table root (not part of ``), and defaults to
+ `position:absolute`:
+ ```
+ # .table.table__group …
+ ├ [class*="table__wrap"] > … ← the rows
+ └ [class*="table__pagination"] ← the pager (sibling, position:absolute)
+ ├ [class*="table__pagination__button"] #-first / -prev / -next / -last (icon-only, g-secondary g-small)
+ └ [class*="table__pagination__current"] → current page number
+ ```
+ The `-next`/`-last` buttons reuse the `-prev`/`-first` arrow with `[class*="rotate__"]`. Because the
+ pager is `position:absolute` and `[class*="table__wrap"]` carries a default `max-height`, laying the
+ pager under a re-flowed table needs a CSS re-place (recipe in the `simulator-styles` skill).
### type=default — cell mini-components
Each cell renders inside ``; the cell type
@@ -561,6 +577,11 @@ decides its contents:
→ 33% / 67%). The reliable group hook is `.row__` (flex container); items via
`[class*="row__item"]` (note: the item wrapper has **only** the hashed `.row__item__…` class — there is
no stable bare `.row__item` class, so use the substring selector).
+- **Multi-token `row`** — the `row` value is space-separated: the first token is the row id (→
+ `.row__`), and every **extra token is emitted as a literal class** on the row wrapper. So
+ `row:"1 my_row"` → `…`; `.my_row` is a stable, author-chosen
+ hook (usable across rows). This is the only way to put your own class on a row — a `styleClass` on a
+ `row` is dropped.
### sortable section + contentLoop
```
diff --git a/plugins/simulator/docs/user-flows/cdu-page-protocol.md b/plugins/simulator/docs/user-flows/cdu-page-protocol.md
index 4024e08..e69d7bc 100644
--- a/plugins/simulator/docs/user-flows/cdu-page-protocol.md
+++ b/plugins/simulator/docs/user-flows/cdu-page-protocol.md
@@ -299,6 +299,12 @@ All component `class` values (renderer `ComponentClasses`). Most are content/inp
> swagger ("Simulator.Company Scripts", `components.schemas`). Treat that spec as the source of
> truth for individual component options.
+> **`mainMenu` inline-vs-popover depth** — `mainMenu.extra.maxInlineDepth` (default **1**) sets how
+> deep branches expand **inline** (accordion, via native ` `); levels below it open as a
+> **hover popover** instead, so raise it for a full inline accordion. `extra.submitOnBranchToggle`
+> (default `false`) toggles a branch client-side with no `/send`; `true` posts `action:expand|collapse`
+> on each toggle. `extra.autoExpandActive` auto-opens the ancestors of the active node.
+
---
## 6. Templating & data binding
@@ -502,6 +508,11 @@ applies no author class. A `styleClass` on a normal component (`upload`, `label`
`select`, `edit`, …) **is** emitted onto that element. Style the leaf elements, never the `row`
wrapper.
+> **Workaround — give a row its own class:** the `row` value is space-separated. The first token is
+> the row id (→ `.row__`), and every **extra token the renderer emits as a literal class** on the
+> generated row wrapper. So `row:"1 my_row"` puts `.my_row` on the wrapper — the one stable hook you
+> can style a whole row by (and reuse across rows: `row:"1 my_row"` + `row:"2 my_row"` share `.my_row`).
+
### 12.3 No client-side conditional visibility — reveal = `submitOnChange` + 200 `changes`
`visibility` is a static enum (`visible|disabled|hidden`); there is no expression/binding language,
so a field cannot show/hide reactively from another field's value on the client. The only way to
@@ -551,6 +562,16 @@ So an `edit` with `visibility:"visible"` but a `styleClass` that hides it in CSS
idiomatic carrier for a value set by `changes[]` (e.g. the selection behind button-cards). A field
with `visibility:"hidden"` is NOT submitted.
+### 12.7 `table type:"group"` hides ungrouped rows; its pager sits outside the rows
+Numbered table pagination requires **`type:"group"`** (a `default` table has no page controls) with
+`extra.page` / `extra.totalPages`. Three traps: **(1)** `type:"group"` **hides every row without a
+`groupValue`** — switching a `default` table to `group` without also giving each `body` row a
+`groupValue` (and declaring ≥1 group in `extra.groups`) leaves an empty table with only the pager.
+**(2)** An empty group still renders a group-title header row (`[class*="table__row__group"]`) — hide
+it when the group is a technical, title-less one. **(3)** The pager renders as a **separate block, not
+inside the rows**, so it needs CSS to sit under the table (DOM hooks in `cdu-dom-tree-reference.md` §7;
+positioning recipe in the `simulator-styles` skill).
+
---
## Related documentation
diff --git a/plugins/simulator/skills/simulator-styles/SKILL.md b/plugins/simulator/skills/simulator-styles/SKILL.md
index adf2a8b..a7d84c0 100644
--- a/plugins/simulator/skills/simulator-styles/SKILL.md
+++ b/plugins/simulator/skills/simulator-styles/SKILL.md
@@ -410,17 +410,44 @@ input, textarea, .button > span, .toggle__title,
`[data-class="grid-one-column"] { max-width:440px; margin:20px auto; padding:20px; background:#fff; border-radius:8px; box-shadow:0 0 10px #0000001a; }`
- **Full-bleed admin** — unlock the cap: `.content__main [data-class="grid-one-column"] { max-width:none; }`
- **Fixed sidebar shell** — `.sidebar { width:250px; grid-template-rows:80px 1fr 120px; }` with header/content/footer panes.
+- **Sidebar `styleClass` + `mainMenu` indent trap** — the sidebar rail **ignores `grid.sideBar.styleClass`**; style it via the base slots `.sidebar` / `.sidebar__header|__content|__footer` / `.page__sidebar` (layout — fixed width, pinned footer — as in *Fixed sidebar shell* above). For a `mainMenu` inside it, **do not** target `[class*="mainMenuItem"]` — the substring also matches the branch wrappers (`.mainMenuItemGroup`, `__summary`, `__dropdown`), so `padding` accumulates per level into huge gaps. Zero the wrappers, style only the exact `.mainMenuItem` (the clickable row), and indent by level with the renderer's `--depth` var:
+ ```less
+ nav.mainMenu [class*="mainMenuItemGroup"], nav.mainMenu details, nav.mainMenu summary { margin:0 !important; padding:0 !important; }
+ nav.mainMenu .mainMenuItem { padding:7px 10px; padding-left:calc(10px + var(--depth,0) * 14px); }
+ ```
- **Pinned footer + scrolling body** — `.info_form{height:calc(100vh - 50px); display:flex; flex-direction:column;} .content_section{flex:1; max-height:calc(100vh - 100px);}`
+- **Card-per-iteration wrapper** — a bare `contentLoop` renders its iterations flat, with no per-iteration box. Set the section's `sortable:true` and the renderer wraps **each iteration** in a `.draggable` card with a drag handle; hide the handle to drop the drag affordance and keep just the wrapper (hiding `.draggable__handle` fully disables dragging — it is the only activator — so there is no `reorder` submit to handle):
+ ```less
+ .section__content .draggable__handle { display:none !important; } /* kill grip → drag off */
+ .section__content .draggable { border:none !important; background:transparent !important;
+ padding:0 !important; box-shadow:none !important; cursor:default; } /* reset outer card, avoid double frame */
+ .section__content .draggable__content { border:1px solid #ececec; border-radius:10px; padding:14px; }
+ ```
+- **Give a `row` its own class (multi-token `row`)** — `styleClass` on a `row` is dropped, but the `row` value is space-separated: the first token is the row id (→ `.row__`), and every **extra token the renderer adds as a literal class** on the row wrapper. So `row:"1 my_row"` and `row:"2 my_row"` both get `.my_row` — a shared, stable hook to style many rows at once (the hashed `.row__…` is per-build and unreliable).
### Components — re-skin, don't accept defaults
- **Floating-label input** — `.edit__text .label { top:36px; } .edit__text.selected .label { top:16px; font-size:12px; } .edit__text.focus .field { border-color:@blue; box-shadow:0 0 0 2px #007bff40; }`
- **Custom checkbox** — hide native (`input{-webkit-appearance:none}`, `i{display:none}`), draw tick on `.checked input::after` (rotated border).
- **Card data table** — `.main_table { border-radius:12px; border:1px solid @border; } .main_table [class*="table__wrap"]{ overflow:hidden; border-radius:12px 12px 0 0; }` + custom pagination by swapping `[id*="table-next"] i::after { content:""; background-image:@icon_arrow_right; }`. Use **modifier classes**: `.main_table.thin_table`, `.no_thead`, `.center_cell`.
- **Table/radio → cards** (the e-commerce look): `thead{display:none}`, `tbody{display:flex;flex-direction:column;gap:10px}`, `.table-row{border:1px solid #ddd;border-radius:8px;padding:10px}`, highlight selection with `.table-row:has([class*="radioItem"].checked){border-color:#34303d}`.
+- **Table pagination at the bottom, centered** — a `table type:"group"`'s pagination is a **sibling** of `[class*="table__wrap"]` (not part of the rows) and defaults to `position:absolute` (pinned top, overlapping content). Lay the table root as a column and re-place it:
+ ```less
+ .myTable { display:flex; flex-direction:column; }
+ .myTable [class*="table__wrap"] { max-height:none !important; height:auto !important; }
+ .myTable [class*="table__pagination"] { position:static !important; order:99;
+ display:flex; justify-content:center; gap:8px; margin-top:20px; }
+ ```
- **Status chip** — state-class + token map: `.status_cell.completed-… div{ background:@color_completed; }` (backend sets `styleClass:"status_cell completed-…"`).
- **Button variants** — base `.button` + semantic modifiers (`.blue_btn`, `.red_button`, `.submit_btn`); hover-lift (`transform:scale(1.02); box-shadow:…`) or an animated `::after` fill-sweep (`transform:skewX(-20deg)`).
- **Toast notifications** — centered, severity icon via `[class*="notifyItem"][class*="success"]::before{ background-image:@icon_success; }`.
- **Modal sizing** — base `.modal` + `:has(.)` to size each, or size-class match `&[class*="xlarge"]{ width:80% !important; }`.
+- **Breadcrumbs (via `tab`)** — model a crumb trail as a `tab` (`options[]` = crumbs, each `{value,title}`; `value` = current; `submitOnChange:true` so a click posts the value). Two `tab` traps surface only once you restyle it: the label `[class*="i-label"]` defaults to `width:0; overflow:hidden` (text clips to nothing), and in a fixed tab (`i-fixed`) each `[class*="tab__item"]` collapses to `width:0` (options overlap). Fix both with `width:auto`:
+ ```less
+ .breadcrumbs [class*="tab__item"] { width:auto !important; min-width:max-content !important;
+ flex:0 0 auto !important; margin:0 !important; }
+ .breadcrumbs [class*="tab__item"] [class*="i-label"] { width:auto !important; overflow:visible !important; }
+ .breadcrumbs [class*="tab__item"]:not(:last-child)::after { content:"›"; margin:0 10px; opacity:.6; }
+ ```
### Interactivity (CSS-only, no backend round-trip)
- **Accordion / dropdown / collapsible tree** — a `check`/`toggle` with `.checked`, expanded by parent `:has()`, animated via `grid-template-rows:0fr → 1fr`:
@@ -428,6 +455,7 @@ input, textarea, .button > span, .toggle__title,
.menu { display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s; }
.sidebar:has(.nav_btn.checked) .menu { grid-template-rows:1fr; }
```
+ When the toggle and its target are **siblings in the same `.section__content`** (the common case), prefer the sibling combinator — no `:has()` needed: `.myCheck.checked ~ .myCollapsible { max-height:300px; }`. `.checked` lands on the component's outer wrapper. Reserve `:has()` for non-sibling targets.
- **Slide-in panel / popover** — toggle `opacity`/`transform`/`pointer-events` under `&:has(.trigger.checked)`.
### State without CSS-only tricks
@@ -484,10 +512,22 @@ Advanced Less available: `each()`, guards (`when`), maps + indirect lookup (`@@k
- **Winning the cascade**: component CSS-modules load after you and win equal-specificity ties — beat
them with a chain of stable classes (`styleClass` + `[class*="…"]` + tag), not `#id` or a lone
`!important`. Full worked example + cascade math in §"How Smart Form styling works" pt 5.
+- **Never write `.cdu-page` yourself**: the compiler wraps your whole stylesheet in `.cdu-page { … }`
+ at serve-time, so a hand-written `.cdu-page` (or `.cdu-page:has(…)`) becomes a *descendant*
+ (`.cdu-page .cdu-page…`) and silently never matches. The page root is `&` (= `.cdu-page`): paint the
+ page with `& { background:#e30613; min-height:100vh; }` and target it with `&:has(…)`.
- **Image/`file` cell renders "wide but a thin strip"**: not a width bug but a **clipping ancestor** —
a wrapper (`.file` / `.file__item`) keeps a small fixed height + `overflow:hidden`. Force the box at
**every** wrapper level (`height` + `min/max-height` + `width:100%` + `max-width:none` +
- `overflow`), not only on ` `.
+ `overflow`), not only on ` `. The default clamp is `max-height:32px` on both ` ` and
+ `[class*="table__img"]`, and `max-height` always beats `height` — so `max-height:none` is the key.
+ One height knob down the whole chain (`.card-img` = the cell's `styleClass`, applied on ``):
+ ```less
+ .card-img, .card-img [class*="table__img"], .card-img .file, .card-img [class*="file__item"] {
+ width:100%; height:240px !important; max-height:none !important; display:block; }
+ .card-img img { width:100% !important; height:100% !important; max-width:none !important;
+ max-height:none !important; object-fit:contain; } /* contain = whole image; cover = fill + crop */
+ ```
- **Dead `styleClass`**: placeholder classes (`my-custom-class`) with no rule are harmless but
noise — remove them.
- **Stale copies**: forms accumulate `style(copy)` / `style BACKUP` files — they're not wired in;
| |