Simulator.Company Smart Form STYLING specialist
You author and apply CSS/Less styles for Smart Forms (CDU / Script apps) on
Simulator.Company. Your domain is the style / styles/ layer — themes, layout, component
re-skinning, reusable patterns, responsive and design-system approaches.
This skill is built on patterns reverse-engineered from real production Smart Forms; the recipes
below are taken from live Less, not invented.
Scope — what this skill owns (and what it does NOT)
| Concern |
Skill |
Styling: style / styles/*, pages/<id>/style, styleClass, themes, Less |
this skill |
| Form template (data fields / Account Template) |
simulator-forms |
Page layout JSON (pages/<id>/config: grid/forms/sections/items), viewModel, locale |
simulator-smart-forms |
Backend logic (Corezoid /get /send, dynamic viewModel, changes[]) |
simulator-smart-forms-logic |
You reuse the Smart Form engine tools (pullSmartForm, pushSmartForm, deploySmartForm,
file-history/rollback) — you do not introduce new platform behaviour. When the user needs a new
form/page first, defer to simulator-smart-forms; you come in to make it look right. To attach a
styleClass to a component you may need a one-line edit to pages/<id>/config — that's in scope
(it's the binding), but designing the layout itself belongs to simulator-smart-forms.
How Smart Form styling works (the model)
One Less stylesheet, compiled per save, scoped to .cdu-page. Whatever you write is wrapped
in .cdu-page { … } at serve time, so & = the page root and your styles can't leak out. Less
syntax (variables, mixins, @import, functions, maps, guards, each()) is fully supported.
File organization — and where each rule goes. Two layouts compile identically; prefer the
modular styles/ folder for anything non-trivial:
| File |
What belongs here |
styles/index |
Just @imports (the entry/manifest), in cascade order. |
colors_fonts |
Color + font tokens (@color_*, @font_*, @font-face). Define them here because everything else — including page styles — inherits them. |
init_styles |
(a) neutralize the platform's default styling (resets); (b) project-wide component defaults — one base look reused on every page (e.g. a .button skin). |
pages/<id>/style |
Only the page's differences from the shared styles — page layout + one-off component tweaks. Never define tokens here (they live in colors_fonts). |
styleClass is the binding contract between layout JSON and CSS. Every grid / form /
section / item in pages/<id>/config may carry a styleClass; your CSS targets that class.
- Static for structure:
"styleClass":"main_table".
- Dynamic for backend-driven state/theme:
"styleClass":"{{settings_page_text_align}}" —
Corezoid pushes the value (e.g. text_align_right, active_sidebar_btn) to switch styling.
Validation: CSS is not validated on save; a Less compile error is emitted as a
/* Less Error … */ comment rather than breaking the page. styleClass values are never
validated — a class with no matching rule is harmless (but dead; clean it up).
Reaching renderer internals: the public knobs are styleClass + documented component
classes (.button, .edit, .select, .table, .check, …). For deeper structure use
substring/attribute selectors — [class*="table__wrap"],
[data-class="grid-one-column"].
Winning the cascade — the renderer ships base/inline styles AND per-component CSS-module rules
that load after your scoped styles/index, so an equal-specificity rule of yours loses the
tie by source order. Beat it by raising specificity with more stable classes, not ids: chain
your styleClass + an ancestor [class*="…"] + the element — e.g.
.book-table td.bc-cover-cell .file img (0,3,2) beats the component's .file__item__hash img
(0,2,1). Prefer this class chain over #id (brittle — it pins the rule to a config id, and
classes are the house rule) and over a lone !important (it only beats non-!important; in an
!important vs !important fight specificity still decides). A doubled class (.x.x) is a last
resort. See the verified per-component DOM map below for exact hooks (edit, select,
multiselect, radio, button, row__<name>).
The cascade math, with real numbers (measured against a live renderer build — dump the
defaults yourself to confirm, see Workflow §"dump the defaults"):
- Your stylesheet is auto-wrapped in
.cdu-page, so a bare single-class rule you write is
already 0,2,0 (.cdu-page .myClass) and beats a bare renderer default .hashed (0,1,0)
for free — most overrides need nothing more.
- BUT the renderer theme-scopes ~⅓ of its rules (
.theme-light .x / .theme-dark .x, on
#mainRoot), and those are also 0,2,0 — a tie with your wrapped single-class rule, and
since defaults load after you, the theme default wins. Symptom: a background/color
override "does nothing" even though your selector clearly matches. Beat it with an extra class
(0,3,0) or !important.
- ~1 in 5 default rules already use
!important (heavy on active/selected/checked
states and theme colors). Matching them with your own !important is normal here, not a smell
— a lone !important still loses to a default !important of higher specificity, so pair it
with a solid class chain.
- Slots own their own background/padding. Containers like
.section__content, the sidebar
slots (.sidebar__header/__content/__footer), and .page__sidebar each ship their own bg/padding
— painting the parent won't show through. Override the slot, not its ancestor.
Authoritative component-class + CSS reference: the "CSS styling" tag in the CDU swagger and
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md.
Rendered DOM map (verified component internals)
Verified against a live control-cdu render. The summary below is enough for most work; for the
full per-component tag tree of every component (incl. all 4 table types, page skeleton,
overlays) see $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md. Use this to target
the right element instead of guessing. Three rules first:
styleClass lands on the component ROOT. E.g. class="label hd-meta label__12ehI",
class="edit edit__text txt-input … bordered", class="radio pills … horizontal". So a root
hook (.hd-meta, .txt-input, .pills) always hits — but only the outer element.
- Internals use hashed CSS-module classes (
label__12ehI, f-item-0-2-64, i-label-0-2-70,
clickOutside(i)-field-0-2-97) that change between renderer builds. Never target the hash.
Reach inside via substring selectors ([class*="radioItem"], [class*="i-icon"],
[class*="chip"], [class*="i-edit"]), stable state classes (.checked .disabled
.bordered), data-class attributes, or element selectors (input, textarea, label,
svg).
row / w grouping yields a STABLE group class .row__<rowName>. An item with "row":"act"
renders inside <div class="row row__act row__hash"> whose children are
<div class="row__item__…" style="width:…%">. Note w is a relative weight, not a raw
percentage: the rendered width is w / Σw across the row (two items at w:50 each get 50%; at
w:50 + w:100 they get 33% / 67%). Style the group via .row__act (flex container) and items
via [class*="row__item"] — your reliable hook for multi-column rows, progress bars, etc.
Page skeleton & theme (stable structural hooks):
- Scope: all your CSS is wrapped in
.cdu-page (& = page root); per-page hook
.cdu-page-<pageId>.
- Dark mode is a CLASS, not a media query:
.theme-light / .theme-dark. In a live render the
class sits on the host wrapper #mainRoot (an ancestor of .cdu-page), which is why
.theme-dark .… {} works from inside your scoped stylesheet. control-cdu itself also stamps the
theme class onto #page (the .cdu-page node) — so verify the placement on your build: if the
class is only on the .cdu-page node (not an ancestor), a wrapped .theme-dark .foo won't match
and you'd need &.theme-dark .foo. Prefer this class approach over @media (prefers-color-scheme).
⚠️ Caveat: the renderer's light/dark token maps are currently identical — the mechanism is
wired but dark mode has no distinct palette yet, so a .theme-dark override is the only way to make
dark actually differ today.
- Grid regions:
[data-class="grid-one-column"] / [data-class="grid-two-column"]
(+-left/-right); header/footer regions via [class*="gridtwo__header"] /
[class*="gridtwo__footer"].
- Section slots:
[data-class="section"] (+ [class*="block__"] for type:"block" cards);
inner [class*="section__header"] / [class*="section__content"].
- Toasts: two
[class*="notify"] containers (top inside #page, bottom at #mainRoot end).
Per-component root + key inner hooks (<sc> = your styleClass):
| Component |
Root selector (styleClass here) |
Key inner hooks / notes |
label |
.label / [data-class="label"] |
<span> text; BBCode → real tags; align→ .left__/.center__/.right__ |
divider |
.divider |
empty |
edit (all types) |
.edit (+ .edit__<type>) |
.field > input / textarea; states bordered(box)/selected/error; kill box .field{border:none}; help/err [class*="Component-helperText"] |
select |
.select (outer) |
readonly <input> in [class*="i-edit"] + caret [class*="endAdornment"]; no native <select> |
multiselect |
.multiselect |
chip field: [class*="chip"] + search <input> via [class*="clickOutside(i)-field"]; not checkbox rows |
radio |
.radio (+ .horizontal for row) |
options [class*="radioItem"](+.checked/.disabled); hide svg [class*="i-icon"], style label [class*="i-label"] → pills/scales |
check |
.check |
[class*="f-icon"] svg + <label>; states .checked/.error; native input{appearance:none} |
toggle |
.toggle (+ .left__/.right__) |
[class*="toggle__button"](+.active) / [class*="i-switch"] |
slider |
.slider (+ .skillBar__) |
rc-slider: .rc-slider-rail/-track/-handle/-dot; [class*="slider__header"], [class*="slider__min"]/max |
otp |
.otp |
boxes [class*="otp__edit"] input |
phone |
[data-class="phone"] |
#countryCode .select + #number .edit input; [class*="phone__items"] |
image |
[data-class="image"] |
<img> (src proxied via /api/1.0/image); align→ .center__ |
timer |
[data-class="timer"] |
<span> text |
comments |
[data-class="comments"] |
[class*="mes__wrap"], [class*="mes__name"], [class*="mes__content"], avatar [class*="i-avatar"] |
carousel |
.carousel |
preview pane [class*="carousel__preview"] (zoom/nav btns), thumb strip [class*="carousel__content__item"](+.active__); give it a full File value for a correct preview (tolerates a missing type — won't error-stub) |
button |
wrapper [data-wrapper-for="<id>"] → inner #<id>.button.button__<type> |
<sc> is on the INNER button; text [class*="button__label"]; align via wrapper/.row__<name>; 7 types via .button__<type> |
copy |
[data-class="copy"] |
[class*="copy__container"], icon [class*="i-icon"], label [class*="i-label"] |
tab |
.tab (outer .tab__…) |
items [class*="tab__item"](+.active/.error); hidden option is absent from DOM |
stepper |
[data-class="stepper"] |
items [class*="stepperItem"](+.completed__/.active__); label [class*="stepperItem__label"] |
mainMenu |
nav.mainMenu |
items [class*="mainMenuItem"](+.active); groups <details>/[class*="mainMenuItemGroup"]; data-depth + --depth; badge [class*="i-badge"] |
upload |
[data-class="upload"] |
[class*="upload__box"], corners [class*="upload__corner"], input[type=file] (webcam: trigger div.upload__file__…[role=button], needs extra.accept) |
file |
.file |
image → [class*="file__item"] img; pdf/doc → .pg-viewer/.pdf-viewer; path chosen by mime value.type |
attachment |
[data-class="attachment"] |
upload ctrl [class*="i-upload"]; chips [class*="fileItemChip"] (text [class*="e-chipText"], remove [class*="e-delFileIcon"]) |
signature |
.signature__wrap |
<canvas>; toolbar [class*="signature__toolbar__clear"]/__save |
table |
[data-class="table"] (+ .table__check/__radio/__group) |
see Tables below |
widget |
[data-class="widget"] |
iframe[class*="iframe__"]; [class*="widget__inner"](+.hidden__ until load) |
| row/w group |
.row__<rowName> |
flex container; items [class*="row__item"] (inline width) |
draggable (sortable/contentLoop) |
[data-class="draggable"] |
grip [class*="draggable__handle"], body [class*="draggable__content"] |
notification (toast) |
none — page root [class*="notify"] |
[class*="notifyItem"] (+ severity [class*="success"]/[class*="error"]/[class*="info"]) → text span[class*="i-title"] + close i[class*="closeIcon"] > svg .fill |
Tables (DOM is a real <table>): head cells [class*="table__head"] (sortable col
[class*="sortable__"], sort arrow [class*="table__head__icon"]); sticky col → td.sticky-col
- the head cell's own
styleClass; body rows .table-row / [class*="table__row__"]
(selected radio row → .active__…); first column for check/radio → [class*="table__check"]
/ .i-radioItem-…; group title row → [class*="table__row__group"]. Default-table cell
mini-components inside <td.table-cell>: file → [class*="table__img"], copy →
#<id>--copy[data-class="copy"], check → #<id>--check[class*="table__check"], button →
[class*="table__button"] (id <id>--button, not .button).
Table cells are NOT text-only. A default-table cell can be plain text, an image (file
cell → real <img>), a button, a copy, or a check — so covers/actions live inside a table
(cell JSON shape belongs to simulator-smart-forms; you style via each cell's own styleClass,
which lands on its <td>). And plain + head cells render BBCode (verified — see the BBCode
matrix below): the text goes into a plain <td>/<div>, so [b]/[color]/[div]… expand there.
This is why you can build a status pill in a cell without a button cell: put [div]Label[/div]
in the plain cell and style .<cellClass> div.
Overlay sections: modal → backdrop [class*="i-bg"] > box [class*="modal__"] (+ size
i-small/i-medium/i-large/i-xlarge ← modalSize), header [class*="section__modal__header"].
float → [class*="float__"] inside [class*="Component-wrapper"] with drag bar
[class*="section__header__dragable"] + 8 [class*="Component-resizeHandle"].
⚠️ File-bearing components need a complete value. The file component and a default
table's file cell read mime via value.type with no guard — a File missing type throws
and the item renders as a [class*="item__error"] stub. (carousel and attachment also read
value.type but tolerate it missing — carousel defaults to '' and uses optional chaining — so
they won't stub out; still give them the full shape for a correct preview.) Give File values the
full shape {fileName, fileSrc, title, type, size}. A stub can also suppress later siblings (a
crashing table cell hid the modal/float sections after it). upload[webcam] needs
extra.accept. With complete values everything renders statically — no backend needed. Full
per-component trees: $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md.
Worked example — radio rendered as pills (the native input is hidden; the svg is the control):
.pills { display:flex; flex-wrap:wrap; gap:8px; }
.pills [class*="i-icon"] { display:none; } // hide the svg circle
.pills [class*="radioItem"] { border:1px solid @line; border-radius:100px; }
.pills [class*="radioItem"] label { padding:7px 16px; } // make the whole pill the label
.pills [class*="radioItem"].checked { background:@ink; }
.pills [class*="radioItem"].checked label { color:#fff; }
Worked example — toast notification. Toasts render at the page root (inside .cdu-page, which
scopes your CSS), so you reach them structurally — there is no styleClass. For the DOM nesting +
the two traps ([class*="i-icon"] matches the close ✕, not a severity icon — don't blanket-hide it;
[class*="i-title"] also matches the wrapper i-titleContainer — qualify as span[class*="i-title"])
see the Toasts entry in cdu-dom-tree-reference.md. A reusable skin:
[class*="notify"] [class*="notifyItem"] {
background:@white; color:@ink; border:1px solid @line; border-radius:@r;
box-shadow:0 8px 28px rgba(0,0,0,.12); padding:14px 16px;
display:flex; align-items:center; gap:10px;
}
[class*="notify"] [class*="notifyItem"]::before { // severity dot
content:""; width:8px; height:8px; border-radius:50%; background:@ink-3; flex-shrink:0;
}
[class*="notify"] [class*="notifyItem"][class*="success"]::before { background:#16a34a; }
[class*="notify"] [class*="notifyItem"][class*="error"]::before { background:#dc2626; }
[class*="notify"] [class*="titleContainer"] { flex:1; min-width:0; }
[class*="notify"] span[class*="i-title"] { font-size:14px; font-weight:500; color:inherit; }
[class*="notify"] [class*="closeIcon"] { opacity:.45; cursor:pointer; } // keep the ✕
[class*="notify"] [class*="closeIcon"]:hover { opacity:1; }
[class*="notify"] [class*="closeIcon"] svg .fill { fill:currentColor; }
Hashes (-0-2-64) are version-pinned to the renderer build — substring selectors survive a bump,
exact-hash selectors don't. If a restyle suddenly breaks, re-dump the rendered DOM (browser
DevTools → copy outerHTML of #page) and re-verify these hooks.
BBCode — where it renders (empirically verified)
A component's text is expanded to HTML by the client (Utils.bbCodeToHtml) only in some fields.
The smart-form BBCode set (swagger bbcode tag) is
[b] [i] [u] [ul][*] [iurl=…] [url=…] [div] [br] [color=…] [size=…] [bg=…] — note [div] (the only
block-level tag) and [iurl] (internal link); there is no [span style=…] here (that exists
only in actor/reaction BBCode). Mapping: [color]→<span style="color">,
[bg]→<span style="background-color">, [div]→<div>.
Verified support matrix (built by rendering a live probe string, not read off the swagger):
| Renders BBCode ✅ |
Stays literal ❌ |
label.value |
edit value / placeholder / multiline |
button.title |
select value · multiselect option |
check.title |
radio option title |
stepper option title |
toggle.title · tab option title |
form.title, mainMenu title, comments, carousel item title, upload.title |
copy.title |
table head title AND plain cell |
|
Heuristic: it expands where the text lands in a plain text node (<span>/<div>/<td>) and stays
literal where it lands in a form-control value/placeholder — but it's only a rough heuristic:
it's whether that specific component passes its title through bbCodeToHtml/dangerouslySetInnerHTML,
not the node type. check ✅ but radio ❌ even though both render a <label>; stepper ✅ but
toggle/tab ❌. Trust the matrix (built from the renderer source), not the swagger: the swagger
marks only label/button/mainMenu as bbcode fields, yet table cells clearly render it. When a field
isn't in the matrix, confirm it in the live UI before relying on it.
Styling use — inject a styleable element where there is none. A plain table cell is bare text
in a <td> (no inner wrapper to hook). [div]…[/div] gives you an inner <div> to style as a
pill/badge (.<cellClass> div { … }); [bg]/[color] give inline <span>s. (Alternative with no
inner element: absolutely-position the <td> so it shrink-wraps its text into a pill.)
Workflow (reuses the Smart Form tools)
1. pullSmartForm(actorId) → downloads <actorId>/develop/ + production/ (incl. style / styles/, pages/<id>/style)
2. Edit ONLY under develop/:
- styles/index (+ partials) OR the root `style` file
- pages/<id>/style (page-specific)
- pages/<id>/config (only to add/adjust a styleClass hook)
3. pushSmartForm(actorId) → validates + uploads changed files; style/styles files carry MIME text/css (the push tool sets it; the server stores the client-supplied type — it does not force it by folder)
4. deploySmartForm(actorId) → publishes develop → production (when approved)
Rules: edit develop only (production is readonly); pullSmartForm first (it writes the
.manifest.json push needs). Files under styles/ are sent as text/css; everything else
application/json.
Verifying a styling result. appGetPage returns the server-resolved config (locale/viewModel
expanded) — it does not run CSS compilation or bbCodeToHtml, so you cannot confirm a visual
result or whether BBCode expanded from it. Visual/BBCode results are verified in the live UI by the
user.
When to ask for the rendered HTML. First fix from the DOM map below; if it doesn't land, correct
once more. If after the second correction the user still doesn't get the expected result, stop
guessing and ask them to copy the element's HTML (Inspect → Copy outerHTML of the element, or dump
#page). The real DOM — hashed classes, wrapper nesting, inline sizes — resolves what the map can't;
most restyle stalls end the moment you read it.
Starter kit — snippets that recur across real forms
These appear near-verbatim in multiple production forms; lift them as a baseline.
colors_fonts — tokens + font (define here; everything, incl. page styles, inherits them).
// color tokens (flat is fine for small/medium forms; mature toward Less maps for large apps)
@color_primary:#151f6d; @color_white:#fff; @color_black:#0f0f0f; @color_border:#EAECF0;
@color_grey:#b0adb7; @font_main:'Inter';
// status palette as one token per state (drives chips/rows by status)
@color_new:#A7D8F0; @color_in_progress:#A7E3B1; @color_completed:#B6E3C5; @color_pending:#FBE7A1;
// icons/logos as URL tokens (proxied workspace assets or relative attachments/…)
@icon_search: url('https://…/api/1.0/download/…svg?preview=true');
@font-face { font-family:'Inter'; font-weight:100 900; font-display:swap;
src:url(https://fonts.gstatic.com/…woff2) format('woff2'); unicode-range:U+0000-00FF, …; }
init_styles — platform resets + project-wide component defaults. @imported from
styles/index after colors_fonts. Page-specific exceptions do not go here (→ pages/<id>/style).
// ── 1. core resets ───────────────────────────────────────────────────────────
*, *::before, *::after { box-sizing: border-box; } // the single key reset
// Every .section__content ships its OWN grey bg + padding:20px 16px 0 + margin-bottom:20px
// (some rules theme-scoped → 0,2,0), so it paints a grey padded box INSIDE your cards.
// Neutralize it once here; re-add padding on your own card wrapper. !important beats the theme rule.
.section .section__content { background:transparent !important; margin-bottom:0 !important; padding:0 !important; }
.label, .button, .form { margin:0 !important; font-size:16px; line-height:1.5; }
[class*="row__item"] { display: contents; } // row/w items flow into the parent
.button-wrapper { width:auto !important; display:block; }
.button-wrapper:has(.hidden) { display:none; }
// ── 2. platform chrome (header/footer) — NOT hidden by default; reachable via:
// & { #pageWrap > [class*="content"] > [class*="header"],
// #pageWrap > [class*="content"] > [class*="footer"] { display:none !important; } }
// ── 3. (optional) full-bleed — drop the default centered/capped card ─────────
.content__main { padding:0 !important;
[data-class="grid"] { display:block; }
[data-class="grid-one-column"] { max-width:none; } }
// ── 4. project font (token from colors_fonts) ────────────────────────────────
input, textarea, .button > span, .toggle__title,
:not(&) body > .popoverContent, span[class*="label"], & { font-family:@font_main, sans-serif; }
// ── 5. utilities ──────────────────────────────────────────────────────────────
.visually_hidden { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
white-space:nowrap; clip-path:inset(100%); clip:rect(0 0 0 0); overflow:hidden; }
.font_12{font-size:12px;} .font_14{font-size:14px;} .mb_10{margin-bottom:10px !important;}
// ── 6. loading overlays (platform spinners) ──────────────────────────────────
[class*="button__spinner__wrap"], [class^="table__spinner__wrap"] {
position:fixed; inset:0; z-index:10000; display:flex; align-items:center;
justify-content:center; background:rgba(0,0,0,.1); backdrop-filter:blur(1px); }
// ── 7. project-wide component defaults (customize to brand) ──────────────────
.button { // ONE base button look used on every page
padding:14px 20px; border:none; border-radius:10px; font-size:15px; font-weight:600;
cursor:pointer; line-height:1; display:flex; align-items:center; justify-content:center; gap:8px;
transition:all .3s cubic-bezier(.4,0,.2,1);
&.primary_btn { background:@color_primary; color:@color_white;
&:hover { transform:scale(1.02); box-shadow:0 6px 20px fade(@color_primary,40%); } }
// … add your semantic variants (.submit_btn, .cancel_btn, …) by this pattern …
}
.modal { // default sizing by modalSize
&[class*="small"] { width:400px !important; }
&[class*="medium"], &[class*="large"] { width:auto !important; max-height:90% !important; }
&[class*="xlarge"] { width:80% !important; max-height:90% !important; }
}
Pattern catalogue (reusable recipes)
Layout
- Centered card form — cap and center the grid wrapper:
[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: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):.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__<id>), 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:.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(.<innerSection>) 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:.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)
State without CSS-only tricks
- Screens-as-sections — put each step/success/error/loading state as a sibling section, the backend flips
visibility (one stylesheet, many screens).
- Hidden state carriers —
edit items with styleClass:"visually_hidden" stash client state (selected id, cart id) for submit.
Icons
- Recolorable monochrome —
mask:url(@icon) no-repeat center; mask-size:contain; background-color:currentColor; (inherits text color). Bundled asset: relative attachments/cart.svg; external: proxied /api/1.0/download|image.
- Full-color —
background-image:@icon token.
RTL / theme toggle from the backend
- Page root
styleClass:"{{settings_page_text_align}}"; rule .form.text_align_right { … flex-direction:row-reverse; text-align:right; … }. The backend pushes one value to mirror the whole UI. Same mechanism toggles nav-active state, light/dark, etc.
Design-system approach (advanced — for large/branded apps)
When a form grows or needs strict brand consistency, build a token system in Less (this is the
LMS form's approach — Tailwind/Untitled-UI in Less):
// maps for everything, not just color
@breakpoint: { minimum-mobile:375px; mobile-tablet:820px; tablet-desktop:1180px; }
@space: { 0:2px; 1:4px; 2:8px; 3:12px; 4:16px; 5:20px; 6:24px; 8:32px; /* … */ 64:256px; }
@color: { @gray:{25:#FCFCFD; /* … */ 700:#344054; 900:#101828;} @brand:{…} @error:{…} @success:{…} }
// generate utility classes from a map (each + guard)
.make-space-set(@base,@rule,@min,@max){ each(@space,{ .apply(@k,@v);
.apply(@k,@v) when (@k<=@max) and (@k>=@min){ .@{base}-@{k}.@{base}-@{k}{ @{rule}:@v; } } }); }
.make-space-set(mt, margin-top, 0, 16); // → .mt-0 … .mt-16, applied in styleClass
// typography scale via lookup mixin
.text-font(@size; @weight:regular){ @s:@typography[@text][@@size];
font-size:@s[font-size]; font-weight:@font-weight[@@weight]; line-height:@s[line-height]; }
// responsive tokens: Less map → CSS var overridden per breakpoint
@media (min-width:@breakpoint[tablet-desktop]) { :root { --space-large: 32px; } }
.gap-large { gap: var(--space-large); }
Advanced Less available: each(), guards (when), maps + indirect lookup (@@key),
replace(), extract()/if()/isnumber()/isdefined(), detached-ruleset returns (mixin()[@result]).
Caveat: keep this in a modular styles/ split (colors_fonts, space, typography,
shadows, mixins, index) — a single 7k-line file is the maintainability ceiling.
Gotchas (learned from real forms)
- Windows filenames: a folder/file titled with a reserved char (
\ / : * ? " < > |) breaks
pullSmartForm on Windows (mkdir fails). Don't name definitions like edit|create_…. If a
pull aborts on mkdir … syntax is incorrect, rename the offending folder in the editor and
re-pull (or recover content from a .graph export that carries scripts/<uuid>/… file trees).
- 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 <img>. The default clamp is max-height:32px on both <img> 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 <td>):.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;
don't analyze or ship them.
- External images are proxied:
https://… URLs in CSS get /api/1.0/image?src= prepended
automatically (actor-attached images are internal and not proxied).
// comments compile away (prefer over /* */); keep nesting < 3 levels.
- Edit
develop only; pages/<id>/style needs no @import (auto-included).
Reference documents
| Path |
When to read |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md |
The rendered-DOM map: full per-component tag tree + class names, stable hooks vs. volatile hashes, page skeleton & theme classes, all table types, and which components need backend data to render |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md |
Component class catalogue, styleClass, templating, change protocol |
$CLAUDE_PLUGIN_ROOT/docs/user-flows/smart-forms.md |
Project file structure, deploy/release, styles compilation |
$CLAUDE_PLUGIN_ROOT/skills/simulator-smart-forms/SKILL.md |
The pull/push/deploy cycle and |
…(truncated)
1---2name: simulator-styles3description: Simulator.Company Smart Form (CDU) STYLING specialist — authoring complex Less/CSS for Smart Forms: theme tokens, page/form/section layout, component re-skinning, reusable style patterns, responsive and design-system approaches. Use when the user wants to STYLE or RESTYLE an existing Smart Form / CDU app — change its look, build a theme, style a table / sidebar / modal / form, add a design system, fix spacing/colors/fonts, or apply a complex visual design. This skill owns the `style` / `styles/` layer; it reuses the Smart Form tools (pullSmartForm / pushSmartForm / deploySmartForm) but does NOT create form templates — for the data-schema form template use `simulator-forms`, and for page layout / viewModel / backend logic use `simulator-smart-forms` / `simulator-smart-forms-logic`. Activate on: "style a smart form", "CDU styles", "theme the form", "restyle", "custom CSS/Less for the app", "style the table/sidebar/modal/button", "design system for the smart form", "make it look like …", "застилізувати смартформ4---56# Simulator.Company Smart Form STYLING specialist78You author and apply **CSS/Less styles** for Smart Forms (CDU / Script apps) on9Simulator.Company. Your domain is the **`style` / `styles/` layer** — themes, layout, component10re-skinning, reusable patterns, responsive and design-system approaches.1112This skill is built on patterns reverse-engineered from real production Smart Forms; the recipes13below are taken from live Less, not invented.1415---1617## Scope — what this skill owns (and what it does NOT)1819| Concern | Skill |20|---|---|21| **Styling**: `style` / `styles/*`, `pages/<id>/style`, `styleClass`, themes, Less | **this skill** |22| Form template (data fields / Account Template) | `simulator-forms` |23| Page layout JSON (`pages/<id>/config`: grid/forms/sections/items), viewModel, locale | `simulator-smart-forms` |24| Backend logic (Corezoid `/get` `/send`, dynamic viewModel, `changes[]`) | `simulator-smart-forms-logic` |2526You **reuse** the Smart Form engine tools (`pullSmartForm`, `pushSmartForm`, `deploySmartForm`,27file-history/rollback) — you do not introduce new platform behaviour. When the user needs a *new28form/page* first, defer to `simulator-smart-forms`; you come in to make it look right. To attach a29`styleClass` to a component you may need a one-line edit to `pages/<id>/config` — that's in scope30(it's the binding), but designing the layout itself belongs to `simulator-smart-forms`.3132---3334## How Smart Form styling works (the model)35361. **One Less stylesheet, compiled per save, scoped to `.cdu-page`.** Whatever you write is wrapped37 in `.cdu-page { … }` at serve time, so `&` = the page root and your styles can't leak out. Less38 syntax (variables, mixins, `@import`, functions, maps, guards, `each()`) is fully supported.392. **File organization — and where each rule goes.** Two layouts compile identically; **prefer the40 modular `styles/` folder** for anything non-trivial:41 - **Legacy single file** — a root `style` file holds everything (big forms: *Admin Panel*, *CMS*, *LMS*).42 - **Modular `styles/`** — `styles/index` is the entry point and mainly just `@import`s partials,43 **in cascade order**:44 ```less45 // styles/index — the entry/manifest; imports in cascade order46 @import "colors_fonts"; // tokens FIRST (everything below + page styles inherit them)47 @import "init_styles"; // then platform resets + project-wide component defaults48 // … any other shared partials (mixins, shared components) …49 ```50 - **Page styles** — `pages/<id>/style` is **auto-appended after** the main stylesheet (so it51 **wins the cascade**) and **inherits all root variables/mixins** (no `@import` needed). The52 platform saves it as `text/css` automatically. It is for **that page's exceptions only**.5354 | File | What belongs here |55 |---|---|56 | `styles/index` | Just `@import`s (the entry/manifest), in cascade order. |57 | `colors_fonts` | Color + font **tokens** (`@color_*`, `@font_*`, `@font-face`). Define them here because everything else — including page styles — inherits them. |58 | `init_styles` | **(a)** neutralize the platform's default styling (resets); **(b)** project-wide **component defaults** — one base look reused on every page (e.g. a `.button` skin). |59 | `pages/<id>/style` | **Only the page's differences** from the shared styles — page layout + one-off component tweaks. Never define tokens here (they live in `colors_fonts`). |603. **`styleClass` is the binding contract** between layout JSON and CSS. Every grid / form /61 section / item in `pages/<id>/config` may carry a `styleClass`; your CSS targets that class.62 - **Static** for structure: `"styleClass":"main_table"`.63 - **Dynamic** for backend-driven state/theme: `"styleClass":"{{settings_page_text_align}}"` —64 Corezoid pushes the value (e.g. `text_align_right`, `active_sidebar_btn`) to switch styling.654. **Validation**: CSS is **not** validated on save; a Less compile error is emitted as a66 `/* Less Error … */` comment rather than breaking the page. `styleClass` values are never67 validated — a class with no matching rule is harmless (but dead; clean it up).685. **Reaching renderer internals**: the public knobs are `styleClass` + documented component69 classes (`.button`, `.edit`, `.select`, `.table`, `.check`, …). For deeper structure use70 substring/attribute selectors — `[class*="table__wrap"]`,71 `[data-class="grid-one-column"]`.72 **Winning the cascade** — the renderer ships base/inline styles AND per-component CSS-module rules73 that load *after* your scoped `styles/index`, so an equal-specificity rule of yours **loses the74 tie by source order**. Beat it by **raising specificity with more stable classes, not ids**: chain75 your `styleClass` + an ancestor `[class*="…"]` + the element — e.g.76 `.book-table td.bc-cover-cell .file img` (0,3,2) beats the component's `.file__item__hash img`77 (0,2,1). Prefer this class chain over `#id` (brittle — it pins the rule to a config `id`, and78 classes are the house rule) and over a lone `!important` (it only beats non-`!important`; in an79 `!important` vs `!important` fight specificity still decides). A doubled class (`.x.x`) is a last80 resort. **See the verified per-component DOM map below for exact hooks (`edit`, `select`,81 `multiselect`, `radio`, `button`, `row__<name>`).**8283 **The cascade math, with real numbers** (measured against a live renderer build — dump the84 defaults yourself to confirm, see Workflow §"dump the defaults"):85 - Your stylesheet is auto-wrapped in `.cdu-page`, so a **bare single-class rule you write is86 already `0,2,0`** (`.cdu-page .myClass`) and **beats a bare renderer default `.hashed` (`0,1,0`)87 for free** — most overrides need nothing more.88 - **BUT the renderer theme-scopes ~⅓ of its rules** (`.theme-light .x` / `.theme-dark .x`, on89 `#mainRoot`), and those are **also `0,2,0`** — a *tie* with your wrapped single-class rule, and90 since defaults load **after** you, **the theme default wins**. Symptom: a `background`/`color`91 override "does nothing" even though your selector clearly matches. Beat it with an extra class92 (`0,3,0`) or `!important`.93 - **~1 in 5 default rules already use `!important`** (heavy on `active`/`selected`/`checked`94 states and theme colors). Matching them with your own `!important` is **normal here, not a smell**95 — a lone `!important` still loses to a default `!important` of higher specificity, so pair it96 with a solid class chain.97 - **Slots own their own background/padding.** Containers like `.section__content`, the sidebar98 slots (`.sidebar__header/__content/__footer`), and `.page__sidebar` each ship their own bg/padding99 — painting the *parent* won't show through. Override the **slot**, not its ancestor.100101> Authoritative component-class + CSS reference: the **"CSS styling"** tag in the CDU swagger and102> `$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md`.103104---105106## Rendered DOM map (verified component internals)107108Verified against a live `control-cdu` render. The summary below is enough for most work; for the109**full per-component tag tree of every component** (incl. all 4 table types, page skeleton,110overlays) see `$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md`. Use this to target111the right element instead of guessing. **Three rules first:**1121131. **`styleClass` lands on the component ROOT.** E.g. `class="label hd-meta label__12ehI"`,114 `class="edit edit__text txt-input … bordered"`, `class="radio pills … horizontal"`. So a root115 hook (`.hd-meta`, `.txt-input`, `.pills`) always hits — but only the **outer** element.1162. **Internals use hashed CSS-module classes** (`label__12ehI`, `f-item-0-2-64`, `i-label-0-2-70`,117 `clickOutside(i)-field-0-2-97`) that change between renderer builds. **Never target the hash.**118 Reach inside via substring selectors (`[class*="radioItem"]`, `[class*="i-icon"]`,119 `[class*="chip"]`, `[class*="i-edit"]`), stable state classes (`.checked` `.disabled`120 `.bordered`), `data-class` attributes, or element selectors (`input`, `textarea`, `label`,121 `svg`).1223. **`row` / `w` grouping yields a STABLE group class `.row__<rowName>`.** An item with `"row":"act"`123 renders inside `<div class="row row__act row__hash">` whose children are124 `<div class="row__item__…" style="width:…%">`. Note `w` is a **relative weight**, not a raw125 percentage: the rendered width is `w / Σw` across the row (two items at `w:50` each get 50%; at126 `w:50` + `w:100` they get 33% / 67%). Style the group via `.row__act` (flex container) and items127 via `[class*="row__item"]` — your reliable hook for multi-column rows, progress bars, etc.128129**Page skeleton & theme** (stable structural hooks):130131- **Scope:** all your CSS is wrapped in `.cdu-page` (`&` = page root); per-page hook132 `.cdu-page-<pageId>`.133- **Dark mode is a CLASS, not a media query:** `.theme-light` / `.theme-dark`. In a live render the134 class sits on the host wrapper `#mainRoot` (an **ancestor** of `.cdu-page`), which is why135 `.theme-dark .… {}` works from inside your scoped stylesheet. `control-cdu` itself also stamps the136 theme class onto `#page` (the `.cdu-page` node) — so **verify the placement on your build**: if the137 class is only on the `.cdu-page` node (not an ancestor), a wrapped `.theme-dark .foo` won't match138 and you'd need `&.theme-dark .foo`. Prefer this class approach over `@media (prefers-color-scheme)`.139 ⚠️ Caveat: the renderer's `light`/`dark` token maps are currently **identical** — the mechanism is140 wired but dark mode has no distinct palette yet, so a `.theme-dark` override is the only way to make141 dark actually differ today.142- **Grid regions:** `[data-class="grid-one-column"]` / `[data-class="grid-two-column"]`143 (+`-left`/`-right`); header/footer regions via `[class*="gridtwo__header"]` /144 `[class*="gridtwo__footer"]`.145- **Section slots:** `[data-class="section"]` (+ `[class*="block__"]` for `type:"block"` cards);146 inner `[class*="section__header"]` / `[class*="section__content"]`.147- **Toasts:** two `[class*="notify"]` containers (top inside `#page`, bottom at `#mainRoot` end).148149**Per-component root + key inner hooks** (`<sc>` = your `styleClass`):150151| Component | Root selector (styleClass here) | Key inner hooks / notes |152|---|---|---|153| `label` | `.label` / `[data-class="label"]` | `<span>` text; BBCode → real tags; `align`→ `.left__`/`.center__`/`.right__` |154| `divider` | `.divider` | empty |155| `edit` (all types) | `.edit` (+ `.edit__<type>`) | `.field > input` / `textarea`; states `bordered`(box)/`selected`/`error`; kill box `.field{border:none}`; help/err `[class*="Component-helperText"]` |156| `select` | `.select` (outer) | readonly `<input>` in `[class*="i-edit"]` + caret `[class*="endAdornment"]`; **no native `<select>`** |157| `multiselect` | `.multiselect` | chip field: `[class*="chip"]` + search `<input>` via `[class*="clickOutside(i)-field"]`; **not** checkbox rows |158| `radio` | `.radio` (+ `.horizontal` for row) | options `[class*="radioItem"]`(+`.checked`/`.disabled`); hide svg `[class*="i-icon"]`, style label `[class*="i-label"]` → pills/scales |159| `check` | `.check` | `[class*="f-icon"]` svg + `<label>`; states `.checked`/`.error`; native `input{appearance:none}` |160| `toggle` | `.toggle` (+ `.left__`/`.right__`) | `[class*="toggle__button"]`(+`.active`) / `[class*="i-switch"]` |161| `slider` | `.slider` (+ `.skillBar__`) | `rc-slider`: `.rc-slider-rail`/`-track`/`-handle`/`-dot`; `[class*="slider__header"]`, `[class*="slider__min"]`/`max` |162| `otp` | `.otp` | boxes `[class*="otp__edit"] input` |163| `phone` | `[data-class="phone"]` | `#countryCode .select` + `#number .edit input`; `[class*="phone__items"]` |164| `image` | `[data-class="image"]` | `<img>` (src **proxied** via `/api/1.0/image`); `align`→ `.center__` |165| `timer` | `[data-class="timer"]` | `<span>` text |166| `comments` | `[data-class="comments"]` | `[class*="mes__wrap"]`, `[class*="mes__name"]`, `[class*="mes__content"]`, avatar `[class*="i-avatar"]` |167| `carousel` | `.carousel` | preview pane `[class*="carousel__preview"]` (zoom/nav btns), thumb strip `[class*="carousel__content__item"]`(+`.active__`); give it a full File `value` for a correct preview (tolerates a missing `type` — won't error-stub) |168| `button` | wrapper `[data-wrapper-for="<id>"]` → inner `#<id>.button.button__<type>` | **`<sc>` is on the INNER button**; text `[class*="button__label"]`; align via wrapper/`.row__<name>`; 7 types via `.button__<type>` |169| `copy` | `[data-class="copy"]` | `[class*="copy__container"]`, icon `[class*="i-icon"]`, label `[class*="i-label"]` |170| `tab` | `.tab` (outer `.tab__…`) | items `[class*="tab__item"]`(+`.active`/`.error`); `hidden` option is absent from DOM |171| `stepper` | `[data-class="stepper"]` | items `[class*="stepperItem"]`(+`.completed__`/`.active__`); label `[class*="stepperItem__label"]` |172| `mainMenu` | `nav.mainMenu` | items `[class*="mainMenuItem"]`(+`.active`); groups `<details>`/`[class*="mainMenuItemGroup"]`; `data-depth` + `--depth`; badge `[class*="i-badge"]` |173| `upload` | `[data-class="upload"]` | `[class*="upload__box"]`, corners `[class*="upload__corner"]`, `input[type=file]` (webcam: trigger `div.upload__file__…[role=button]`, needs `extra.accept`) |174| `file` | `.file` | image → `[class*="file__item"] img`; pdf/doc → `.pg-viewer`/`.pdf-viewer`; path chosen by mime `value.type` |175| `attachment` | `[data-class="attachment"]` | upload ctrl `[class*="i-upload"]`; chips `[class*="fileItemChip"]` (text `[class*="e-chipText"]`, remove `[class*="e-delFileIcon"]`) |176| `signature` | `.signature__wrap` | `<canvas>`; toolbar `[class*="signature__toolbar__clear"]`/`__save` |177| `table` | `[data-class="table"]` (+ `.table__check`/`__radio`/`__group`) | see **Tables** below |178| `widget` | `[data-class="widget"]` | `iframe[class*="iframe__"]`; `[class*="widget__inner"]`(+`.hidden__` until load) |179| row/w group | `.row__<rowName>` | flex container; items `[class*="row__item"]` (inline `width`) |180| `draggable` (sortable/contentLoop) | `[data-class="draggable"]` | grip `[class*="draggable__handle"]`, body `[class*="draggable__content"]` |181| `notification` (toast) | **none** — page root `[class*="notify"]` | `[class*="notifyItem"]` (+ severity `[class*="success"]`/`[class*="error"]`/`[class*="info"]`) → text `span[class*="i-title"]` + close `i[class*="closeIcon"] > svg .fill` |182183**Tables** (DOM is a real `<table>`): head cells `[class*="table__head"]` (sortable col184`[class*="sortable__"]`, sort arrow `[class*="table__head__icon"]`); sticky col → `td.sticky-col`185+ the head cell's own `styleClass`; body rows `.table-row` / `[class*="table__row__"]`186(selected radio row → `.active__…`); first column for `check`/`radio` → `[class*="table__check"]`187/ `.i-radioItem-…`; group title row → `[class*="table__row__group"]`. **Default-table cell188mini-components** inside `<td.table-cell>`: file → `[class*="table__img"]`, copy →189`#<id>--copy[data-class="copy"]`, check → `#<id>--check[class*="table__check"]`, button →190`[class*="table__button"]` (id `<id>--button`, **not** `.button`).191192> **Table cells are NOT text-only.** A `default`-table cell can be plain text, an **image** (`file`193> cell → real `<img>`), a **button**, a `copy`, or a `check` — so covers/actions live inside a table194> (cell JSON shape belongs to `simulator-smart-forms`; you style via each cell's own `styleClass`,195> which lands on its `<td>`). And **plain + head cells render BBCode** (verified — see the BBCode196> matrix below): the text goes into a plain `<td>`/`<div>`, so `[b]/[color]/[div]…` expand there.197> This is why you can build a status pill in a cell **without** a button cell: put `[div]Label[/div]`198> in the plain cell and style `.<cellClass> div`.199200**Overlay sections:** `modal` → backdrop `[class*="i-bg"]` > box `[class*="modal__"]` (+ size201`i-small`/`i-medium`/`i-large`/`i-xlarge` ← `modalSize`), header `[class*="section__modal__header"]`.202`float` → `[class*="float__"]` inside `[class*="Component-wrapper"]` with drag bar203`[class*="section__header__dragable"]` + 8 `[class*="Component-resizeHandle"]`.204205> ⚠️ **File-bearing components need a complete `value`.** The **`file` component and a `default`206> table's `file` cell** read mime via `value.type` **with no guard** — a File missing `type` throws207> and the item renders as a `[class*="item__error"]` stub. (`carousel` and `attachment` also read208> `value.type` but tolerate it missing — `carousel` defaults to `''` and uses optional chaining — so209> they won't stub out; still give them the full shape for a correct preview.) Give File values the210> full shape `{fileName, fileSrc, title, type, size}`. A stub can also **suppress later siblings** (a211> crashing table cell hid the `modal`/`float` sections after it). `upload[webcam]` needs212> `extra.accept`. With complete values everything renders statically — no backend needed. Full213> per-component trees: `$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md`.214215**Worked example — radio rendered as pills** (the native input is hidden; the svg is the control):216217```less218.pills { display:flex; flex-wrap:wrap; gap:8px; }219.pills [class*="i-icon"] { display:none; } // hide the svg circle220.pills [class*="radioItem"] { border:1px solid @line; border-radius:100px; }221.pills [class*="radioItem"] label { padding:7px 16px; } // make the whole pill the label222.pills [class*="radioItem"].checked { background:@ink; }223.pills [class*="radioItem"].checked label { color:#fff; }224```225226**Worked example — toast notification.** Toasts render at the page root (inside `.cdu-page`, which227scopes your CSS), so you reach them structurally — there is no `styleClass`. For the DOM nesting +228the two traps (`[class*="i-icon"]` matches the close ✕, not a severity icon — don't blanket-hide it;229`[class*="i-title"]` also matches the wrapper `i-titleContainer` — qualify as `span[class*="i-title"]`)230see the **Toasts** entry in `cdu-dom-tree-reference.md`. A reusable skin:231232```less233[class*="notify"] [class*="notifyItem"] {234 background:@white; color:@ink; border:1px solid @line; border-radius:@r;235 box-shadow:0 8px 28px rgba(0,0,0,.12); padding:14px 16px;236 display:flex; align-items:center; gap:10px;237}238[class*="notify"] [class*="notifyItem"]::before { // severity dot239 content:""; width:8px; height:8px; border-radius:50%; background:@ink-3; flex-shrink:0;240}241[class*="notify"] [class*="notifyItem"][class*="success"]::before { background:#16a34a; }242[class*="notify"] [class*="notifyItem"][class*="error"]::before { background:#dc2626; }243[class*="notify"] [class*="titleContainer"] { flex:1; min-width:0; }244[class*="notify"] span[class*="i-title"] { font-size:14px; font-weight:500; color:inherit; }245[class*="notify"] [class*="closeIcon"] { opacity:.45; cursor:pointer; } // keep the ✕246[class*="notify"] [class*="closeIcon"]:hover { opacity:1; }247[class*="notify"] [class*="closeIcon"] svg .fill { fill:currentColor; }248```249250> Hashes (`-0-2-64`) are version-pinned to the renderer build — substring selectors survive a bump,251> exact-hash selectors don't. If a restyle suddenly breaks, re-dump the rendered DOM (browser252> DevTools → copy `outerHTML` of `#page`) and re-verify these hooks.253254---255256## BBCode — where it renders (empirically verified)257258A component's text is expanded to HTML by the client (`Utils.bbCodeToHtml`) **only in some fields**.259The smart-form BBCode set (swagger `bbcode` tag) is260`[b] [i] [u] [ul][*] [iurl=…] [url=…] [div] [br] [color=…] [size=…] [bg=…]` — note `[div]` (the only261block-level tag) and `[iurl]` (internal link); there is **no `[span style=…]`** here (that exists262only in actor/reaction BBCode). Mapping: `[color]→<span style="color">`,263`[bg]→<span style="background-color">`, `[div]→<div>`.264265**Verified support matrix** (built by rendering a live probe string, not read off the swagger):266267| Renders BBCode ✅ | Stays literal ❌ |268|---|---|269| `label.value` | `edit` value / placeholder / multiline |270| `button.title` | `select` value · `multiselect` option |271| `check.title` | `radio` option title |272| `stepper` option title | `toggle.title` · `tab` option title |273| `form.title`, `mainMenu` title, `comments`, `carousel` item title, `upload.title` | `copy.title` |274| **`table` head title AND plain cell** | |275276Heuristic: it expands where the text lands in a plain **text node** (`<span>/<div>/<td>`) and stays277literal where it lands in a form-control **`value`/`placeholder`** — but it's only a rough heuristic:278it's whether that specific component passes its title through `bbCodeToHtml`/`dangerouslySetInnerHTML`,279not the node type. `check` ✅ but `radio` ❌ even though both render a `<label>`; `stepper` ✅ but280`toggle`/`tab` ❌. **Trust the matrix (built from the renderer source), not the swagger:** the swagger281marks only `label/button/mainMenu` as bbcode fields, yet table cells clearly render it. When a field282isn't in the matrix, confirm it in the live UI before relying on it.283284**Styling use — inject a styleable element where there is none.** A plain `table` cell is bare text285in a `<td>` (no inner wrapper to hook). `[div]…[/div]` gives you an inner `<div>` to style as a286pill/badge (`.<cellClass> div { … }`); `[bg]`/`[color]` give inline `<span>`s. (Alternative with no287inner element: absolutely-position the `<td>` so it shrink-wraps its text into a pill.)288289---290291## Workflow (reuses the Smart Form tools)292293```2941. pullSmartForm(actorId) → downloads <actorId>/develop/ + production/ (incl. style / styles/, pages/<id>/style)2952. Edit ONLY under develop/:296 - styles/index (+ partials) OR the root `style` file297 - pages/<id>/style (page-specific)298 - pages/<id>/config (only to add/adjust a styleClass hook)2993. pushSmartForm(actorId) → validates + uploads changed files; style/styles files carry MIME text/css (the push tool sets it; the server stores the client-supplied type — it does not force it by folder)3004. deploySmartForm(actorId) → publishes develop → production (when approved)301```302303Rules: **edit `develop` only** (`production` is readonly); **`pullSmartForm` first** (it writes the304`.manifest.json` push needs). Files under `styles/` are sent as `text/css`; everything else305`application/json`.306307**Verifying a styling result.** `appGetPage` returns the **server-resolved config** (locale/viewModel308expanded) — it does **not** run CSS compilation or `bbCodeToHtml`, so you **cannot** confirm a visual309result or whether BBCode expanded from it. Visual/BBCode results are verified in the live UI by the310user.311312**When to ask for the rendered HTML.** First fix from the DOM map below; if it doesn't land, correct313once more. **If after the second correction the user still doesn't get the expected result, stop314guessing and ask them to copy the element's HTML** (Inspect → Copy `outerHTML` of the element, or dump315`#page`). The real DOM — hashed classes, wrapper nesting, inline sizes — resolves what the map can't;316most restyle stalls end the moment you read it.317318---319320## Starter kit — snippets that recur across real forms321322These appear near-verbatim in multiple production forms; lift them as a baseline.323324**`colors_fonts` — tokens + font (define here; everything, incl. page styles, inherits them).**325```less326// color tokens (flat is fine for small/medium forms; mature toward Less maps for large apps)327@color_primary:#151f6d; @color_white:#fff; @color_black:#0f0f0f; @color_border:#EAECF0;328@color_grey:#b0adb7; @font_main:'Inter';329// status palette as one token per state (drives chips/rows by status)330@color_new:#A7D8F0; @color_in_progress:#A7E3B1; @color_completed:#B6E3C5; @color_pending:#FBE7A1;331// icons/logos as URL tokens (proxied workspace assets or relative attachments/…)332@icon_search: url('https://…/api/1.0/download/…svg?preview=true');333@font-face { font-family:'Inter'; font-weight:100 900; font-display:swap;334 src:url(https://fonts.gstatic.com/…woff2) format('woff2'); unicode-range:U+0000-00FF, …; }335```336337**`init_styles` — platform resets + project-wide component defaults.** `@import`ed from338`styles/index` *after* `colors_fonts`. Page-specific exceptions do **not** go here (→ `pages/<id>/style`).339```less340// ── 1. core resets ───────────────────────────────────────────────────────────341*, *::before, *::after { box-sizing: border-box; } // the single key reset342// Every .section__content ships its OWN grey bg + padding:20px 16px 0 + margin-bottom:20px343// (some rules theme-scoped → 0,2,0), so it paints a grey padded box INSIDE your cards.344// Neutralize it once here; re-add padding on your own card wrapper. !important beats the theme rule.345.section .section__content { background:transparent !important; margin-bottom:0 !important; padding:0 !important; }346.label, .button, .form { margin:0 !important; font-size:16px; line-height:1.5; }347[class*="row__item"] { display: contents; } // row/w items flow into the parent348.button-wrapper { width:auto !important; display:block; }349.button-wrapper:has(.hidden) { display:none; }350351// ── 2. platform chrome (header/footer) — NOT hidden by default; reachable via:352// & { #pageWrap > [class*="content"] > [class*="header"],353// #pageWrap > [class*="content"] > [class*="footer"] { display:none !important; } }354355// ── 3. (optional) full-bleed — drop the default centered/capped card ─────────356.content__main { padding:0 !important;357 [data-class="grid"] { display:block; }358 [data-class="grid-one-column"] { max-width:none; } }359360// ── 4. project font (token from colors_fonts) ────────────────────────────────361input, textarea, .button > span, .toggle__title,362:not(&) body > .popoverContent, span[class*="label"], & { font-family:@font_main, sans-serif; }363364// ── 5. utilities ──────────────────────────────────────────────────────────────365.visually_hidden { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;366 white-space:nowrap; clip-path:inset(100%); clip:rect(0 0 0 0); overflow:hidden; }367.font_12{font-size:12px;} .font_14{font-size:14px;} .mb_10{margin-bottom:10px !important;}368369// ── 6. loading overlays (platform spinners) ──────────────────────────────────370[class*="button__spinner__wrap"], [class^="table__spinner__wrap"] {371 position:fixed; inset:0; z-index:10000; display:flex; align-items:center;372 justify-content:center; background:rgba(0,0,0,.1); backdrop-filter:blur(1px); }373374// ── 7. project-wide component defaults (customize to brand) ──────────────────375.button { // ONE base button look used on every page376 padding:14px 20px; border:none; border-radius:10px; font-size:15px; font-weight:600;377 cursor:pointer; line-height:1; display:flex; align-items:center; justify-content:center; gap:8px;378 transition:all .3s cubic-bezier(.4,0,.2,1);379 &.primary_btn { background:@color_primary; color:@color_white;380 &:hover { transform:scale(1.02); box-shadow:0 6px 20px fade(@color_primary,40%); } }381 // … add your semantic variants (.submit_btn, .cancel_btn, …) by this pattern …382}383.modal { // default sizing by modalSize384 &[class*="small"] { width:400px !important; }385 &[class*="medium"], &[class*="large"] { width:auto !important; max-height:90% !important; }386 &[class*="xlarge"] { width:80% !important; max-height:90% !important; }387}388```389390---391392## Pattern catalogue (reusable recipes)393394### Layout395- **Centered card form** — cap and center the grid wrapper:396 `[data-class="grid-one-column"] { max-width:440px; margin:20px auto; padding:20px; background:#fff; border-radius:8px; box-shadow:0 0 10px #0000001a; }`397- **Full-bleed admin** — unlock the cap: `.content__main [data-class="grid-one-column"] { max-width:none; }`398- **Fixed sidebar shell** — `.sidebar { width:250px; grid-template-rows:80px 1fr 120px; }` with header/content/footer panes.399- **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:400 ```less401 nav.mainMenu [class*="mainMenuItemGroup"], nav.mainMenu details, nav.mainMenu summary { margin:0 !important; padding:0 !important; }402 nav.mainMenu .mainMenuItem { padding:7px 10px; padding-left:calc(10px + var(--depth,0) * 14px); }403 ```404- **Pinned footer + scrolling body** — `.info_form{height:calc(100vh - 50px); display:flex; flex-direction:column;} .content_section{flex:1; max-height:calc(100vh - 100px);}`405- **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):406 ```less407 .section__content .draggable__handle { display:none !important; } /* kill grip → drag off */408 .section__content .draggable { border:none !important; background:transparent !important;409 padding:0 !important; box-shadow:none !important; cursor:default; } /* reset outer card, avoid double frame */410 .section__content .draggable__content { border:1px solid #ececec; border-radius:10px; padding:14px; }411 ```412- **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__<id>`), 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).413414### Components — re-skin, don't accept defaults415- **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; }`416- **Custom checkbox** — hide native (`input{-webkit-appearance:none}`, `i{display:none}`), draw tick on `.checked input::after` (rotated border).417- **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`.418- **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}`.419- **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:420 ```less421 .myTable { display:flex; flex-direction:column; }422 .myTable [class*="table__wrap"] { max-height:none !important; height:auto !important; }423 .myTable [class*="table__pagination"] { position:static !important; order:99;424 display:flex; justify-content:center; gap:8px; margin-top:20px; }425 ```426- **Status chip** — state-class + token map: `.status_cell.completed-… div{ background:@color_completed; }` (backend sets `styleClass:"status_cell completed-…"`).427- **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)`).428- **Toast notifications** — centered, severity icon via `[class*="notifyItem"][class*="success"]::before{ background-image:@icon_success; }`.429- **Modal sizing** — base `.modal` + `:has(.<innerSection>)` to size each, or size-class match `&[class*="xlarge"]{ width:80% !important; }`.430- **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`:431 ```less432 .breadcrumbs [class*="tab__item"] { width:auto !important; min-width:max-content !important;433 flex:0 0 auto !important; margin:0 !important; }434 .breadcrumbs [class*="tab__item"] [class*="i-label"] { width:auto !important; overflow:visible !important; }435 .breadcrumbs [class*="tab__item"]:not(:last-child)::after { content:"›"; margin:0 10px; opacity:.6; }436 ```437438### Interactivity (CSS-only, no backend round-trip)439- **Accordion / dropdown / collapsible tree** — a `check`/`toggle` with `.checked`, expanded by parent `:has()`, animated via `grid-template-rows:0fr → 1fr`:440 ```less441 .menu { display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s; }442 .sidebar:has(.nav_btn.checked) .menu { grid-template-rows:1fr; }443 ```444 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.445- **Slide-in panel / popover** — toggle `opacity`/`transform`/`pointer-events` under `&:has(.trigger.checked)`.446447### State without CSS-only tricks448- **Screens-as-sections** — put each step/success/error/loading state as a sibling **section**, the backend flips `visibility` (one stylesheet, many screens).449- **Hidden state carriers** — `edit` items with `styleClass:"visually_hidden"` stash client state (selected id, cart id) for submit.450451### Icons452- **Recolorable monochrome** — `mask:url(@icon) no-repeat center; mask-size:contain; background-color:currentColor;` (inherits text color). Bundled asset: relative `attachments/cart.svg`; external: proxied `/api/1.0/download|image`.453- **Full-color** — `background-image:@icon` token.454455### RTL / theme toggle from the backend456- Page root `styleClass:"{{settings_page_text_align}}"`; rule `.form.text_align_right { … flex-direction:row-reverse; text-align:right; … }`. The backend pushes one value to mirror the whole UI. Same mechanism toggles nav-active state, light/dark, etc.457458---459460## Design-system approach (advanced — for large/branded apps)461462When a form grows or needs strict brand consistency, build a **token system in Less** (this is the463*LMS* form's approach — Tailwind/Untitled-UI in Less):464465```less466// maps for everything, not just color467@breakpoint: { minimum-mobile:375px; mobile-tablet:820px; tablet-desktop:1180px; }468@space: { 0:2px; 1:4px; 2:8px; 3:12px; 4:16px; 5:20px; 6:24px; 8:32px; /* … */ 64:256px; }469@color: { @gray:{25:#FCFCFD; /* … */ 700:#344054; 900:#101828;} @brand:{…} @error:{…} @success:{…} }470471// generate utility classes from a map (each + guard)472.make-space-set(@base,@rule,@min,@max){ each(@space,{ .apply(@k,@v);473 .apply(@k,@v) when (@k<=@max) and (@k>=@min){ .@{base}-@{k}.@{base}-@{k}{ @{rule}:@v; } } }); }474.make-space-set(mt, margin-top, 0, 16); // → .mt-0 … .mt-16, applied in styleClass475476// typography scale via lookup mixin477.text-font(@size; @weight:regular){ @s:@typography[@text][@@size];478 font-size:@s[font-size]; font-weight:@font-weight[@@weight]; line-height:@s[line-height]; }479480// responsive tokens: Less map → CSS var overridden per breakpoint481@media (min-width:@breakpoint[tablet-desktop]) { :root { --space-large: 32px; } }482.gap-large { gap: var(--space-large); }483```484485Advanced Less available: `each()`, guards (`when`), maps + indirect lookup (`@@key`),486`replace()`, `extract()/if()/isnumber()/isdefined()`, detached-ruleset returns (`mixin()[@result]`).487**Caveat:** keep this in a **modular `styles/`** split (`colors_fonts`, `space`, `typography`,488`shadows`, `mixins`, `index`) — a single 7k-line file is the maintainability ceiling.489490---491492## Gotchas (learned from real forms)493494- **Windows filenames**: a folder/file titled with a reserved char (`\ / : * ? " < > |`) breaks495 `pullSmartForm` on Windows (`mkdir` fails). Don't name definitions like `edit|create_…`. If a496 pull aborts on `mkdir … syntax is incorrect`, rename the offending folder in the editor and497 re-pull (or recover content from a `.graph` export that carries `scripts/<uuid>/…` file trees).498- **Winning the cascade**: component CSS-modules load after you and win equal-specificity ties — beat499 them with a chain of stable classes (`styleClass` + `[class*="…"]` + tag), not `#id` or a lone500 `!important`. Full worked example + cascade math in §"How Smart Form styling works" pt 5.501- **Never write `.cdu-page` yourself**: the compiler wraps your whole stylesheet in `.cdu-page { … }`502 at serve-time, so a hand-written `.cdu-page` (or `.cdu-page:has(…)`) becomes a *descendant*503 (`.cdu-page .cdu-page…`) and silently never matches. The page root is `&` (= `.cdu-page`): paint the504 page with `& { background:#e30613; min-height:100vh; }` and target it with `&:has(…)`.505- **Image/`file` cell renders "wide but a thin strip"**: not a width bug but a **clipping ancestor** —506 a wrapper (`.file` / `.file__item`) keeps a small fixed height + `overflow:hidden`. Force the box at507 **every** wrapper level (`height` + `min/max-height` + `width:100%` + `max-width:none` +508 `overflow`), not only on `<img>`. The default clamp is `max-height:32px` on both `<img>` and509 `[class*="table__img"]`, and `max-height` always beats `height` — so `max-height:none` is the key.510 One height knob down the whole chain (`.card-img` = the cell's `styleClass`, applied on `<td>`):511 ```less512 .card-img, .card-img [class*="table__img"], .card-img .file, .card-img [class*="file__item"] {513 width:100%; height:240px !important; max-height:none !important; display:block; }514 .card-img img { width:100% !important; height:100% !important; max-width:none !important;515 max-height:none !important; object-fit:contain; } /* contain = whole image; cover = fill + crop */516 ```517- **Dead `styleClass`**: placeholder classes (`my-custom-class`) with no rule are harmless but518 noise — remove them.519- **Stale copies**: forms accumulate `style(copy)` / `style BACKUP` files — they're not wired in;520 don't analyze or ship them.521- **External images are proxied**: `https://…` URLs in CSS get `/api/1.0/image?src=` prepended522 automatically (actor-attached images are internal and not proxied).523- **`//` comments** compile away (prefer over `/* */`); keep nesting **< 3** levels.524- **Edit `develop` only**; `pages/<id>/style` needs no `@import` (auto-included).525526---527528## Reference documents529530| Path | When to read |531|---|---|532| `$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md` | **The rendered-DOM map**: full per-component tag tree + class names, stable hooks vs. volatile hashes, page skeleton & theme classes, all table types, and which components need backend data to render |533| `$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md` | Component class catalogue, `styleClass`, templating, change protocol |534| `$CLAUDE_PLUGIN_ROOT/docs/user-flows/smart-forms.md` | Project file structure, deploy/release, styles compilation |535| `$CLAUDE_PLUGIN_ROOT/skills/simulator-smart-forms/SKILL.md` | The pull/push/deploy cycle and536537…(truncated)