@imgly/codesign-mcp 0.1.9-next.20260721180834.g2260c2e → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -189,6 +189,8 @@ await engine.block.forceLoadResources([page]); // recurses into children
189
189
 
190
190
  `forceLoadResources(blocks)` returns `Promise<void>` that resolves once all fonts/images/fills bound to those blocks (and descendants) have finished loading. Pass `[page]` to await everything on the page. **No `setTimeout`, no polling.**
191
191
 
192
+ This barrier is for **your own measuring** inside `edit` code — positioning by measured frame width needs the fonts loaded first. Capturing is covered server-side: `preview`, `export`, `save`, and the commit at the end of every `edit` all force-load the scene's resources before rendering or serializing, so you never need a separate wait-for-fonts edit.
193
+
192
194
  `getFrameWidth(id) > 0` after the await is your "font loaded + shaped successfully" signal. **Never** use `getWidth(id) === 0` as a font-load probe — it returns 0 by design in Auto mode regardless of font state.
193
195
 
194
196
  <!-- feature:video -->
@@ -23,7 +23,6 @@ These are **runtime semantics that don't show up in the .d.ts**. Check this list
23
23
  | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
24
  | `setFont(id, newUri, …)` doesn't change the rendered font on an existing block. | Engine treats two typeface objects with the same `name` as identical and skips the swap. | Use a unique `name` per swap (e.g. `"Playfair Display v2"`), OR delete and recreate that single block with the new font. |
25
25
  | Text doesn't reflow after `setFont` / `setTextFontSize`. Old glyph layout sticks. | The text shape is cached and only invalidated when the string content changes. | After the font/size change, force a re-shape: `engine.block.setString(id, "text/text", engine.block.getString(id, "text/text"))`. |
26
- | `preview` immediately after `setFont` shows blank glyphs even though the URL is good. | Font fetch hasn't completed when export runs. | `await engine.block.forceLoadResources([page])` inside your edit before reading frame dims or capturing. Resolves when all fonts on the page (and descendants) are loaded. |
27
26
  | Text block exists with the right font, string, size, and position — but glyphs don't draw. | Block height is too small for the font's ascender + descender extent. The engine silently drops the render rather than overflowing or clipping. | Set height ≥ `fontSize × 1.6` for sans-serif body, `≥ 1.8` (better 2.0) for display serifs (Playfair, Cormorant) and scripts (Great Vibes, Caveat). Or use Auto + getFrameWidth (SKILL.md §4). |
28
27
  | `getWidth(id)` / `getHeight(id)` return `0` on an Auto-sized text block. Tempting to read this as "font didn't load → text broken". | `getWidth`/`getHeight` return what you set; in Auto mode that's `0`. They are **not** measurement APIs and **not** a font-load signal. | Use `engine.block.getFrameWidth(id)` / `getFrameHeight(id)` for the measured shaped extent. `getFrameWidth(id) > 0` is the actual "font loaded + shaped" probe. |
29
28
  | Pages attached via `engine.block.appendChild(scene, page)` don't show up, render at the wrong position, or break multi-page layout. Or `findByType("stack")` returns `[]` and `appendChild(stack, page)` errors with `parent: Expected number, received undefined`. | `engine.scene.create()` (no argument) creates a scene WITHOUT a stack. Pages must attach to a stack, not the scene root. | Always pass the layout type: `engine.scene.create("VerticalStack")` (or `"HorizontalStack"`). Then `engine.block.findByType("stack")[0]` returns the auto-created stack, and `engine.block.appendChild(stack, page)` works. The hierarchy is always **scene → stack → page**. |
@@ -40,7 +39,7 @@ These are **runtime semantics that don't show up in the .d.ts**. Check this list
40
39
 
41
40
  Cheap → expensive:
42
41
 
43
- 1. **Did you await fonts?** — `await engine.block.forceLoadResources([page])` inside the same edit before measuring or capturing.
42
+ 1. **Did you await fonts?** — `await engine.block.forceLoadResources([page])` inside the same edit before measuring. (Capturing is covered: `preview`/`export`/`save` and the commit itself run this barrier server-side.)
44
43
  2. **Height vs font size** — `getHeight(id) >= getTextFontSizes(id)[0] * 1.6`?
45
44
  3. **Position on page** — block within `[0, pageWidth] × [0, pageHeight]`?
46
45
  4. **Font URI valid** — was setFont called with the 3-arg form? Did the URI return 200?
@@ -572,9 +572,9 @@ export const INLINE_ASSETS = {
572
572
  "docs/guide/use-templates/programmatic.md": "> This is one page of the CE.SDK Node.js documentation. For a complete overview, see the [Node.js Documentation Index](https://img.ly/docs/cesdk/node.md). For all docs in one file, see [llms-full.txt](./llms-full.txt.md).\n\n**Navigation:** [Guides](./guides.md) > [Create and Use Templates](./create-templates.md) > [Programmatic](./use-templates/programmatic.md)\n\n---\n\nAutomate template workflows with CE.SDK's engine APIs for batch processing, personalization, and headless design generation.\n\n> **Reading time:** 15 minutes\n>\n> **Resources:**\n>\n> - [Download examples](https://github.com/imgly/cesdk-web-examples/archive/refs/tags/release-$UBQ_VERSION$.zip)\n>\n> - [View source on GitHub](https://github.com/imgly/cesdk-web-examples/tree/release-$UBQ_VERSION$/guides-use-templates-programmatic-server-js)\n>\n> - [Open in StackBlitz](https://stackblitz.com/github/imgly/cesdk-web-examples/tree/v$UBQ_VERSION$/guides-use-templates-programmatic-server-js)\n\nTemplates are scenes with predefined structures that support dynamic content through variables. This guide shows you how to work with templates programmatically using CE.SDK's engine APIs—without requiring user interface interactions.\n\n```typescript file=@cesdk_web_examples/guides-use-templates-programmatic-server-js/server-js.ts reference-only\nimport CreativeEngine from '@cesdk/node';\nimport { writeFileSync } from 'fs';\nimport { config as loadEnv } from 'dotenv';\n\n// Load environment variables\nloadEnv();\n\n/**\n * Use Templates Programmatically (Server/Node.js)\n *\n * This example demonstrates headless template workflows:\n * 1. Creating templates from scratch with text variables\n * 2. Setting up text variables for dynamic content\n * 3. Batch processing: populating templates with data\n * 4. Exporting multiple personalized outputs\n */\n\nasync function run() {\n let engine;\n\n try {\n const config = {\n baseURL: process.env.IMGLY_LOCAL_ASSETS_URL,\n // license: process.env.CESDK_LICENSE,\n logger: (message: string, logLevel?: string) => {\n if (logLevel === 'ERROR' || logLevel === 'WARN') {\n console.log(`[${logLevel}]`, message);\n }\n }\n };\n\n engine = await CreativeEngine.init(config);\n console.log('✓ Engine initialized');\n\n // Create a greeting card template from scratch\n const scene = engine.scene.create();\n const page = engine.block.create('page');\n engine.block.appendChild(scene, page);\n\n // Set page dimensions\n engine.block.setWidth(page, 800);\n engine.block.setHeight(page, 600);\n\n // Set page background\n const pageFill = engine.block.getFill(page);\n engine.block.setColor(pageFill, 'fill/color/value', {\n r: 0.95,\n g: 0.95,\n b: 0.95,\n a: 1.0\n });\n\n // Set up text variables FIRST so they're available when text is created\n engine.variable.setString('recipientName', 'Template');\n engine.variable.setString('customMessage', 'This is a template example');\n\n // Add title text block with variable placeholder\n const titleBlock = engine.block.create('text');\n engine.block.setName(titleBlock, 'title');\n engine.block.appendChild(page, titleBlock);\n engine.block.setPositionX(titleBlock, 50);\n engine.block.setPositionY(titleBlock, 50);\n engine.block.setWidth(titleBlock, 700);\n engine.block.setHeight(titleBlock, 80);\n\n // Set text with variable syntax\n engine.block.replaceText(titleBlock, 'Hello, {{recipientName}}!');\n engine.block.setTextColor(titleBlock, {\n r: 0.2,\n g: 0.2,\n b: 0.2,\n a: 1.0\n });\n\n // Configure font size and weight\n engine.block.setFloat(titleBlock, 'text/fontSize', 48);\n engine.block.setBool(titleBlock, 'text/bold', true);\n\n // Add message text block with variable\n const messageBlock = engine.block.create('text');\n engine.block.setName(messageBlock, 'message');\n engine.block.appendChild(page, messageBlock);\n engine.block.setPositionX(messageBlock, 50);\n engine.block.setPositionY(messageBlock, 140);\n engine.block.setWidth(messageBlock, 700);\n engine.block.setHeight(messageBlock, 120);\n\n engine.block.replaceText(messageBlock, '{{customMessage}}');\n engine.block.setTextColor(messageBlock, {\n r: 0.3,\n g: 0.3,\n b: 0.3,\n a: 1.0\n });\n\n engine.block.setFloat(messageBlock, 'text/fontSize', 28);\n\n console.log('✓ Template structure created');\n\n // Variables have already been set earlier in the template creation\n // List all variables\n const allVariables = engine.variable.findAll();\n console.log('✓ Variables initialized:', allVariables);\n\n // Save the template for reuse\n const templateString = await engine.scene.saveToString();\n writeFileSync('template.scene', templateString);\n console.log('✓ Template saved to template.scene');\n\n // Demonstrate batch processing: populate template with multiple data records\n const recipients = [\n {\n name: 'Alice',\n message: 'Congratulations on your promotion!'\n },\n {\n name: 'Bob',\n message: 'Happy Birthday! Have a wonderful day!'\n },\n {\n name: 'Charlie',\n message: 'Thank you for your amazing work!'\n }\n ];\n\n console.log('\\n✓ Starting batch processing...');\n\n for (let i = 0; i < recipients.length; i++) {\n const recipient = recipients[i];\n\n // Populate template with recipient data\n engine.variable.setString('recipientName', recipient.name);\n engine.variable.setString('customMessage', recipient.message);\n\n // Export the personalized card\n const blob = await engine.block.export(page, {\n mimeType: 'image/png',\n targetWidth: 800,\n targetHeight: 600\n });\n\n const filename = `greeting-card-${recipient.name.toLowerCase()}.png`;\n const buffer = Buffer.from(await blob.arrayBuffer());\n writeFileSync(filename, buffer);\n console.log(` ✓ Exported ${filename}`);\n }\n\n console.log('\\n✓ Batch processing complete!');\n console.log(`✓ Generated ${recipients.length} personalized cards`);\n\n console.log('\\n✓ All operations completed successfully!');\n } catch (error) {\n console.error('Error:', error);\n process.exit(1);\n } finally {\n // Always dispose the engine\n engine?.dispose();\n console.log('\\n✓ Engine disposed');\n }\n}\n\n// Run the example\nrun();\n```\n\nThis guide covers creating templates from scratch, configuring text variables, populating templates with data, implementing batch processing workflows, and exporting personalized designs.\n\n## Initialize CE.SDK\n\nWe start by initializing CE.SDK and creating a design scene. This provides the foundation for programmatic template operations.\n\n```typescript highlight=highlight-setup\n const config = {\n baseURL: process.env.IMGLY_LOCAL_ASSETS_URL,\n // license: process.env.CESDK_LICENSE,\n logger: (message: string, logLevel?: string) => {\n if (logLevel === 'ERROR' || logLevel === 'WARN') {\n console.log(`[${logLevel}]`, message);\n }\n }\n };\n\n engine = await CreativeEngine.init(config);\n console.log('✓ Engine initialized');\n```\n\nWe create a page with specific dimensions and set a light gray background. This serves as the canvas for our template structure.\n\n## Creating Templates from Scratch\n\nWe build templates programmatically by creating and arranging blocks with `engine.block.create()` and `engine.block.appendChild()`.\n\n```typescript highlight=highlight-create-template\n // Create a greeting card template from scratch\n const scene = engine.scene.create();\n const page = engine.block.create('page');\n engine.block.appendChild(scene, page);\n\n // Set page dimensions\n engine.block.setWidth(page, 800);\n engine.block.setHeight(page, 600);\n\n // Set page background\n const pageFill = engine.block.getFill(page);\n engine.block.setColor(pageFill, 'fill/color/value', {\n r: 0.95,\n g: 0.95,\n b: 0.95,\n a: 1.0\n });\n\n // Set up text variables FIRST so they're available when text is created\n engine.variable.setString('recipientName', 'Template');\n engine.variable.setString('customMessage', 'This is a template example');\n\n // Add title text block with variable placeholder\n const titleBlock = engine.block.create('text');\n engine.block.setName(titleBlock, 'title');\n engine.block.appendChild(page, titleBlock);\n engine.block.setPositionX(titleBlock, 50);\n engine.block.setPositionY(titleBlock, 50);\n engine.block.setWidth(titleBlock, 700);\n engine.block.setHeight(titleBlock, 80);\n\n // Set text with variable syntax\n engine.block.replaceText(titleBlock, 'Hello, {{recipientName}}!');\n engine.block.setTextColor(titleBlock, {\n r: 0.2,\n g: 0.2,\n b: 0.2,\n a: 1.0\n });\n\n // Configure font size and weight\n engine.block.setFloat(titleBlock, 'text/fontSize', 48);\n engine.block.setBool(titleBlock, 'text/bold', true);\n\n // Add message text block with variable\n const messageBlock = engine.block.create('text');\n engine.block.setName(messageBlock, 'message');\n engine.block.appendChild(page, messageBlock);\n engine.block.setPositionX(messageBlock, 50);\n engine.block.setPositionY(messageBlock, 140);\n engine.block.setWidth(messageBlock, 700);\n engine.block.setHeight(messageBlock, 120);\n\n engine.block.replaceText(messageBlock, '{{customMessage}}');\n engine.block.setTextColor(messageBlock, {\n r: 0.3,\n g: 0.3,\n b: 0.3,\n a: 1.0\n });\n\n engine.block.setFloat(messageBlock, 'text/fontSize', 28);\n\n console.log('✓ Template structure created');\n```\n\nWe create a greeting card template with two text blocks. The title block contains `{{recipientName}}` and the message block contains `{{customMessage}}`—these double-brace syntax markers define where variables will be replaced. We position each block precisely and configure text properties like font size and color.\n\n## Text Variables for Dynamic Content\n\nVariables enable text replacement throughout templates. We set variable values with `engine.variable.setString()`, which automatically updates any text containing `{{variableName}}` syntax.\n\n```typescript highlight=highlight-manage-variables\n// Variables have already been set earlier in the template creation\n// List all variables\nconst allVariables = engine.variable.findAll();\nconsole.log('✓ Variables initialized:', allVariables);\n```\n\nWe initialize variables for `recipientName` and `customMessage`. The `engine.variable.getString()` method retrieves current values, and `engine.variable.findAll()` lists all variables in the scene. Variables persist with the scene and automatically update text content whenever changed.\n\n## Saving Templates for Reuse\n\nTemplates can be serialized for storage and reuse. We use `engine.scene.saveToString()` to create portable template files.\n\n```typescript highlight=highlight-save-template\n// Save the template for reuse\nconst templateString = await engine.scene.saveToString();\nwriteFileSync('template.scene', templateString);\nconsole.log('✓ Template saved to template.scene');\n```\n\nThe `saveToString()` method returns a base64-encoded string containing the complete scene. This string can be stored in databases or file systems for later use in batch processing workflows.\n\n## Batch Processing with Templates\n\nWe demonstrate batch processing by populating the template with multiple data records and exporting personalized outputs.\n\n```typescript highlight=highlight-batch-processing\n // Demonstrate batch processing: populate template with multiple data records\n const recipients = [\n {\n name: 'Alice',\n message: 'Congratulations on your promotion!'\n },\n {\n name: 'Bob',\n message: 'Happy Birthday! Have a wonderful day!'\n },\n {\n name: 'Charlie',\n message: 'Thank you for your amazing work!'\n }\n ];\n\n console.log('\\n✓ Starting batch processing...');\n\n for (let i = 0; i < recipients.length; i++) {\n const recipient = recipients[i];\n\n // Populate template with recipient data\n engine.variable.setString('recipientName', recipient.name);\n engine.variable.setString('customMessage', recipient.message);\n\n // Export the personalized card\n const blob = await engine.block.export(page, {\n mimeType: 'image/png',\n targetWidth: 800,\n targetHeight: 600\n });\n\n const filename = `greeting-card-${recipient.name.toLowerCase()}.png`;\n const buffer = Buffer.from(await blob.arrayBuffer());\n writeFileSync(filename, buffer);\n console.log(` ✓ Exported ${filename}`);\n }\n\n console.log('\\n✓ Batch processing complete!');\n console.log(`✓ Generated ${recipients.length} personalized cards`);\n```\n\nThe batch processing pattern loads a template once, then iterates through data records. For each record, we update variables with specific values, then export the result. This efficient approach generates multiple personalized designs from a single template. Each export operation renders the current state to a PNG file saved to disk.\n\n## Data-Driven Workflows\n\nBatch processing combines template creation, data population, and export operations. A common pattern loads a template once, then iterates through data records:\n\n```typescript\nconst templateString = await engine.scene.saveToString();\n\nfor (const record of dataRecords) {\n await engine.scene.loadFromString(templateString);\n engine.variable.setString('name', record.name);\n engine.variable.setString('title', record.title);\n\n const page = engine.block.findByType('page')[0];\n const buffer = await engine.block.export(page, 'image/png');\n writeFileSync(`output-${record.id}.png`, Buffer.from(buffer));\n}\n```\n\nThis pattern works for generating personalized certificates, greeting cards, social media graphics, or any scenario requiring multiple customized outputs from a single template.\n\n## Loading Existing Templates\n\nTemplates can be loaded from various sources. Use `engine.scene.loadFromURL()` to fetch remote templates:\n\n```typescript\nawait engine.scene.loadFromURL(\n 'https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_postcard_1.scene'\n);\n```\n\nFor templates with embedded assets, `engine.scene.loadFromArchiveURL()` loads the complete package including all resources.\n\nThe `engine.scene.applyTemplateFromString()` and `engine.scene.applyTemplateFromURL()` methods merge template content into existing scenes without replacing everything—useful for adding template sections to ongoing designs.\n\n## Populating Template Content\n\nWe populate templates by updating variables with new data. When variables are updated, all text blocks containing those variable references automatically display the new values. This approach enables efficient template population without manually finding and updating individual blocks.\n\n## Managing Variables\n\nVariables integrate with text blocks automatically. When you set a variable value, CE.SDK updates all text containing that variable immediately:\n\n```typescript\nengine.variable.setString('userName', 'Alice');\n// All text with {{userName}} now displays 'Alice'\n\nengine.variable.setString('userName', 'Bob');\n// Same text now displays 'Bob'\n```\n\nRemove variables with `engine.variable.remove()` when they're no longer needed. This doesn't affect existing text—the variable syntax remains as literal text.\n\n## API Reference\n\n| Method | Description |\n| --- | --- |\n| `engine.block.create()` | Create a new design block |\n| `engine.block.appendChild()` | Add a block to the scene hierarchy |\n| `engine.block.setPositionX()` | Set block horizontal position |\n| `engine.block.setPositionY()` | Set block vertical position |\n| `engine.block.setWidth()` | Set block width |\n| `engine.block.setHeight()` | Set block height |\n| `engine.block.replaceText()` | Set text content (supports variable tokens) |\n| `engine.variable.setString()` | Create or update a text variable |\n| `engine.variable.getString()` | Read the current value of a variable |\n| `engine.variable.findAll()` | Get array of all variable keys in the scene |\n| `engine.variable.remove()` | Delete a variable from the scene |\n| `engine.scene.saveToString()` | Serialize scene to portable string |\n| `engine.scene.loadFromString()` | Load scene from serialized string |\n| `engine.scene.loadFromURL()` | Load scene from remote URL |\n| `engine.block.export()` | Export block to image blob |\n\n## Troubleshooting\n\n**Template loading failures:** Verify scene strings are properly encoded and URLs are accessible. Use `try-catch` blocks around loading operations to handle network or parsing errors.\n\n**Variables not replacing text:** Variable names in text (within `{{}}`) must exactly match keys passed to `engine.variable.setString()`. Variables are case-sensitive.\n\n**Export issues:** Validate that all required assets are accessible before exporting. Missing images or fonts can cause export failures. Check block hierarchy structure—orphaned blocks (not connected to the page tree) won't appear in exports.\n\n\n\n---\n\n## More Resources\n\n- **[Node.js Documentation Index](https://img.ly/docs/cesdk/node.md)** - Browse all Node.js documentation\n- **[Complete Documentation](./llms-full.txt.md)** - Full documentation in one file (for LLMs)\n- **[Web Documentation](./node.md)** - Interactive documentation with examples\n- **[Support](mailto:support@img.ly)** - Contact IMG.LY support",
573
573
  "docs/guide/use-templates/replace-content.md": "> This is one page of the CE.SDK Node.js documentation. For a complete overview, see the [Node.js Documentation Index](https://img.ly/docs/cesdk/node.md). For all docs in one file, see [llms-full.txt](./llms-full.txt.md).\n\n**Navigation:** [Guides](./guides.md) > [Create and Use Templates](./create-templates.md) > [Replace Content](./use-templates/replace-content.md)\n\n---\n\nDynamically replace content within templates using CE.SDK's placeholder and variable systems in server-side Node.js applications. Find placeholder blocks by name, update text using variables, and swap images programmatically.\n\n> **Reading time:** 10 minutes\n>\n> **Resources:**\n>\n> - [Download examples](https://github.com/imgly/cesdk-web-examples/archive/refs/tags/release-$UBQ_VERSION$.zip)\n>\n> - [View source on GitHub](https://github.com/imgly/cesdk-web-examples/tree/release-$UBQ_VERSION$/guides-use-templates-replace-content-server-js)\n>\n> - [Open in StackBlitz](https://stackblitz.com/github/imgly/cesdk-web-examples/tree/v$UBQ_VERSION$/guides-use-templates-replace-content-server-js)\n\nTemplate content replacement enables dynamic designs by swapping placeholder content programmatically. Templates contain blocks marked as placeholders that can be located by name or discovered in bulk for batch processing. Text replacement uses the variable system with `{{variableName}}` syntax, while images are updated by modifying fill properties.\n\n```typescript file=@cesdk_web_examples/guides-use-templates-replace-content-server-js/server-js.ts reference-only\nimport CreativeEngine from '@cesdk/node';\nimport { writeFileSync, mkdirSync, existsSync } from 'fs';\nimport { config } from 'dotenv';\n\n// Load environment variables\nconfig();\n\n/**\n * CE.SDK Server Guide: Replace Content\n *\n * Demonstrates how to dynamically replace content in templates:\n * - Finding placeholder blocks by name\n * - Using text variables for dynamic content\n * - Replacing image sources\n * - Building data-driven template workflows\n */\nasync function main() {\n // Initialize the headless Creative Engine\n const engine = await CreativeEngine.init({\n baseURL: process.env.IMGLY_LOCAL_ASSETS_URL\n // license: process.env.CESDK_LICENSE\n });\n\n try {\n // Create a scene with a page\n engine.scene.create('VerticalStack', {\n page: { size: { width: 800, height: 600 } }\n });\n\n const page = engine.block.findByType('page')[0];\n\n // Create a text block with a name for later retrieval\n const headerText = engine.block.create('text');\n engine.block.setName(headerText, 'header-text');\n engine.block.replaceText(headerText, 'Welcome, {{userName}}!');\n engine.block.setTextFontSize(headerText, 96);\n engine.block.setWidthMode(headerText, 'Auto');\n engine.block.setHeightMode(headerText, 'Auto');\n engine.block.appendChild(page, headerText);\n engine.block.setPositionX(headerText, 50);\n engine.block.setPositionY(headerText, 30);\n\n // Find the block by its name\n const [foundHeader] = engine.block.findByName('header-text');\n console.log('Found header block:', foundHeader);\n\n // Enable placeholder behavior on blocks\n engine.block.setPlaceholderEnabled(headerText, true);\n\n // Create an image placeholder\n const imageBlock = await engine.block.addImage(\n 'https://img.ly/static/ubq_samples/sample_1.jpg',\n { size: { width: 300, height: 200 } }\n );\n engine.block.appendChild(page, imageBlock);\n engine.block.setPositionX(imageBlock, 50);\n engine.block.setPositionY(imageBlock, 120);\n engine.block.setName(imageBlock, 'product-image');\n engine.block.setPlaceholderEnabled(imageBlock, true);\n\n // Find all placeholder blocks in the scene\n const placeholders = engine.block.findAllPlaceholders();\n console.log('Found placeholders:', placeholders.length);\n\n // Check if a block supports placeholder behavior\n const supportsPlaceholder =\n engine.block.supportsPlaceholderBehavior(imageBlock);\n console.log('Supports placeholder behavior:', supportsPlaceholder);\n\n // Check if placeholder is enabled\n const isPlaceholderEnabled = engine.block.isPlaceholderEnabled(imageBlock);\n console.log('Placeholder enabled:', isPlaceholderEnabled);\n\n // Set text variables to replace {{variableName}} placeholders\n engine.variable.setString('userName', 'Alex');\n\n // The text block now displays \"Welcome, Alex!\"\n console.log('Variable set, text updated automatically');\n\n // List all variables in the scene\n const allVariables = engine.variable.findAll();\n console.log('All variables:', allVariables);\n\n // Get a variable value\n const userName = engine.variable.getString('userName');\n console.log('Current userName:', userName);\n\n // Update the variable\n engine.variable.setString('userName', 'Jordan');\n\n // Replace image content by updating the fill's image URI\n const [productImage] = engine.block.findByName('product-image');\n const fill = engine.block.getFill(productImage);\n engine.block.setString(\n fill,\n 'fill/image/imageFileURI',\n 'https://img.ly/static/ubq_samples/sample_2.jpg'\n );\n console.log('Image replaced');\n\n // Create another text block for direct replacement\n const subtitleText = engine.block.create('text');\n engine.block.setName(subtitleText, 'subtitle');\n engine.block.replaceText(subtitleText, 'Original subtitle text');\n engine.block.setTextFontSize(subtitleText, 48);\n engine.block.setWidthMode(subtitleText, 'Auto');\n engine.block.setHeightMode(subtitleText, 'Auto');\n engine.block.appendChild(page, subtitleText);\n engine.block.setPositionX(subtitleText, 50);\n engine.block.setPositionY(subtitleText, 350);\n\n // Replace text directly without using variables\n const [subtitle] = engine.block.findByName('subtitle');\n engine.block.replaceText(subtitle, 'Updated subtitle content');\n console.log('Text replaced directly');\n\n // Demonstrate data-driven template workflow pattern\n const dataRecords = [\n { name: 'Alice', title: 'Designer' },\n { name: 'Bob', title: 'Developer' }\n ];\n\n // Process each record (in practice, you'd export between iterations)\n for (const record of dataRecords) {\n engine.variable.setString('userName', record.name);\n console.log(`Processed record for: ${record.name}`);\n // In a real workflow, you would export here:\n // const blob = await engine.block.export(page, { mimeType: 'image/png' });\n }\n\n // Export the final result\n const outputDir = './output';\n if (!existsSync(outputDir)) {\n mkdirSync(outputDir, { recursive: true });\n }\n\n const blob = await engine.block.export(page, { mimeType: 'image/png' });\n const buffer = Buffer.from(await blob.arrayBuffer());\n writeFileSync(`${outputDir}/replaced-content.png`, buffer);\n\n console.log('Exported result to output/replaced-content.png');\n } finally {\n // Always dispose of the engine to free resources\n engine.dispose();\n }\n}\n\nmain().catch(console.error);\n```\n\nThis guide covers how to find placeholder blocks, replace text using variables, swap image content, and build data-driven template workflows in server-side applications.\n\n## Initialize the Engine\n\nWe start by initializing the headless Creative Engine with a scene and page for our template processing workflow.\n\n```typescript highlight=highlight-setup\n // Initialize the headless Creative Engine\n const engine = await CreativeEngine.init({\n baseURL: process.env.IMGLY_LOCAL_ASSETS_URL\n // license: process.env.CESDK_LICENSE\n });\n\n try {\n // Create a scene with a page\n engine.scene.create('VerticalStack', {\n page: { size: { width: 800, height: 600 } }\n });\n\n const page = engine.block.findByType('page')[0];\n```\n\n## Finding Placeholder Blocks\n\nLocate replaceable content using block discovery APIs. Use `engine.block.findByName()` to find specific blocks when you know the placeholder name.\n\n```typescript highlight=highlight-find-by-name\n // Create a text block with a name for later retrieval\n const headerText = engine.block.create('text');\n engine.block.setName(headerText, 'header-text');\n engine.block.replaceText(headerText, 'Welcome, {{userName}}!');\n engine.block.setTextFontSize(headerText, 96);\n engine.block.setWidthMode(headerText, 'Auto');\n engine.block.setHeightMode(headerText, 'Auto');\n engine.block.appendChild(page, headerText);\n engine.block.setPositionX(headerText, 50);\n engine.block.setPositionY(headerText, 30);\n\n // Find the block by its name\n const [foundHeader] = engine.block.findByName('header-text');\n console.log('Found header block:', foundHeader);\n```\n\n### Discover All Placeholders\n\nUse `engine.block.findAllPlaceholders()` to discover all placeholder blocks in a template for iterating through them programmatically.\n\n```typescript highlight=highlight-find-all-placeholders\n // Enable placeholder behavior on blocks\n engine.block.setPlaceholderEnabled(headerText, true);\n\n // Create an image placeholder\n const imageBlock = await engine.block.addImage(\n 'https://img.ly/static/ubq_samples/sample_1.jpg',\n { size: { width: 300, height: 200 } }\n );\n engine.block.appendChild(page, imageBlock);\n engine.block.setPositionX(imageBlock, 50);\n engine.block.setPositionY(imageBlock, 120);\n engine.block.setName(imageBlock, 'product-image');\n engine.block.setPlaceholderEnabled(imageBlock, true);\n\n // Find all placeholder blocks in the scene\n const placeholders = engine.block.findAllPlaceholders();\n console.log('Found placeholders:', placeholders.length);\n```\n\n### Query Placeholder State\n\nVerify blocks support replacement with `engine.block.isPlaceholderEnabled()` and `engine.block.supportsPlaceholderBehavior()` before attempting content updates.\n\n```typescript highlight=highlight-query-placeholder-state\n // Check if a block supports placeholder behavior\n const supportsPlaceholder =\n engine.block.supportsPlaceholderBehavior(imageBlock);\n console.log('Supports placeholder behavior:', supportsPlaceholder);\n\n // Check if placeholder is enabled\n const isPlaceholderEnabled = engine.block.isPlaceholderEnabled(imageBlock);\n console.log('Placeholder enabled:', isPlaceholderEnabled);\n```\n\n## Text Variable Replacement\n\nReplace text content dynamically using CE.SDK's variable system. Text blocks containing `{{variableName}}` syntax automatically update when you set variable values with `engine.variable.setString()`.\n\n```typescript highlight=highlight-text-variables\n // Set text variables to replace {{variableName}} placeholders\n engine.variable.setString('userName', 'Alex');\n\n // The text block now displays \"Welcome, Alex!\"\n console.log('Variable set, text updated automatically');\n```\n\n### Managing Variables\n\nList all variables with `engine.variable.findAll()`, retrieve current values using `engine.variable.getString()`, and update variables as needed.\n\n```typescript highlight=highlight-manage-variables\n // List all variables in the scene\n const allVariables = engine.variable.findAll();\n console.log('All variables:', allVariables);\n\n // Get a variable value\n const userName = engine.variable.getString('userName');\n console.log('Current userName:', userName);\n\n // Update the variable\n engine.variable.setString('userName', 'Jordan');\n```\n\n## Replacing Image Content\n\nUpdate image placeholders by modifying the fill's image URI. First get the fill block using `engine.block.getFill()`, then set the new image source with `engine.block.setString()` on the `'fill/image/imageFileURI'` property.\n\n```typescript highlight=highlight-replace-image\n// Replace image content by updating the fill's image URI\nconst [productImage] = engine.block.findByName('product-image');\nconst fill = engine.block.getFill(productImage);\nengine.block.setString(\n fill,\n 'fill/image/imageFileURI',\n 'https://img.ly/static/ubq_samples/sample_2.jpg'\n);\nconsole.log('Image replaced');\n```\n\n## Direct Text Replacement\n\nReplace text content directly without variables using `engine.block.replaceText()`. This method replaces all text in a block when you need precise control without the variable system.\n\n```typescript highlight=highlight-direct-text-replacement\n // Create another text block for direct replacement\n const subtitleText = engine.block.create('text');\n engine.block.setName(subtitleText, 'subtitle');\n engine.block.replaceText(subtitleText, 'Original subtitle text');\n engine.block.setTextFontSize(subtitleText, 48);\n engine.block.setWidthMode(subtitleText, 'Auto');\n engine.block.setHeightMode(subtitleText, 'Auto');\n engine.block.appendChild(page, subtitleText);\n engine.block.setPositionX(subtitleText, 50);\n engine.block.setPositionY(subtitleText, 350);\n\n // Replace text directly without using variables\n const [subtitle] = engine.block.findByName('subtitle');\n engine.block.replaceText(subtitle, 'Updated subtitle content');\n console.log('Text replaced directly');\n```\n\n## Data-Driven Template Workflows\n\nBuild automated template population by iterating through data records. Load the template once, then loop through your data, updating variables and placeholders for each record before exporting.\n\n```typescript highlight=highlight-data-driven\n // Demonstrate data-driven template workflow pattern\n const dataRecords = [\n { name: 'Alice', title: 'Designer' },\n { name: 'Bob', title: 'Developer' }\n ];\n\n // Process each record (in practice, you'd export between iterations)\n for (const record of dataRecords) {\n engine.variable.setString('userName', record.name);\n console.log(`Processed record for: ${record.name}`);\n // In a real workflow, you would export here:\n // const blob = await engine.block.export(page, { mimeType: 'image/png' });\n }\n```\n\n## Export and Cleanup\n\nAfter processing the template, we export the result to a file and dispose of the engine to free resources.\n\n```typescript highlight=highlight-export\n // Export the final result\n const outputDir = './output';\n if (!existsSync(outputDir)) {\n mkdirSync(outputDir, { recursive: true });\n }\n\n const blob = await engine.block.export(page, { mimeType: 'image/png' });\n const buffer = Buffer.from(await blob.arrayBuffer());\n writeFileSync(`${outputDir}/replaced-content.png`, buffer);\n\n console.log('Exported result to output/replaced-content.png');\n```\n\nAlways dispose of the engine in a finally block to ensure resources are freed even if an error occurs.\n\n```typescript highlight=highlight-cleanup\n// Always dispose of the engine to free resources\nengine.dispose();\n```\n\n## Troubleshooting\n\n### Block Not Found by Name\n\nVerify the exact name string matches what's set in the template. Names are case-sensitive. Use `engine.block.getName()` to inspect existing block names.\n\n### Variable Not Replacing Text\n\nEnsure the variable syntax `{{variableName}}` in the text block matches the key used in `engine.variable.setString()` exactly, including casing.\n\n### Image Not Updating\n\nConfirm the block has an image fill by checking `engine.block.getFill()` returns a valid fill block. Verify the URI is accessible and properly formatted.\n\n### Placeholder State Queries Return False\n\nNot all blocks support placeholder behavior. Use `engine.block.supportsPlaceholderBehavior()` to check compatibility before querying enabled state.\n\n## API Reference\n\n| Method | Description |\n|--------|-------------|\n| `block.findByName(name)` | Find blocks by name identifier |\n| `block.getName(block)` | Get the name of a block |\n| `block.findAllPlaceholders()` | Discover all placeholder blocks in scene |\n| `block.isPlaceholderEnabled(block)` | Check if placeholder functionality is enabled |\n| `block.supportsPlaceholderBehavior(block)` | Verify block supports placeholder behavior |\n| `block.getFill(block)` | Get fill block from a graphic block |\n| `block.setString(block, property, value)` | Set string properties like image URIs |\n| `block.replaceText(block, text)` | Replace text content directly |\n| `variable.setString(name, value)` | Set text variable value for dynamic replacement |\n| `variable.getString(name)` | Get current variable value |\n| `variable.findAll()` | List all variable names in scene |\n\n\n\n---\n\n## More Resources\n\n- **[Node.js Documentation Index](https://img.ly/docs/cesdk/node.md)** - Browse all Node.js documentation\n- **[Complete Documentation](./llms-full.txt.md)** - Full documentation in one file (for LLMs)\n- **[Web Documentation](./node.md)** - Interactive documentation with examples\n- **[Support](mailto:support@img.ly)** - Contact IMG.LY support",
574
574
  "docs/guide/what-is-cesdk.md": "> This is one page of the CE.SDK Node.js documentation. For a complete overview, see the [Node.js Documentation Index](https://img.ly/docs/cesdk/node.md). For all docs in one file, see [llms-full.txt](./llms-full.txt.md).\n\n**Navigation:** [Get Started](./get-started/overview.md) > [What is CE.SDK?](./what-is-cesdk.md)\n\n---\n\n### What is CE.SDK?\n\n**CreativeEditor SDK (CE.SDK)** on Node.js is a powerful design engine that brings fully automated image and design editing directly into your Node app. Whether you're enabling AI-powered design workflows, template-based creation, or dynamic content generation, CE.SDK offers the flexibility, performance, and developer control you need — all with minimal integration overhead.\n\n[Get Started](./get-started/overview.md)\n\nTrusted by leading organizations worldwide, CE.SDK powers the creative editors used in best-in-class applications, including those from Shopify, Semrush, HP, Shutterfly, Ticketmaster, and Swiss Post.\n\n## Key Capabilities of the Node.js Creative Editor SDK\n\n<CapabilityGrid\n features={[\n {\n title: 'Transform',\n description:\n 'Perform operations like cropping, rotating, and resizing design elements.',\n imageId: 'transform',\n },\n {\n title: 'Templating',\n description:\n 'Create and apply design templates with placeholders and text variables for dynamic content.',\n imageId: 'templating',\n },\n {\n title: 'Placeholders & Lockable Design',\n description:\n 'Constrain templates to guide your users’ design and ensure brand consistency.',\n imageId: 'placeholders',\n },\n {\n title: 'Asset Management',\n description:\n 'Import and manage images, shapes, and other assets to build your designs.',\n imageId: 'asset-libraries',\n },\n {\n title: 'Design Collage',\n description:\n 'Arrange multiple elements on a single canvas to create complex layouts.',\n imageId: 'video-collage',\n },\n {\n title: 'Text Editing',\n description:\n 'Add and style text blocks with various fonts, colors, and effects.',\n imageId: 'text-editing',\n },\n {\n title: 'Background Removal',\n description:\n 'This plugin makes it easy to remove the background from images running entirely in the browser.',\n imageId: 'green-screen',\n },\n {\n title: 'Extendible',\n description:\n 'Hook into the engine API and editor events to implement custom features.',\n imageId: 'extendible',\n },\n {\n title: 'Headless & Automation',\n description:\n 'Programmatically edit designs within your React application using the engine API.',\n imageId: 'headless',\n },\n]}\n/>\n\n## File Format Support\n\nCE.SDK supports a wide range of file types to ensure maximum flexibility for developers:\n\n### Importing Media\n\n### Exporting Media\n\n### Importing Templates\n\nFor detailed information, see the [full file format support list](./file-format-support.md).\n\n## Integrations\n\nCE.SDK supports out-of-the-box integrations with:\n\n- **Getty Images**\n- **Unsplash**\n- **Pexels**\n- **Soundstripe**\n\nWant to connect your own asset sources? Register a custom provider using our API.\n\n\n\n---\n\n## More Resources\n\n- **[Node.js Documentation Index](https://img.ly/docs/cesdk/node.md)** - Browse all Node.js documentation\n- **[Complete Documentation](./llms-full.txt.md)** - Full documentation in one file (for LLMs)\n- **[Web Documentation](./node.md)** - Interactive documentation with examples\n- **[Support](mailto:support@img.ly)** - Contact IMG.LY support",
575
- "docs/handbook/SKILL.md": "---\nname: handbook\ndescription: |\n The CoDesign design loop, tool contract, workspace cheatsheet, typeface\n schema, common operations cheatsheet, and the full design-quality rubric.\n **Read this skill before any edit call** — it contains the 5-step Loop\n (decide parent → read API → exec → preview → judge), the tool roster and\n cross-tool rules, the workspace revision model (list / history / rename),\n rewind-recovery procedure, the typeface object shape required by setFont,\n and §6: the design-rules index — the full enforceable rubric (typography,\n hierarchy, composition, craft) ships as this skill's design-rules.md, and\n the runtime-quirk reference as its quirks.md, both read on demand.\n\n Triggered for any task using mcp__codesign__edit or\n mcp__codesign__preview.\n---\n\n## 1. Loop\n\nYou MUST follow these steps in order for ANY canvas change:\n\n1. **Decide where you're editing from — based on what the user asked for, not what's in the workspace.** Either:\n - **New design requested** (the default in a fresh conversation) → pass `parent: null` and supply a `title` (the design name). The workspace persists across all past conversations — designs in it are not yours to continue unless the user points at them. For a new design from a vague prompt, run the `create` skill's intake first to assemble the brief.\n - **Continuing this conversation's design** → use the `revision` id from your last `edit` response as `parent`.\n - **User explicitly asked to resume earlier work** (\"keep working on yesterday's flyer\") → call `list` to see designs newest-first, then `history({ revision })` on the design they meant; pass the leaf as `parent`. Never do this unprompted.\n2. **Read the API docs** for every engine method you plan to use. Use `skill({ name: 'api' })` for signatures, `skill({ name: 'guide' })` for prose explanations and recipes. NEVER guess parameters.\n3. **Run `edit({ parent, code, note? })`** — execute the JS body. The response's first text part is JSON `{ revision, parent }` — save `revision`; it's your `parent` for the next edit.\n4. **Visually verify.** Call `preview({ blockId: <pageId>, revision: <revision> })`. Both `blockId` and `revision` are REQUIRED. Never tell the user something \"looks good\" without seeing it yourself.\n\n5. **Judge & remediate before reporting done.** When the design is a candidate for \"done\", run the build → judge → remediate loop from the `judge` skill: score all 7 design axes against your actual `preview` renders, dry-run the applicable §6 Pass checklists (§6.1.10, §6.5.2 — in this skill's design-rules.md), and remediate any applicable axis below 8 via further `edit`s — then re-preview and re-judge. Never report an unjudged design as done.\n\nSteps 1, 2, 4, and 5 are NOT optional. Step 3's `note` is encouraged: a one-liner describing what you accomplished, so you can re-ground later.\n\n### Re-grounding after a rewind / restart\n\nIf your conversation was rewound (double-ESC in Claude Code), or your process restarted mid-conversation, your in-memory \"current revision\" is wrong. The engine's state is the truth, but the server can't tell you've been rewound. Recovery:\n\n1. Call `list` — see the workspace's designs and their `latestLeaf` revisions.\n2. Call `history({ revision: latestLeaf })` — read the notes to find where the user wanted to be.\n3. Pass that revision id as `parent` on your next `edit`. The server reloads the engine to that scene before running your code — your edits land on top of the right state.\n\nRe-grounding recovers the design this conversation was already working on. It is not for starting a new conversation — a fresh session with a new request starts at `parent: null`.\n\nEditing on top of an OLDER revision (not the latest leaf) is fine: it creates a **non-destructive fork**. The previous leaves stay in storage; you can switch back any time by passing one of their ids as `parent`.\n\n## 2. Tools\n\nEach tool's full contract lives on the tool itself — its description in tools/list is authoritative and always in front of you. This section is only the roster plus the rules that span tools.\n\n- `edit({ parent, code, title?, note? })` — THE mutating tool: runs async JS (`engine` in scope) and commits a revision. The response's first text part is JSON `{ revision, parent }` — save `revision`; it is your `parent` for the next edit.\n- `preview({ blockId, revision })` — render a page/block to an inline PNG; both args required.\n- `export({ target, format, revision, blockId })` — write a pdf/image artifact; returns `{ uri, httpUrl?, path?, bytes, format, revision }`. `uri` is the durable handle; `httpUrl` is a fetch link tied to this process — never persist it.\n<!-- feature:video -->\n- `export` also accepts `target=video` (with `format: \"mp4\"`) — renders the block's authored timeline (see the video recipe in §4); needs the native engine (the default while video is enabled).\n<!-- /feature:video -->\n- `save({ revision, outPath? })` — portable `.zip` archive of a revision, for taking an editable copy OUT. Every `edit` already auto-persists; `save` is never needed for durability.\n- `load({ source, title? })` — ingest a design as a NEW root: native `.scene` / `.zip`, or foreign Photoshop `.psd` / InDesign `.idml` / PowerPoint `.pptx` / PDF (translated on the way in; the returned `warnings` are your touch-up list — preview, then repair with `edit`). Continue with `edit({ parent: revision })`.\n- `import({ source: { path } })` — bring a local file into the workspace; embed the returned `workspace://` uri in `edit` code, never `httpUrl`.\n- `asset_search({ sourceId?, query?, page?, perPage? })` — discover and search the engine's asset sources; apply results inside `edit` (the exact patterns are in the tool's description).\n- `list()` / `history({ revision })` / `inspect({ revision })` / `rename({ revision, title })` — the workspace tools; usage in §3.\n- `view({ revision, pin? })` — human-facing viewer URL. Print it once; the viewer keeps it live.\n\nCross-tool rules:\n\n- **Revision ids are durable, block ids are not.** Everything flows through revisions: `edit` produces one, `preview`/`export`/`view` consume one, `history`/`inspect` walk them. Block ids are session-scoped — re-discover blocks in the current engine; never persist or replay literal block ids (see `inspect`'s description).\n- **The single mutating tool is `edit`.** `import`, `asset_search`, and `view` never change the scene; placement and application always happen inside `edit` code.\n\n## 3. Workspace cheatsheet\n\nThe workspace is your durable memory: every revision you commit via `edit` is there, addressable by id, walkable via `history`. It survives a process restart and a Claude rewind.\n\n```\nlist() → [{ rootRevision, title, latestLeaf, updatedAt }, …]\nhistory({ revision: leaf }) → [{ revision, parent, note?, createdAt }, …] (root first)\ninspect({ revision }) → { revision, parent, note?, createdAt, title?, code? }\nedit({ parent: leaf, code }) → linear edit, no engine reload\nedit({ parent: olderRev, code }) → fork; engine reloads to olderRev first\nedit({ parent: null, title }) → new design\nrename({ revision: any, title }) → updates the root title\n```\n\nA revision id is a 12-hex-char string. You never construct it; the server hands it back from `edit`. Treat it as opaque.\n\n## 4. Engine recipes\n\n### Build ONE complete page per `edit`\n\nThe unit of work is the **page**: every `edit` call emits one complete page — all its blocks, all setFont, all sizes, the `forceLoadResources` await, then positioning. Do **not** split a single page's build across 2–3 calls. Each tool call costs 20–200s of LLM streaming wall time; one ~10000-char call is faster than three ~3000-char calls because there's no thinking-between-calls overhead.\n\nFor a **single-page design** that means the entire scene in one `edit` — scene, page, all blocks:\n\n```js\n// 1. Structure\nconst scene = engine.scene.create('VerticalStack');\nconst stack = engine.block.findByType('stack')[0];\nconst page = engine.block.create('page');\nengine.block.appendChild(stack, page);\nengine.block.setWidth(page, 720);\nengine.block.setHeight(page, 1008);\n\n// 2. Create all blocks + setFont + setString (fire-and-forget loads)\nconst titleText = engine.block.create('text');\nengine.block.setFont(titleText, URI_PLAYFAIR, TF_PLAYFAIR);\nengine.block.setString(titleText, 'text/text', 'Alex & Jordan');\nengine.block.setTextFontSize(titleText, 48, { unit: 'Pixel' });\nengine.block.setWidthMode(titleText, 'Auto');\nengine.block.setHeightMode(titleText, 'Auto');\nengine.block.appendChild(page, titleText);\n// ... repeat for every text + graphic block ...\n\n// 3. Deterministic font-load await\nawait engine.block.forceLoadResources([page]);\n\n// 4. Now position with measured frame dims\nconst w = engine.block.getFrameWidth(titleText);\nengine.block.setPositionX(titleText, (720 - w) / 2);\nengine.block.setPositionY(titleText, 250);\n// ... position everything ...\n\nreturn { type: 'text', text: `page=${page} ready` };\n```\n\nAfter this one call, your next tool call is `preview({ blockId: page, revision })`. **Exactly 2 calls per page — build, then capture. For a single-page design that's the whole job. Then STOP.**\n\n### Multi-page designs: page by page, page 1 first\n\nNever emit a multi-page design (carousel, slide deck, brochure) in one giant `edit`. Build it **page by page**, one `edit` + one `preview` per page:\n\n1. **Page 1 is the design-language gate.** The first `edit` builds the scene and page 1 only — it establishes fonts, palette, margins, grid. Preview it before touching page 2: a wrong aesthetic choice caught here costs one page, not N.\n2. **Pages 2..N each get their own `edit` + `preview`.** Each call builds one complete page reusing the design language page 1 proved out. Preview each page right after building it — the bugs on later pages are content-dependent (a headline that overflows, an image that crops badly), so batching pages doesn't avoid them, it just delays discovery. Feed findings forward: if body text at 28px overflowed on page 3, use 26px from page 4 on.\n3. **Batching is allowed only for near-identical clones** — templated pages differing by a short label or image swap. Even then, a thrown error mid-call loses every page in the batch; when in doubt, one page per call.\n\nThe objective-bug bar below applies per page: fix an overflow on page 3 with a targeted `edit` to page 3, then re-capture page 3 only.\n\n**Editing an existing multi-page design:** a mechanical change applied uniformly (swap a font everywhere, recolor a brand accent) is one `edit` looping over `engine.scene.getPages()` — that's one logical change, not a batch. Substantial per-page rework (rewriting layouts, restructuring content) goes page by page like a build, with a `preview` per reworked page.\n\nAfter the capture, fire a 2nd `edit` ONLY if the rendered PNG shows a specific, OBJECTIVE bug (text overflowing the page bounds, blocks visibly overlapping, missing characters, blank glyphs). Aesthetic refinement does NOT warrant another exec — small sub-pixel margin tweaks, \"could be a touch tighter\", \"I'd nudge this 4px\", etc. are all DISALLOWED. If in doubt, STOP. The cost of a needless 3rd or 4th call (~30s wall + thinking tokens + latency) far outweighs any aesthetic gain.\n\nCompositional balance, vertical centering, and whitespace distribution are AESTHETIC, not objective bugs — even if the imbalance is large or intentional. \"Top-heavy\", \"feels off-center\", \"bottom third looks empty\", \"needs to be re-centered\" are all aesthetic; they do NOT warrant another exec. A read-only `edit` (one that returns data and mutates nothing — `getTextFontSizes`, `getFrameWidth`, etc.) still counts as a call against the limit, plus it forces a follow-up capture. Only fire a read-only exec if you'd otherwise be guessing at a number you can't read off the captured PNG.\n\n### Font sizes are PIXELS, not points\n\nCE.SDK's font size APIs default to **points**. CE.SDK's scene unit defaults to **Inch** out of the box — but this demo wraps `engine.scene.create(...)` to call `setDesignUnit(\"Pixel\", sceneId)` immediately, so every scene the agent creates is in pixels by default. You don't need to set the unit yourself, but if you ever destroy + bare-call into the engine outside `scene.create`, re-set it. Mixing point font sizes against the pixel scene unit silently scales text ~2.5× too large.\n\n**ALWAYS pass `{ unit: 'Pixel' }`** when reading or writing font sizes:\n\n```js\nengine.block.setTextFontSize(block, 32, { unit: 'Pixel' }); // 32 px ✓\nengine.block.getTextFontSizes(block, { unit: 'Pixel' }); // returns px ✓\n```\n\n**NEVER:**\n\n```js\nengine.block.setFloat(block, 'text/fontSize', 32); // unit-less, always points ✗\nengine.block.setTextFontSize(block, 32); // defaults to points (~43 px) ✗\n```\n\nLine-height is NOT a raw multiplier either: CE.SDK's `text/lineHeight` scales the font's content area, not the em — every visual line-height target must be divided by the font's content area (measured via `engine.editor.getFontMetrics`) before being set, and a reshape (`setString`) afterwards wipes per-character style ranges, so the order is line-height → reshape → re-apply every color/style range. Full rule and code: §6.1.3 in this skill's design-rules.md.\n\n### Text sizing — Auto + read frame dims\n\nFor text blocks, the canonical CE.SDK pattern is **Auto width/height + `getFrameWidth` / `getFrameHeight`** for measured dims. The engine shapes the text and exposes the actual frame extent through these reads, which you then use for centering, stacking, and overflow checks. Manual `setWidth`/`setHeight` on text is fragile: too small and the engine silently drops the render (no glyphs drawn, no error); too large and your alignment math is off.\n\n```js\nconst text = engine.block.create('text');\nengine.block.replaceText(text, 'Alex & Jordan');\nengine.block.setFont(text, uri, typeface); // 3-arg form (mandatory)\nengine.block.setTextFontSize(text, 64, { unit: 'Pixel' });\nengine.block.setWidthMode(text, 'Auto');\nengine.block.setHeightMode(text, 'Auto');\nengine.block.appendChild(page, text);\n\n// IMPORTANT: getWidth/getHeight return 0 in Auto mode. Read frame dims instead.\nconst w = engine.block.getFrameWidth(text); // measured shaped width\nconst h = engine.block.getFrameHeight(text); // measured shaped height\nengine.block.setPositionX(text, (pageW - w) / 2);\nengine.block.setPositionY(text, y);\n```\n\nUse absolute `setWidth`/`setHeight` only when you specifically want to constrain a wrap box (e.g. multi-line body copy with a fixed column width). Even then, set `setWidthMode(id, 'Absolute')` and pick height ≥ `fontSize × 1.6` for body sans, `≥ 1.8` for display serifs (Playfair, Cormorant) and scripts (Great Vibes, Caveat) — ascenders/descenders need the headroom or the engine drops the render.\n\n### Font-load barrier: `forceLoadResources`\n\n```js\nawait engine.block.forceLoadResources([page]); // recurses into children\n// now getFrameWidth returns real values for every text block on the page\n```\n\n`forceLoadResources(blocks)` returns `Promise<void>` that resolves once all fonts/images/fills bound to those blocks (and descendants) have finished loading. Pass `[page]` to await everything on the page. **No `setTimeout`, no polling.**\n\n`getFrameWidth(id) > 0` after the await is your \"font loaded + shaped successfully\" signal. **Never** use `getWidth(id) === 0` as a font-load probe — it returns 0 by design in Auto mode regardless of font state.\n\n<!-- feature:video -->\n\n### Video: any design can become a video\n\nScenes are not \"static\" or \"video\" at creation time — add time-based content\nto the scene you already have, then `export({ target: \"video\" })` renders the\nauthored timeline. Do NOT rebuild a scene with `scene.createVideo()` just to\nanimate it (`createVideo` and `scene.setMode('Video')` exist as conveniences;\nyou rarely need either).\n\n```js\n// Page duration = length of the exported video.\nengine.block.setDuration(page, 10); // seconds\n\n// Tracks hold timed children; blocks get offsets + durations.\nconst track = engine.block.create('track');\nengine.block.appendChild(page, track);\n// Tracks auto-arrange children back-to-back by default (this is silent —\n// no error, no warning). Turn it off before setting custom offsets below,\n// otherwise setTimeOffset appears to work but gets overwritten later.\nengine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false);\nconst block = engine.block.create('graphic');\nengine.block.appendChild(track, block);\nengine.block.setTimeOffset(block, 1.0); // enters at t=1s\nengine.block.setDuration(block, 5.0); // visible for 5s\n\n// Video fill — use the workspace:// URI from `import`.\nconst fill = engine.block.createFill('video');\nengine.block.setString(\n fill,\n 'fill/video/fileURI',\n 'workspace://assets/<sha>.mp4'\n);\nengine.block.setFill(block, fill);\n// Trim setters need the video's metadata — load it first, or they throw\n// \"The video has not been loaded yet.\"\nawait engine.block.forceLoadAVResource(fill);\nengine.block.setTrimOffset(fill, 2.0); // start 2s into the source\nengine.block.setTrimLength(fill, 5.0); // play 5s of it\n\n// Audio block — plays during the page's timeline.\nconst audio = engine.block.create('audio');\nengine.block.setString(audio, 'audio/fileURI', 'workspace://assets/<sha>.mp3');\nengine.block.appendChild(page, audio);\n```\n\n- Export: `export({ target: \"video\", format: \"mp4\", revision, blockId: page })`.\n Duration/resolution come from the page — there are no export knobs.\n- Needs the native engine (the default while video is enabled). On an engine\n error, run `diagnostics` and check `status.config.engineResolved` —\n `wasm`/`wasm-fallback` cannot encode video.\n- `preview` renders a STILL frame of a video scene, not motion — pass `time`\n (seconds) to seek: verify a few salient moments (start, a transition, the\n end). Each video preview returns `{time, duration}` JSON next to the image.\n Export for the real thing.\n- Resource loading during export is handled server-side — you do NOT need a\n `forceLoadResources` barrier before `export` (unlike the font barrier above).\n The one load you DO need is `forceLoadAVResource(fill)` before reading or\n trimming a video fill (see the recipe above).\n- **Multi-clip tracks (sequences, crossfades): disable auto-arrange first.**\n`track/automaticallyManageBlockOffsets` defaults to `true` and silently\noverwrites every child's `setTimeOffset` with an auto-computed back-to-back\nvalue — no error. It looks fine with one clip (offset 0 either way), which\nis why it hides until you need two clips to overlap. Before setting any\nchild offsets: `engine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false)`.\n<!-- /feature:video -->\n\n### Common operations cheatsheet\n\nThe `engine.*` calls used in nearly every `edit`. Verify exact signatures + types in the `cesdk-api` skill before using anything not listed here.\n\n```js\n// Scene + page basics — hierarchy is scene → stack → page (NOT scene → page).\n// engine.scene.create(\"VerticalStack\") makes the scene AND a stack container.\n// Pages attach to the stack, never directly to the scene root.\n// The \"VerticalStack\" argument is REQUIRED — without it, no stack is created\n// and findByType(\"stack\") returns []. (Use \"HorizontalStack\" for side-by-side.)\nconst scene = engine.scene.create('VerticalStack');\nconst stack = engine.block.findByType('stack')[0];\nconst page = engine.block.create('page');\nengine.block.appendChild(stack, page); // ← stack, not scene\nconst pages = engine.scene.getPages(); // DesignBlockId[]\nconst current = engine.scene.getCurrentPage(); // DesignBlockId | null\n\n// Block creation\nconst text = engine.block.create('text');\nconst graphic = engine.block.create('graphic');\n\n// Position & size\nengine.block.getPositionX(id);\nengine.block.setPositionX(id, x);\nengine.block.getPositionY(id);\nengine.block.setPositionY(id, y);\nengine.block.getWidth(id);\nengine.block.setWidth(id, w);\nengine.block.getHeight(id);\nengine.block.setHeight(id, h);\n\n// Page dimensions (for proportional calculations)\nconst pageW = engine.block.getWidth(page);\nconst pageH = engine.block.getHeight(page);\n\n// Fills + shapes (graphic blocks)\nconst shape = engine.block.createShape('rect'); // also: \"ellipse\", \"star\", \"line\", ...\nengine.block.setShape(graphic, shape);\nconst fill = engine.block.createFill('color'); // also: \"image\"\nengine.block.setFill(graphic, fill);\nengine.block.setColor(fill, 'fill/color/value', { r, g, b, a });\n\n// Text content + color + size (PIXELS — see pixel rule above)\nengine.block.setString(text, 'text/text', 'Hello');\nengine.block.setTextColor(text, { r, g, b, a }); // whole text\nengine.block.setTextColor(text, { r, g, b, a }, start, end); // char range\nengine.block.setTextFontSize(text, 32, { unit: 'Pixel' });\nengine.block.getTextFontSizes(text, { unit: 'Pixel' });\n\n// Text alignment + spacing (typed properties via setEnum / setFloat)\nengine.block.setEnum(text, 'text/horizontalAlignment', 'Center'); // \"Left\" | \"Center\" | \"Right\"\nengine.block.setEnum(text, 'text/verticalAlignment', 'Center'); // \"Top\" | \"Center\" | \"Bottom\"\nengine.block.setFloat(text, 'text/letterSpacing', 0.05); // ratio of em\nengine.block.setFloat(text, 'text/lineHeight', 1.2); // ratio of font size\n\n// Font — REQUIRES three args. Passing only (text, uri) throws a cryptic\n// \"Cannot read properties of undefined (reading 'name')\" because cesdk\n// reads typeface.name internally. The typeface object is mandatory.\nconst typeface = {\n name: 'Playfair Display',\n fonts: [{ uri, subFamily: 'Regular', weight: 'normal', style: 'normal' }]\n};\nengine.block.setFont(text, uri, typeface);\n\n// Await all resource loads (fonts, images, fills) for blocks + descendants.\n// Use this after a batch of setFont calls before reading frame dims or capturing.\nawait engine.block.forceLoadResources([page]);\n```\n\n### Z-order via child order (no z-index property)\n\nCE.SDK has no z-index. Render order = child order. Later children paint on top of earlier siblings.\n\n```js\nengine.block.appendChild(parent, child); // child becomes LAST = top-most\nengine.block.insertChild(parent, child, index); // index 0 = back-most\nengine.block.getChildren(parent); // current children, back-to-front\n```\n\n**Build pages back-to-front.** Suggested canonical layer stack for a designed page:\n\n1. page background fill — `setFill` on the page block (an attribute, not a child; sits below any children)\n2. full-bleed photo / image (first child, index 0)\n3. scrim / vignette / gradient overlay\n4. structural rules — grid hairlines, dividers, frames\n5. accent shapes — pills, badges, accent bars, icons\n6. body text\n7. headline / display text\n8. foreground decoration (corner brackets, stickers)\n\nAppend in this order. If you create text before its background pill, the pill will cover the text. Deviate from the canonical order when intent demands — it is a default, not a rule.\n\n**Fix without rebuilding** — to demote an existing sibling underneath another, use `insertChild` with a measured index:\n\n```js\n// text block already exists at top of stack; demote a sibling pill behind it\nconst children = engine.block.getChildren(page);\nconst textIdx = children.indexOf(textId); // textId must be a direct child of page (else -1)\nengine.block.insertChild(page, pillId, textIdx); // pillId now sits just behind textId\n```\n\nStack-layout pages (`VerticalStack` / `HorizontalStack`): the stack arranges pages along an axis; the same child-order rule applies WITHIN each page's children.\n\n**Never destroy + recreate to fix ordering.** Use `insertChild`.\n\n## 5. Quirks\n\nCE.SDK errors are short and opaque, and several runtime behaviors don't show up in the .d.ts. The full quirk reference — the error-message → cause table, the runtime-symptom → workaround table, and the text-doesn't-render diagnosis order — lives in this skill's `quirks.md`. On ANY cryptic error or non-rendering block, search it BEFORE bisecting: `skill({ name: 'handbook', path: 'quirks.md', query: '<fragment of the error message>' })`.\n\n### Prefer surgical fixes over wipe-and-rebuild\n\nEach rebuild costs ~10 extra tool calls (re-create scene, re-position every block, re-verify). When something looks wrong, identify the specific block and edit it in place — `setPositionY`, `setTextFontSize`, `replaceText`, etc. Reach for `engine.scene.create(...)` only when the scene graph is genuinely unrecoverable, which is rare.\n\n## 6. Design rules\n\nEnforceable design best practices with numeric thresholds — what you build to, and what the `judge` skill scores against. Each rule group ends with a **Pass condition**; a failed Pass condition pulls its parent axis below 8, and one axis below 8 fails the design (no averaging — see the `judge` skill for the axes and the gate).\n\n- **Web vs print** — where the two media diverge (line height, minimum sizes) the rules are split **Web** / **Print**; \"web\" values apply to screen-destined canvases (social posts, banners). An unqualified rule applies to both.\n- **N/A = 10** — a rule that cannot apply to a given design scores N/A, i.e. 10.\n- **Defaults fill silence** — when the user's brief gives an explicit value for a property, use it; when it is silent, apply this section's defined **default** rather than leaving the property unset or letting the engine default stand. Every numeric rule below that names a _default_ (line-height §6.1.3, type scale §6.1.2, measure §6.1.4, all-caps tracking §6.1.6, margins §6.4) is that fallback. Silence is not permission to skip the property. A brief value that violates a hard threshold is clamped to the threshold — except line-height, which is judged by the rendered gap after conversion, never by the raw number (§6.1.3).\n\n**The full rubric lives in this skill's `design-rules.md`** — read it BEFORE building: `skill({ name: 'handbook', path: 'design-rules.md' })`. Section map:\n\n- **§6.1 Typography** (§6.1.1 families/pairing · §6.1.2 type scale & canvas bands · §6.1.3 line-height/leading incl. the content-area conversion · §6.1.4 measure · §6.1.5 alignment/rag · §6.1.6 tracking & case · §6.1.7 micro-typography · §6.1.9 print rules · §6.1.10 the typography pass checklist)\n- **§6.2 Color** — palette ≤ 3 + neutrals, one accent, contrast on the actual background\n- **§6.3 Hierarchy** — one dominant element; rank via size > weight > color > spacing > position\n- **§6.4 Composition & grid** — margins 8–12% of the shortest dimension, the spacing ladder, no dead void\n- **§6.5 Craft & finish** — §6.5.1 chips/pills/badges as native text backgrounds; §6.5.2 the craft pass checklist\n- **§6.6 Localization & RTL** — pointer to the `localize` skill\n- **§6.7 Font URIs & pairing** — the pre-validated static font table + context→pairing table\n",
575
+ "docs/handbook/SKILL.md": "---\nname: handbook\ndescription: |\n The CoDesign design loop, tool contract, workspace cheatsheet, typeface\n schema, common operations cheatsheet, and the full design-quality rubric.\n **Read this skill before any edit call** — it contains the 5-step Loop\n (decide parent → read API → exec → preview → judge), the tool roster and\n cross-tool rules, the workspace revision model (list / history / rename),\n rewind-recovery procedure, the typeface object shape required by setFont,\n and §6: the design-rules index — the full enforceable rubric (typography,\n hierarchy, composition, craft) ships as this skill's design-rules.md, and\n the runtime-quirk reference as its quirks.md, both read on demand.\n\n Triggered for any task using mcp__codesign__edit or\n mcp__codesign__preview.\n---\n\n## 1. Loop\n\nYou MUST follow these steps in order for ANY canvas change:\n\n1. **Decide where you're editing from — based on what the user asked for, not what's in the workspace.** Either:\n - **New design requested** (the default in a fresh conversation) → pass `parent: null` and supply a `title` (the design name). The workspace persists across all past conversations — designs in it are not yours to continue unless the user points at them. For a new design from a vague prompt, run the `create` skill's intake first to assemble the brief.\n - **Continuing this conversation's design** → use the `revision` id from your last `edit` response as `parent`.\n - **User explicitly asked to resume earlier work** (\"keep working on yesterday's flyer\") → call `list` to see designs newest-first, then `history({ revision })` on the design they meant; pass the leaf as `parent`. Never do this unprompted.\n2. **Read the API docs** for every engine method you plan to use. Use `skill({ name: 'api' })` for signatures, `skill({ name: 'guide' })` for prose explanations and recipes. NEVER guess parameters.\n3. **Run `edit({ parent, code, note? })`** — execute the JS body. The response's first text part is JSON `{ revision, parent }` — save `revision`; it's your `parent` for the next edit.\n4. **Visually verify.** Call `preview({ blockId: <pageId>, revision: <revision> })`. Both `blockId` and `revision` are REQUIRED. Never tell the user something \"looks good\" without seeing it yourself.\n\n5. **Judge & remediate before reporting done.** When the design is a candidate for \"done\", run the build → judge → remediate loop from the `judge` skill: score all 7 design axes against your actual `preview` renders, dry-run the applicable §6 Pass checklists (§6.1.10, §6.5.2 — in this skill's design-rules.md), and remediate any applicable axis below 8 via further `edit`s — then re-preview and re-judge. Never report an unjudged design as done.\n\nSteps 1, 2, 4, and 5 are NOT optional. Step 3's `note` is encouraged: a one-liner describing what you accomplished, so you can re-ground later.\n\n### Re-grounding after a rewind / restart\n\nIf your conversation was rewound (double-ESC in Claude Code), or your process restarted mid-conversation, your in-memory \"current revision\" is wrong. The engine's state is the truth, but the server can't tell you've been rewound. Recovery:\n\n1. Call `list` — see the workspace's designs and their `latestLeaf` revisions.\n2. Call `history({ revision: latestLeaf })` — read the notes to find where the user wanted to be.\n3. Pass that revision id as `parent` on your next `edit`. The server reloads the engine to that scene before running your code — your edits land on top of the right state.\n\nRe-grounding recovers the design this conversation was already working on. It is not for starting a new conversation — a fresh session with a new request starts at `parent: null`.\n\nEditing on top of an OLDER revision (not the latest leaf) is fine: it creates a **non-destructive fork**. The previous leaves stay in storage; you can switch back any time by passing one of their ids as `parent`.\n\n## 2. Tools\n\nEach tool's full contract lives on the tool itself — its description in tools/list is authoritative and always in front of you. This section is only the roster plus the rules that span tools.\n\n- `edit({ parent, code, title?, note? })` — THE mutating tool: runs async JS (`engine` in scope) and commits a revision. The response's first text part is JSON `{ revision, parent }` — save `revision`; it is your `parent` for the next edit.\n- `preview({ blockId, revision })` — render a page/block to an inline PNG; both args required.\n- `export({ target, format, revision, blockId })` — write a pdf/image artifact; returns `{ uri, httpUrl?, path?, bytes, format, revision }`. `uri` is the durable handle; `httpUrl` is a fetch link tied to this process — never persist it.\n<!-- feature:video -->\n- `export` also accepts `target=video` (with `format: \"mp4\"`) — renders the block's authored timeline (see the video recipe in §4); needs the native engine (the default while video is enabled).\n<!-- /feature:video -->\n- `save({ revision, outPath? })` — portable `.zip` archive of a revision, for taking an editable copy OUT. Every `edit` already auto-persists; `save` is never needed for durability.\n- `load({ source, title? })` — ingest a design as a NEW root: native `.scene` / `.zip`, or foreign Photoshop `.psd` / InDesign `.idml` / PowerPoint `.pptx` / PDF (translated on the way in; the returned `warnings` are your touch-up list — preview, then repair with `edit`). Continue with `edit({ parent: revision })`.\n- `import({ source: { path } })` — bring a local file into the workspace; embed the returned `workspace://` uri in `edit` code, never `httpUrl`.\n- `asset_search({ sourceId?, query?, page?, perPage? })` — discover and search the engine's asset sources; apply results inside `edit` (the exact patterns are in the tool's description).\n- `list()` / `history({ revision })` / `inspect({ revision })` / `rename({ revision, title })` — the workspace tools; usage in §3.\n- `view({ revision, pin? })` — human-facing viewer URL. Print it once; the viewer keeps it live.\n\nCross-tool rules:\n\n- **Revision ids are durable, block ids are not.** Everything flows through revisions: `edit` produces one, `preview`/`export`/`view` consume one, `history`/`inspect` walk them. Block ids are session-scoped — re-discover blocks in the current engine; never persist or replay literal block ids (see `inspect`'s description).\n- **The single mutating tool is `edit`.** `import`, `asset_search`, and `view` never change the scene; placement and application always happen inside `edit` code.\n\n## 3. Workspace cheatsheet\n\nThe workspace is your durable memory: every revision you commit via `edit` is there, addressable by id, walkable via `history`. It survives a process restart and a Claude rewind.\n\n```\nlist() → [{ rootRevision, title, latestLeaf, updatedAt }, …]\nhistory({ revision: leaf }) → [{ revision, parent, note?, createdAt }, …] (root first)\ninspect({ revision }) → { revision, parent, note?, createdAt, title?, code? }\nedit({ parent: leaf, code }) → linear edit, no engine reload\nedit({ parent: olderRev, code }) → fork; engine reloads to olderRev first\nedit({ parent: null, title }) → new design\nrename({ revision: any, title }) → updates the root title\n```\n\nA revision id is a 12-hex-char string. You never construct it; the server hands it back from `edit`. Treat it as opaque.\n\n## 4. Engine recipes\n\n### Build ONE complete page per `edit`\n\nThe unit of work is the **page**: every `edit` call emits one complete page — all its blocks, all setFont, all sizes, the `forceLoadResources` await, then positioning. Do **not** split a single page's build across 2–3 calls. Each tool call costs 20–200s of LLM streaming wall time; one ~10000-char call is faster than three ~3000-char calls because there's no thinking-between-calls overhead.\n\nFor a **single-page design** that means the entire scene in one `edit` — scene, page, all blocks:\n\n```js\n// 1. Structure\nconst scene = engine.scene.create('VerticalStack');\nconst stack = engine.block.findByType('stack')[0];\nconst page = engine.block.create('page');\nengine.block.appendChild(stack, page);\nengine.block.setWidth(page, 720);\nengine.block.setHeight(page, 1008);\n\n// 2. Create all blocks + setFont + setString (fire-and-forget loads)\nconst titleText = engine.block.create('text');\nengine.block.setFont(titleText, URI_PLAYFAIR, TF_PLAYFAIR);\nengine.block.setString(titleText, 'text/text', 'Alex & Jordan');\nengine.block.setTextFontSize(titleText, 48, { unit: 'Pixel' });\nengine.block.setWidthMode(titleText, 'Auto');\nengine.block.setHeightMode(titleText, 'Auto');\nengine.block.appendChild(page, titleText);\n// ... repeat for every text + graphic block ...\n\n// 3. Deterministic font-load await\nawait engine.block.forceLoadResources([page]);\n\n// 4. Now position with measured frame dims\nconst w = engine.block.getFrameWidth(titleText);\nengine.block.setPositionX(titleText, (720 - w) / 2);\nengine.block.setPositionY(titleText, 250);\n// ... position everything ...\n\nreturn { type: 'text', text: `page=${page} ready` };\n```\n\nAfter this one call, your next tool call is `preview({ blockId: page, revision })`. **Exactly 2 calls per page — build, then capture. For a single-page design that's the whole job. Then STOP.**\n\n### Multi-page designs: page by page, page 1 first\n\nNever emit a multi-page design (carousel, slide deck, brochure) in one giant `edit`. Build it **page by page**, one `edit` + one `preview` per page:\n\n1. **Page 1 is the design-language gate.** The first `edit` builds the scene and page 1 only — it establishes fonts, palette, margins, grid. Preview it before touching page 2: a wrong aesthetic choice caught here costs one page, not N.\n2. **Pages 2..N each get their own `edit` + `preview`.** Each call builds one complete page reusing the design language page 1 proved out. Preview each page right after building it — the bugs on later pages are content-dependent (a headline that overflows, an image that crops badly), so batching pages doesn't avoid them, it just delays discovery. Feed findings forward: if body text at 28px overflowed on page 3, use 26px from page 4 on.\n3. **Batching is allowed only for near-identical clones** — templated pages differing by a short label or image swap. Even then, a thrown error mid-call loses every page in the batch; when in doubt, one page per call.\n\nThe objective-bug bar below applies per page: fix an overflow on page 3 with a targeted `edit` to page 3, then re-capture page 3 only.\n\n**Editing an existing multi-page design:** a mechanical change applied uniformly (swap a font everywhere, recolor a brand accent) is one `edit` looping over `engine.scene.getPages()` — that's one logical change, not a batch. Substantial per-page rework (rewriting layouts, restructuring content) goes page by page like a build, with a `preview` per reworked page.\n\nAfter the capture, fire a 2nd `edit` ONLY if the rendered PNG shows a specific, OBJECTIVE bug (text overflowing the page bounds, blocks visibly overlapping, missing characters, blank glyphs). Aesthetic refinement does NOT warrant another exec — small sub-pixel margin tweaks, \"could be a touch tighter\", \"I'd nudge this 4px\", etc. are all DISALLOWED. If in doubt, STOP. The cost of a needless 3rd or 4th call (~30s wall + thinking tokens + latency) far outweighs any aesthetic gain.\n\nCompositional balance, vertical centering, and whitespace distribution are AESTHETIC, not objective bugs — even if the imbalance is large or intentional. \"Top-heavy\", \"feels off-center\", \"bottom third looks empty\", \"needs to be re-centered\" are all aesthetic; they do NOT warrant another exec. A read-only `edit` (one that returns data and mutates nothing — `getTextFontSizes`, `getFrameWidth`, etc.) still counts as a call against the limit, plus it forces a follow-up capture. Only fire a read-only exec if you'd otherwise be guessing at a number you can't read off the captured PNG.\n\n### Font sizes are PIXELS, not points\n\nCE.SDK's font size APIs default to **points**. CE.SDK's scene unit defaults to **Inch** out of the box — but this demo wraps `engine.scene.create(...)` to call `setDesignUnit(\"Pixel\", sceneId)` immediately, so every scene the agent creates is in pixels by default. You don't need to set the unit yourself, but if you ever destroy + bare-call into the engine outside `scene.create`, re-set it. Mixing point font sizes against the pixel scene unit silently scales text ~2.5× too large.\n\n**ALWAYS pass `{ unit: 'Pixel' }`** when reading or writing font sizes:\n\n```js\nengine.block.setTextFontSize(block, 32, { unit: 'Pixel' }); // 32 px ✓\nengine.block.getTextFontSizes(block, { unit: 'Pixel' }); // returns px ✓\n```\n\n**NEVER:**\n\n```js\nengine.block.setFloat(block, 'text/fontSize', 32); // unit-less, always points ✗\nengine.block.setTextFontSize(block, 32); // defaults to points (~43 px) ✗\n```\n\nLine-height is NOT a raw multiplier either: CE.SDK's `text/lineHeight` scales the font's content area, not the em — every visual line-height target must be divided by the font's content area (measured via `engine.editor.getFontMetrics`) before being set, and a reshape (`setString`) afterwards wipes per-character style ranges, so the order is line-height → reshape → re-apply every color/style range. Full rule and code: §6.1.3 in this skill's design-rules.md.\n\n### Text sizing — Auto + read frame dims\n\nFor text blocks, the canonical CE.SDK pattern is **Auto width/height + `getFrameWidth` / `getFrameHeight`** for measured dims. The engine shapes the text and exposes the actual frame extent through these reads, which you then use for centering, stacking, and overflow checks. Manual `setWidth`/`setHeight` on text is fragile: too small and the engine silently drops the render (no glyphs drawn, no error); too large and your alignment math is off.\n\n```js\nconst text = engine.block.create('text');\nengine.block.replaceText(text, 'Alex & Jordan');\nengine.block.setFont(text, uri, typeface); // 3-arg form (mandatory)\nengine.block.setTextFontSize(text, 64, { unit: 'Pixel' });\nengine.block.setWidthMode(text, 'Auto');\nengine.block.setHeightMode(text, 'Auto');\nengine.block.appendChild(page, text);\n\n// IMPORTANT: getWidth/getHeight return 0 in Auto mode. Read frame dims instead.\nconst w = engine.block.getFrameWidth(text); // measured shaped width\nconst h = engine.block.getFrameHeight(text); // measured shaped height\nengine.block.setPositionX(text, (pageW - w) / 2);\nengine.block.setPositionY(text, y);\n```\n\nUse absolute `setWidth`/`setHeight` only when you specifically want to constrain a wrap box (e.g. multi-line body copy with a fixed column width). Even then, set `setWidthMode(id, 'Absolute')` and pick height ≥ `fontSize × 1.6` for body sans, `≥ 1.8` for display serifs (Playfair, Cormorant) and scripts (Great Vibes, Caveat) — ascenders/descenders need the headroom or the engine drops the render.\n\n### Font-load barrier: `forceLoadResources`\n\n```js\nawait engine.block.forceLoadResources([page]); // recurses into children\n// now getFrameWidth returns real values for every text block on the page\n```\n\n`forceLoadResources(blocks)` returns `Promise<void>` that resolves once all fonts/images/fills bound to those blocks (and descendants) have finished loading. Pass `[page]` to await everything on the page. **No `setTimeout`, no polling.**\n\nThis barrier is for **your own measuring** inside `edit` code — positioning by measured frame width needs the fonts loaded first. Capturing is covered server-side: `preview`, `export`, `save`, and the commit at the end of every `edit` all force-load the scene's resources before rendering or serializing, so you never need a separate wait-for-fonts edit.\n\n`getFrameWidth(id) > 0` after the await is your \"font loaded + shaped successfully\" signal. **Never** use `getWidth(id) === 0` as a font-load probe — it returns 0 by design in Auto mode regardless of font state.\n\n<!-- feature:video -->\n\n### Video: any design can become a video\n\nScenes are not \"static\" or \"video\" at creation time — add time-based content\nto the scene you already have, then `export({ target: \"video\" })` renders the\nauthored timeline. Do NOT rebuild a scene with `scene.createVideo()` just to\nanimate it (`createVideo` and `scene.setMode('Video')` exist as conveniences;\nyou rarely need either).\n\n```js\n// Page duration = length of the exported video.\nengine.block.setDuration(page, 10); // seconds\n\n// Tracks hold timed children; blocks get offsets + durations.\nconst track = engine.block.create('track');\nengine.block.appendChild(page, track);\n// Tracks auto-arrange children back-to-back by default (this is silent —\n// no error, no warning). Turn it off before setting custom offsets below,\n// otherwise setTimeOffset appears to work but gets overwritten later.\nengine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false);\nconst block = engine.block.create('graphic');\nengine.block.appendChild(track, block);\nengine.block.setTimeOffset(block, 1.0); // enters at t=1s\nengine.block.setDuration(block, 5.0); // visible for 5s\n\n// Video fill — use the workspace:// URI from `import`.\nconst fill = engine.block.createFill('video');\nengine.block.setString(\n fill,\n 'fill/video/fileURI',\n 'workspace://assets/<sha>.mp4'\n);\nengine.block.setFill(block, fill);\n// Trim setters need the video's metadata — load it first, or they throw\n// \"The video has not been loaded yet.\"\nawait engine.block.forceLoadAVResource(fill);\nengine.block.setTrimOffset(fill, 2.0); // start 2s into the source\nengine.block.setTrimLength(fill, 5.0); // play 5s of it\n\n// Audio block — plays during the page's timeline.\nconst audio = engine.block.create('audio');\nengine.block.setString(audio, 'audio/fileURI', 'workspace://assets/<sha>.mp3');\nengine.block.appendChild(page, audio);\n```\n\n- Export: `export({ target: \"video\", format: \"mp4\", revision, blockId: page })`.\n Duration/resolution come from the page — there are no export knobs.\n- Needs the native engine (the default while video is enabled). On an engine\n error, run `diagnostics` and check `status.config.engineResolved` —\n `wasm`/`wasm-fallback` cannot encode video.\n- `preview` renders a STILL frame of a video scene, not motion — pass `time`\n (seconds) to seek: verify a few salient moments (start, a transition, the\n end). Each video preview returns `{time, duration}` JSON next to the image.\n Export for the real thing.\n- Resource loading during export is handled server-side — you do NOT need a\n `forceLoadResources` barrier before `export` (unlike the font barrier above).\n The one load you DO need is `forceLoadAVResource(fill)` before reading or\n trimming a video fill (see the recipe above).\n- **Multi-clip tracks (sequences, crossfades): disable auto-arrange first.**\n`track/automaticallyManageBlockOffsets` defaults to `true` and silently\noverwrites every child's `setTimeOffset` with an auto-computed back-to-back\nvalue — no error. It looks fine with one clip (offset 0 either way), which\nis why it hides until you need two clips to overlap. Before setting any\nchild offsets: `engine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false)`.\n<!-- /feature:video -->\n\n### Common operations cheatsheet\n\nThe `engine.*` calls used in nearly every `edit`. Verify exact signatures + types in the `cesdk-api` skill before using anything not listed here.\n\n```js\n// Scene + page basics — hierarchy is scene → stack → page (NOT scene → page).\n// engine.scene.create(\"VerticalStack\") makes the scene AND a stack container.\n// Pages attach to the stack, never directly to the scene root.\n// The \"VerticalStack\" argument is REQUIRED — without it, no stack is created\n// and findByType(\"stack\") returns []. (Use \"HorizontalStack\" for side-by-side.)\nconst scene = engine.scene.create('VerticalStack');\nconst stack = engine.block.findByType('stack')[0];\nconst page = engine.block.create('page');\nengine.block.appendChild(stack, page); // ← stack, not scene\nconst pages = engine.scene.getPages(); // DesignBlockId[]\nconst current = engine.scene.getCurrentPage(); // DesignBlockId | null\n\n// Block creation\nconst text = engine.block.create('text');\nconst graphic = engine.block.create('graphic');\n\n// Position & size\nengine.block.getPositionX(id);\nengine.block.setPositionX(id, x);\nengine.block.getPositionY(id);\nengine.block.setPositionY(id, y);\nengine.block.getWidth(id);\nengine.block.setWidth(id, w);\nengine.block.getHeight(id);\nengine.block.setHeight(id, h);\n\n// Page dimensions (for proportional calculations)\nconst pageW = engine.block.getWidth(page);\nconst pageH = engine.block.getHeight(page);\n\n// Fills + shapes (graphic blocks)\nconst shape = engine.block.createShape('rect'); // also: \"ellipse\", \"star\", \"line\", ...\nengine.block.setShape(graphic, shape);\nconst fill = engine.block.createFill('color'); // also: \"image\"\nengine.block.setFill(graphic, fill);\nengine.block.setColor(fill, 'fill/color/value', { r, g, b, a });\n\n// Text content + color + size (PIXELS — see pixel rule above)\nengine.block.setString(text, 'text/text', 'Hello');\nengine.block.setTextColor(text, { r, g, b, a }); // whole text\nengine.block.setTextColor(text, { r, g, b, a }, start, end); // char range\nengine.block.setTextFontSize(text, 32, { unit: 'Pixel' });\nengine.block.getTextFontSizes(text, { unit: 'Pixel' });\n\n// Text alignment + spacing (typed properties via setEnum / setFloat)\nengine.block.setEnum(text, 'text/horizontalAlignment', 'Center'); // \"Left\" | \"Center\" | \"Right\"\nengine.block.setEnum(text, 'text/verticalAlignment', 'Center'); // \"Top\" | \"Center\" | \"Bottom\"\nengine.block.setFloat(text, 'text/letterSpacing', 0.05); // ratio of em\nengine.block.setFloat(text, 'text/lineHeight', 1.2); // ratio of font size\n\n// Font — REQUIRES three args. Passing only (text, uri) throws a cryptic\n// \"Cannot read properties of undefined (reading 'name')\" because cesdk\n// reads typeface.name internally. The typeface object is mandatory.\nconst typeface = {\n name: 'Playfair Display',\n fonts: [{ uri, subFamily: 'Regular', weight: 'normal', style: 'normal' }]\n};\nengine.block.setFont(text, uri, typeface);\n\n// Await all resource loads (fonts, images, fills) for blocks + descendants.\n// Use this after a batch of setFont calls before reading frame dims or capturing.\nawait engine.block.forceLoadResources([page]);\n```\n\n### Z-order via child order (no z-index property)\n\nCE.SDK has no z-index. Render order = child order. Later children paint on top of earlier siblings.\n\n```js\nengine.block.appendChild(parent, child); // child becomes LAST = top-most\nengine.block.insertChild(parent, child, index); // index 0 = back-most\nengine.block.getChildren(parent); // current children, back-to-front\n```\n\n**Build pages back-to-front.** Suggested canonical layer stack for a designed page:\n\n1. page background fill — `setFill` on the page block (an attribute, not a child; sits below any children)\n2. full-bleed photo / image (first child, index 0)\n3. scrim / vignette / gradient overlay\n4. structural rules — grid hairlines, dividers, frames\n5. accent shapes — pills, badges, accent bars, icons\n6. body text\n7. headline / display text\n8. foreground decoration (corner brackets, stickers)\n\nAppend in this order. If you create text before its background pill, the pill will cover the text. Deviate from the canonical order when intent demands — it is a default, not a rule.\n\n**Fix without rebuilding** — to demote an existing sibling underneath another, use `insertChild` with a measured index:\n\n```js\n// text block already exists at top of stack; demote a sibling pill behind it\nconst children = engine.block.getChildren(page);\nconst textIdx = children.indexOf(textId); // textId must be a direct child of page (else -1)\nengine.block.insertChild(page, pillId, textIdx); // pillId now sits just behind textId\n```\n\nStack-layout pages (`VerticalStack` / `HorizontalStack`): the stack arranges pages along an axis; the same child-order rule applies WITHIN each page's children.\n\n**Never destroy + recreate to fix ordering.** Use `insertChild`.\n\n## 5. Quirks\n\nCE.SDK errors are short and opaque, and several runtime behaviors don't show up in the .d.ts. The full quirk reference — the error-message → cause table, the runtime-symptom → workaround table, and the text-doesn't-render diagnosis order — lives in this skill's `quirks.md`. On ANY cryptic error or non-rendering block, search it BEFORE bisecting: `skill({ name: 'handbook', path: 'quirks.md', query: '<fragment of the error message>' })`.\n\n### Prefer surgical fixes over wipe-and-rebuild\n\nEach rebuild costs ~10 extra tool calls (re-create scene, re-position every block, re-verify). When something looks wrong, identify the specific block and edit it in place — `setPositionY`, `setTextFontSize`, `replaceText`, etc. Reach for `engine.scene.create(...)` only when the scene graph is genuinely unrecoverable, which is rare.\n\n## 6. Design rules\n\nEnforceable design best practices with numeric thresholds — what you build to, and what the `judge` skill scores against. Each rule group ends with a **Pass condition**; a failed Pass condition pulls its parent axis below 8, and one axis below 8 fails the design (no averaging — see the `judge` skill for the axes and the gate).\n\n- **Web vs print** — where the two media diverge (line height, minimum sizes) the rules are split **Web** / **Print**; \"web\" values apply to screen-destined canvases (social posts, banners). An unqualified rule applies to both.\n- **N/A = 10** — a rule that cannot apply to a given design scores N/A, i.e. 10.\n- **Defaults fill silence** — when the user's brief gives an explicit value for a property, use it; when it is silent, apply this section's defined **default** rather than leaving the property unset or letting the engine default stand. Every numeric rule below that names a _default_ (line-height §6.1.3, type scale §6.1.2, measure §6.1.4, all-caps tracking §6.1.6, margins §6.4) is that fallback. Silence is not permission to skip the property. A brief value that violates a hard threshold is clamped to the threshold — except line-height, which is judged by the rendered gap after conversion, never by the raw number (§6.1.3).\n\n**The full rubric lives in this skill's `design-rules.md`** — read it BEFORE building: `skill({ name: 'handbook', path: 'design-rules.md' })`. Section map:\n\n- **§6.1 Typography** (§6.1.1 families/pairing · §6.1.2 type scale & canvas bands · §6.1.3 line-height/leading incl. the content-area conversion · §6.1.4 measure · §6.1.5 alignment/rag · §6.1.6 tracking & case · §6.1.7 micro-typography · §6.1.9 print rules · §6.1.10 the typography pass checklist)\n- **§6.2 Color** — palette ≤ 3 + neutrals, one accent, contrast on the actual background\n- **§6.3 Hierarchy** — one dominant element; rank via size > weight > color > spacing > position\n- **§6.4 Composition & grid** — margins 8–12% of the shortest dimension, the spacing ladder, no dead void\n- **§6.5 Craft & finish** — §6.5.1 chips/pills/badges as native text backgrounds; §6.5.2 the craft pass checklist\n- **§6.6 Localization & RTL** — pointer to the `localize` skill\n- **§6.7 Font URIs & pairing** — the pre-validated static font table + context→pairing table\n",
576
576
  "docs/handbook/design-rules.md": "# Handbook §6 — Design rules (the full rubric)\n\nThe enforceable design best practices this server's designs are built to and the `judge` skill scores against. Section numbers here are canonical: `§6.x` references across the handbook and the other skills resolve to this file. Sliceable — pass `offset`/`limit` for a section, or `query` to search.\n\n### 6.1 Typography\n\nExpands **Axis 3 — Typography** (\"≤3 font families, correct pairing, sizes in band, sane line-spacing and leading, no tofu\"). Every subsection below must hold for Axis 3 to score ≥ 8.\n\n#### 6.1.1 Font families & pairing\n\n- Use **≤3 font families** per design (≤2 preferred); no more than **4 weights** total. Every extra family/weight dilutes hierarchy and reads as inconsistency.\n- Pair by **contrast**, not similarity — e.g. a serif display against a sans body, or one superfamily used across roles. Never pair two families that are almost alike (two humanist sans, two geometric sans).\n- Give every family a defined fallback: an **embedded/outlined** font (print) — and verify glyph coverage for every character in the copy (web/canvas). System substitution silently changes metrics and breaks leading and measure.\n- **Pass:** ≤3 families, ≤4 weights, pairing is by intentional contrast/superfamily, every family verified to cover the copy (print: embedded/outlined).\n\n#### 6.1.2 Type scale & size bands\n\n- Derive all sizes from **one modular scale** (ratio 1.2–1.618; 1.25 is a safe default). No arbitrary one-off sizes.\n- Keep text in clear **bands** — Display/Headline, Subhead, Body, Caption, Fine print — each separated by **≥1 scale step** so the hierarchy is unambiguous (supports Axis 2).\n- Minimum legible sizes:\n - **Web** — body ≥ **16px**, caption ≥ **12px**, never below **11px**.\n - **Print** — body **9–12pt**, caption ≥ **7pt**, never below **5pt**.\n- **Pass:** every size snaps to one modular scale; body ≥ 16px (web) / ≥ 9pt (print); bands are ≥1 step apart and visually distinct.\n\n**Canvas guidance — sizes as % of page height:**\n\n| Role | % of page height | Example for 1080 px page |\n| ----------- | ---------------- | ------------------------ |\n| Headline | 3 – 5 % | 32 – 54 px |\n| Body | 1.5 – 2 % | 16 – 22 px |\n| Caption | 1.2 – 1.5 % | 13 – 16 px |\n| Min legible | — | 14 px |\n\n#### 6.1.3 Line height / leading — the headline rule\n\nLine spacing is the top driver of the \"inconsistent typography\" complaint. Three principles govern it: **explicitness** (every text block carries a deliberately set line-height — never an inherited default), the **inverse rule** (small text needs more line-height; large display needs less), and **consistency** (identical leading across same-band text within one design).\n\n- **Set line-height explicitly on every text block** — including single-line labels, captions, and all-caps titles, not just multi-line body copy. An unset line-height (the engine/CSS default) is a defect even when the block is one line: it silently tracks the font's default metrics, can't be verified against the bands below, and defeats both the consistency and all-caps rules. A one-line all-caps label still takes the tighter all-caps multiplier (**0.9–1.0**); the value is judged by band + intent, not by whether the text happens to wrap. Same-band single-line blocks (e.g. an attribution and a caption in the same face) share their leading just as body blocks do.\n\n> **Authoring in CoDesign / CE.SDK — convert visual line-height to the engine's content-area scale.** Every line-height figure in this section is a _visual_ target expressed as a CSS-style unitless multiplier. CE.SDK's `text/lineHeight` is **scaled differently** from CSS: it multiplies the font's **content area** — `(ascender − descender + lineGap) / unitsPerEm`, which varies by face (~1.15–1.35 for common text faces, higher for tall display faces) — not the em / point-size. Setting `text/lineHeight` to the raw visual target therefore renders visibly looser, leaving a descender-sized void between lines (worst on multi-line all-caps display like Anton). **Convert every value here by _dividing_ the visual target by the font's content area, measured per font via `engine.editor.getFontMetrics`,** before calling `engine.block.setFloat(id, 'text/lineHeight', …)`:\n>\n> ```js\n> const [block] = engine.block.findAllSelected(); // or a known block id\n> const fontUri = engine.block.getString(block, 'text/fontFileUri');\n> const m = await engine.editor.getFontMetrics(fontUri);\n> const contentArea = (m.ascender - m.descender + m.lineGap) / m.unitsPerEm;\n>\n> const targetLineHeight = 1.2; // the VISUAL line-height the design/prompt wants (a §6.1.3 band value)\n> engine.block.setFloat(\n> block,\n> 'text/lineHeight',\n> targetLineHeight / contentArea\n> );\n> ```\n>\n> `targetLineHeight` is the **visual** leading you want to _see_ (all-caps headline 0.95, body 1.5, …) — the same number as the bands below, and the value you would naively set. It is **not** what lands on the block: the engine receives `targetLineHeight / contentArea`, which is what makes the _render_ hit the target (measured: a Playfair Display headline at content area ~1.33 → `0.95 / 1.33 ≈ 0.71`; Roboto at ~1.17 → `0.95 / 1.17 ≈ 0.81` — **different engine values for the same visual target**, which the old fixed `× 0.66` → 0.63 could not give). `descender` is negative in font metrics, so `ascender − descender` sums the two extents. **Fallback:** when the metrics aren't available (font not yet loaded, etc.), `targetLineHeight × ~0.66` is a rough eyeball only — it freezes `1 / content-area` at a typical ~1.5 em and will mis-correct compact or extra-tall faces; prefer the measured conversion. After changing `lineHeight`, re-set the block's string to itself to force a reshape (CE.SDK caches text layout until the string changes). **⚠ Re-setting the string wipes every per-character style range** — accent colors and per-word font/size applied via `setTextColor(id, …, start, end)` (and the like) silently revert to the block's base style. So the order is **line-height → reshape → re-apply every color/style range**; never reshape a multi-color headline (e.g. one accent-colored word) without re-coloring it afterward, or the accent vanishes with no error. The rubric targets below are **unchanged** — the judge skill scores the _rendered_ visual gap (~0.15–0.25× cap height for all-caps, §6.1.6), never the raw engine number.\n>\n> **⚠ An explicit line-height from the user's brief is a raw visual target — it is NOT a rubric violation until you have applied the content-area conversion and checked the rendered gap.** A value from the user's brief expresses the _same_ visual-multiplier space as the targets in this section, so it needs the content-area conversion (divide by the font's content area, §6.1.3) before it means anything to the engine, and the \"prompt value clamped to the rubric\" carve-out (\"Defaults fill silence\" above) only fires on a **real** violation — which, per this section, is judged by the **rendered visual gap**, not the raw number. Concretely: a prompt asking for an all-caps line-height of **1.15** is not \"too loose for the 0.9–1.0 all-caps band\" — divided by the font's content area (~1.5 for a typical display face) it becomes an engine value of ~0.77 that renders as a correctly tight all-caps line. **Never** compare the prompt's pre-conversion number against a rubric range in raw-number space and clamp on the mismatch; convert it (divide by content area), render it, and only override if the _gap you see_ violates §6.1.6. When the prompt gives an explicit line-height, the default is to **use it** (the prompt author is naming the visual target, not the engine value) — the rubric overrides only a rendered-gap violation, never a raw-number one.\n\n- **Web (unitless multipliers):**\n - Body **1.4–1.6** (default **1.5**); long-form reading up to **1.65**.\n - Headlines **1.1–1.25** (default **1.1**); large display **≤1.1** (default **1.1**).\n - **All-caps display / headlines** take a **tighter** multiplier than mixed-case — **0.9–1.0** for multi-line — because caps fill only the cap-height band (~70% of the em) and use no descender space; a value that looks tight for mixed-case leaves a descender-sized void between all-caps lines. Judge by the **visual gap** (target ~0.15–0.25× cap height), not the number.\n - **WCAG:** body line-height ≥ **1.5**; paragraph spacing ≥ **2×** the font size.\n- **Print (leading as a unitless multiplier of point size):**\n - Body **1.2–1.45** (default **1.2**). Push toward the low end for dense or large-x-height faces, toward the high end for airy or small type.\n - Headlines **1.0–1.2** (default **1.05**); tight display setting may go slightly negative (**0.95–1.0**).\n - **All-caps titles: 0.9–1.0** (tighter than mixed-case display's 1.0–1.2) — same cap-height reason.\n- **Localized / non-Latin copy — the per-script rules live in the `localize` skill.** Per-script leading bands and diacritic rules — the capital-diacritic all-caps loosening, the marked-capital mixed-case nudge, the localized-display default, and the CJK / Arabic-Naskh / Vietnamese bands — live in the `localize` skill's `reference/` family files (`latin-expansion.md`, `arabic.md`, `cjk.md`, `vietnamese.md`, `cyrillic.md`). All values there are **visual multipliers** — apply this section's content-area conversion like every value here (§6.6).\n- **Tune leading to measure** — longer measure needs more leading; short measure needs less. Never set leading in isolation from line length (§6.1.4).\n- **Consistency** — every body block in one design shares the same leading; every same-band heading shares the same leading.\n- **Pass:** **every text block carries an explicit line-height (incl. single-line & all-caps labels)**; body line-height **1.4–1.6** (web) / **1.2–1.45** (print); headline leading tighter than body; **all-caps titles tighter still (0.9–1.0, web & print), no descender-void between lines**; leading uniform across same-band text; **localized / non-Latin leading set per the `localize` skill's family files**; WCAG body ≥ 1.5 (web).\n\n#### 6.1.4 Measure (line length)\n\n- Set body **measure to 45–75 characters** (~66 ideal for a single column; **40–50** per column in multi-column layouts). Too-long lines lose the return sweep; too-short lines fracture rhythm.\n- On wide canvases, constrain body width — don't let body text run the full canvas width.\n- **Pass:** body measure 45–75 chars (40–50 multi-column); no full-canvas-width body lines.\n\n#### 6.1.5 Alignment, rag, justification\n\n- Default body alignment: **left** (LTR) / **right** (RTL). Avoid centered or justified _running_ text.\n- Justify only with **hyphenation on** and rivers controlled (print); **never justify short measure**.\n- Keep a **soft rag** — no ladders (repeated hyphens/word-shapes down the edge), no line more than ~50% shorter than its neighbors.\n- **Pass:** body not centered/justified without cause; RTL layouts mirrored (ties to the RTL axis); rag is soft, no ladders.\n\n#### 6.1.6 Tracking / letter-spacing & case\n\n- **Don't letter-space lowercase body text.** Tighten large display slightly (**−1% to −3%**).\n- **Loosen all-caps and small-caps — always, and to a defined default.** Never leave all-caps at 0 tracking. **Default +10% (0.1em)**; acceptable band **+5% to +25% (0.05–0.25em)**, scaled by size and length — tighter (~+8%) for larger/longer strings, looser (up to +25%) for short airy display labels and wide-set wordmarks. On **web**, never below the WCAG floor of **0.12em** — so a web all-caps label defaults to **0.12em**, not 0.1em. This default applies even to one-line labels and even when the prompt is silent on tracking (per \"Defaults fill silence\" above).\n- Avoid long strings of **all-caps** — reserve caps for short labels; they slow reading in body copy.\n- **Setting all-caps titles** — caps have no ascender/descender extension, so set them **optically**: pull line-height below the mixed-case display range (see §6.1.3), space to **cap height** rather than the em box, and prefer real small-caps to shrunk-and-tracked caps where the family offers them. Keep all-caps to a line or two of display — never a paragraph.\n- Use **real weights and real italics**, never faux-bold or faux-oblique when the family provides the true cut.\n- **Pass:** body untracked; all-caps loosened to the defined default (**+10%; web ≥0.12em**, band +5–25%) and short, **with line-height tightened per §6.1.3 (no inter-line void)**; no faux styling where a real weight/italic exists.\n\n#### 6.1.7 Micro-typography\n\n- Use **smart punctuation**: curly quotes (\" \" ' '), en dash (–) and em dash (—) correctly, real ellipsis (…). No straight quotes, no `--` for a dash.\n- **No double spaces.** Use **non-breaking spaces** to prevent bad breaks (number + unit, \"Fig. 4\", brand names).\n- **No widows or orphans**; no single-word last line; no hyphen on a paragraph's last line.\n- **No missing glyphs / tofu (□)** and **no clipped** ascenders/descenders — the direct expansion of Axis 3's \"no tofu.\" Verify the chosen font actually covers every character in the copy (critical for localized text).\n- **Pass:** smart punctuation throughout; zero tofu and zero clipping; no widows/orphans in headings or short blocks.\n\n#### 6.1.9 Print-specific rules\n\n- Work in **points/picas** and set text to a **baseline grid** so leading stays consistent across columns and pages.\n- Honor the print minimum sizes (§6.1.2); target **≥300dpi**; **embed or outline** all fonts; keep text out of bleed and inside the safe margin (**~8%**, ties to Composition/Axis 5).\n- **Pass:** baseline-grid aligned; fonts embedded/outlined; sizes ≥ print minimums; text inside the safe area, clear of bleed.\n\n#### 6.1.10 Typography pass checklist\n\nExpands **Axis 3 (Typography) — must score ≥ 8.** Each line is a yes/no the judge skill runs against the rendered output. Any \"no\" on an applicable line drops Axis 3 below 8.\n\n**Both media**\n\n- [ ] ≤3 font families, ≤4 weights, pairing by contrast/superfamily (§6.1.1)\n- [ ] Every family has a defined fallback / is embedded-outlined (§6.1.1)\n- [ ] All sizes come from one modular scale; bands ≥1 step apart (§6.1.2)\n- [ ] Line-height set explicitly on every text block, incl. single-line & all-caps labels (§6.1.3)\n- [ ] Headline leading tighter than body; leading uniform across same-band text (§6.1.3)\n- [ ] All-caps titles set by cap-height — line-height tightened, no descender-void between lines (§6.1.3, §6.1.6)\n- [ ] Localized / non-Latin text: leading bands + per-language conventions pass the `localize` skill's family files & checklist (§6.1.3 pointer, §6.6)\n- [ ] Body measure 45–75 chars (40–50 multi-column) (§6.1.4)\n- [ ] Body not centered/justified without cause; rag soft, no ladders (§6.1.5)\n- [ ] Body untracked; all-caps loosened to default (+10%; web ≥0.12em, band +5–25%) and short; no faux bold/italic (§6.1.6)\n- [ ] Smart punctuation; no double spaces; non-breaking spaces where needed (§6.1.7)\n- [ ] No widows/orphans; zero tofu; no clipped ascenders/descenders (§6.1.7)\n\n**Web**\n\n- [ ] Body ≥16px, caption ≥12px, never <11px (§6.1.2)\n- [ ] Body line-height 1.4–1.6; WCAG body ≥1.5, paragraph spacing ≥2× size (§6.1.3)\n- [ ] All-caps display line-height 0.9–1.0 (tighter than mixed-case display) (§6.1.3)\n\n**Print**\n\n- [ ] Body 9–12pt, caption ≥7pt, never <5pt (§6.1.2)\n- [ ] Body leading 1.2–1.45; headlines 1.0–1.2; all-caps titles 0.9–1.0 (§6.1.3)\n- [ ] Baseline-grid aligned; fonts embedded/outlined; ≥300dpi; text inside ~8% safe margin (§6.1.9)\n\n### 6.2 Color\n\nExpands **Axis 4 — Color** (palette respected, exactly one consistent accent, strong contrast everywhere, esp. text on photos/dark backgrounds).\n\n#### Color hierarchy (text on dark backgrounds)\n\n| Role | RGB |\n| --------------------------- | ------------------ |\n| Primary text (headline) | (1.0, 1.0, 1.0) |\n| Accent text (subtitle) | brand color |\n| Secondary text (tagline) | (0.75, 0.75, 0.80) |\n| Tertiary text (description) | (0.5, 0.5, 0.55) |\n\n**Pass:** ≤ 3 colors + neutrals; exactly one consistent accent; all text meets contrast on its actual background (fill, photo, or field — not the page).\n\n### 6.3 Hierarchy\n\nExpands **Axis 2 — Hierarchy**: one element dominates, the reading order is unambiguous, and the eye lands where it should first.\n\n- **One primary element.** Exactly one thing is the hero — the largest, heaviest, highest-contrast element on the page. Everything else is visibly subordinate. Two elements competing at the same visual weight is the most common hierarchy failure: neither wins and the eye stalls.\n- **Rank with the hierarchy chain — `size > weight > color > spacing > position`.** Establish rank primarily by **size**, then **weight**, then **color/contrast**, then the **space around** an element, and only last by position. Don't lean on position alone — a block isn't dominant just because it sits at the top; size/weight/contrast must carry it.\n- **Reading order is deliberate.** The scale steps map to the intended path: hero → supporting → detail. A reader should never hunt for where to start or what comes next. Keep the levels few — ≈3 (primary / secondary / tertiary); more than ~4 competing sizes reads as noise.\n- **Group related, separate unrelated (proximity).** Tight spacing binds a set (eyebrow + headline, label + value); wider spacing sets groups apart (§6.4's ladder). Proximity — not boxes or rules — is the first grouping tool.\n- **Colour serves rank, not decoration.** The single accent (§6.2) marks the one thing it should pull the eye to; spending it on secondary elements flattens the hierarchy.\n- **Pass:** exactly one dominant element; no two elements competing at the same level; reading order unambiguous (hero → supporting → detail); rank carried by size/weight/colour/spacing, not position alone; ≈3 (≤4) distinct levels; related items grouped tighter than unrelated ones; the accent reinforces the primary.\n\n### 6.4 Composition & grid\n\nExpands **Axis 5 — Composition**: proper margins, a shared grid, a consistent spacing scale, balanced whitespace, and nothing off-canvas or overlapping. Composition is **relative to the canvas** — every number below scales with the page, so it holds across formats.\n\n- **One margin line / shared grid.** Left-aligned elements share one X; centered elements are **mathematically centered** (`pageX + (pageW − blockW)/2`); right-aligned share one right edge. Pick an alignment per element and hold it — a block that's \"almost\" on the margin reads as a mistake.\n- **Page margins = 8–12% of the _shortest_ page dimension.** Key the outer margin to the shortest side so it stays proportional on any aspect (≈86–130px on a 1080-wide canvas). Content lives inside this frame; nothing crosses it except a deliberate full-bleed background.\n- **One spacing scale.** Base unit ≈0.5–1% of page height; use multiples — 1× / 2× / 3× / 4× / 6× — never arbitrary gaps. More space above a heading than below it; tighter within a group than between groups. The spacing ladder (proportion of page height):\n\n | Relationship | Spacing |\n | --------------------------- | ------- |\n | Accent / decoration → title | 0.5–1× |\n | Title → subtitle | 1–2× |\n | Subtitle → body | 2–3× |\n | Between body paragraphs | 1.5–2× |\n | Between content groups | 4–6× |\n\n- **Balanced whitespace — no lopsided dead void.** Whitespace is intentional and distributed; the content should occupy or balance the whole canvas. A large empty band on one side — a top-clustered layout that letterboxes the bottom of a tall canvas — fails composition even when every element is individually fine. Balance the content group within the frame; don't top-anchor and leave a void.\n- **Nothing off-canvas or overlapping.** Every element sits fully within the page (bar intentional edge-bleed decoration); no unintended overlap, no clipping at the edges.\n- **Background back-most.** The full-bleed background / photo is the first child (behind everything); structural rules, then accents, then text paint on top — build back-to-front.\n- **Pass:** margins 8–12% of the shortest dimension; every element on the shared grid / one alignment; one consistent spacing scale following the ladder; whitespace balanced with no lopsided dead void; nothing off-canvas, clipped, or overlapping; background back-most.\n\n#### Background\n\n- Card / poster with theme → use a generated or imported background image; place back-most via `engine.block.insertChild(page, bg, 0)` (see §4 \"Z-order via child order\"). If text contrast suffers, add a semi-transparent overlay.\n- Card without theme → solid color or subtle gradient.\n- Document / form / data → solid color or white.\n\n#### Decorative elements\n\n- Low opacity (15 – 30 %) for background decoration.\n- Echo the existing color system — never introduce new colors for decoration alone.\n- Less is more — one accent bar > three.\n\n### 6.5 Craft & finish\n\nExpands **Axis 6 — Craft** (no overflow/overlap/blank text, legible chart labels with correct proportions, intentional crops, clean decorative elements). Most of this axis is still to be written on the §6.1 pattern; the first written topic is accent components (§6.5.1).\n\n#### 7.5.1 Accent components — pills, badges, buttons, tags\n\nA **chip** — a pill/eyebrow, badge, tag, or button — is a short label sitting on its own filled, rounded field. How it's _built_ is a craft concern: a well-made chip binds the field to the label so they never drift.\n\n- **Build it as a text block with a native text background — not a rectangle behind floating text** — rationale: a separate backing shape forces you to hand-measure the label width and re-center it inside the box, and the two fall out of sync on any copy/font/localization change (label creeps off-center, box mis-fits, re-fit math must be redone per variant). Binding the field to the text keeps them locked and reflows correctly when the label changes. _(CoDesign/CE.SDK: enable `backgroundColor/enabled`, then `backgroundColor/color` + `backgroundColor/padding{Left,Right,Top,Bottom}` + `backgroundColor/cornerRadius` on the text block itself.)_ A separate container block is legitimate **only** for a chip carrying **non-text** content — an icon + text, an image, multiple elements.\n- **Radius matches the chip's intent** — key the corner radius to the field's padded height `h`: **pill / capsule = `h/2`** (true stadium ends — eyebrows, status, filter chips); **badge / tag / button (soft rectangle) = ~0.1–0.25×h** (≈6–12px on a standard control); **`0` = a square tag**. Pick capsule _or_ clearly-soft-rect — never an ambiguous near-capsule that reads as a mistake.\n- **Padding — horizontal roomier than vertical, symmetric per axis** — left = right, top = bottom (unless deliberately asymmetric); horizontal padding **~1.5–2.5× the vertical**; against the label size, horizontal **~0.8–1.5× the font size**, vertical **~0.4–0.7×**. The label must never touch the field edge. Compensate trailing **letter-spacing** on all-caps labels (§6.1.6) — tracking adds space after the last glyph, so trim right padding a hair to keep the label optically centered.\n- **The label is still a label** — it obeys the type rules like any other text: all-caps tracking (§6.1.6), explicit line-height (§6.1.3), and the type-scale minimums (§6.1.2) — a chip never licenses sub-minimum text. Keep it **short (≤3 words)**; a chip is not a text container.\n- **Align by the field edge, not the text.** A chip aligns to the margin line, the grid, and to neighbouring elements (a headline edge, another chip) by its **background field's outer edge** — the padded box — never by its glyph run. The two differ by the padding, so aligning on the text leaves the visible chip off the line by exactly that padding. **Measure the field's real edge** — the outer edge of the padded box (the glyph extent ± its padding), not the label's glyph run alone; and beware that engine glyph metrics can misreport (especially for RTL/Arabic), silently drifting the chip off-margin.\n- **RTL** — the whole chip mirrors to the opposite margin (§6.6), its field edge on that margin.\n- **Color & contrast** — the field uses the design's single accent or a neutral, **never a new color** (§6.2); check the label's contrast **on the fill**, not against the page (§6.2).\n- **Consistency across the design** — all chips share one system: one radius family (all capsules _or_ all the same soft-rect radius — don't mix), one padding scale, one label treatment. One-off chip geometry reads as a defect.\n- **Pass:** every text-only chip is a text block with a native text background (no separate backing rectangle); radius matches intent (pill = h/2; tag/button ~0.1–0.25×h, no ambiguous near-capsule); padding symmetric per axis, horizontal ~1.5–2.5× vertical, label clear of the field edge with trailing tracking compensated; label follows §6.1.2/§6.1.3/§6.1.6 and stays ≤3 words; the chip is aligned to the margin line / grid / neighbouring elements by its **background field edge** (not its text), measured from the field's real rendered edge, and mirrored to the opposite margin in RTL; single-accent (or neutral) fill with the label contrast-checked on the fill; all chips in the design share one radius/padding/label system.\n\n#### 7.5.2 Craft pass checklist\n\nExpands **Axis 6 (Craft) — must score ≥ 8.** Each line is a yes/no the judge skill runs against the rendered output. Any \"no\" on an applicable line drops Axis 6 below 8. _(Craft is otherwise still being written; this checklist currently covers §6.5.1 — accent components. Construction is medium-agnostic, so there is no Web/Print split.)_\n\n**Both media**\n\n- [ ] Text-only chips (pills/badges/buttons/tags) built as a text block with a native text background — no separate rectangle behind floating text (§6.5.1)\n- [ ] Radius matches intent: pill/capsule = half the padded height; tag/button = small fixed radius (~0.1–0.25×h); no ambiguous near-capsule (§6.5.1)\n- [ ] Padding symmetric per axis, horizontal ~1.5–2.5× vertical, label never touching the field edge; trailing all-caps tracking compensated (§6.5.1, §6.1.6)\n- [ ] Chip label follows label typography — all-caps tracking (§6.1.6), explicit line-height (§6.1.3), ≥ type-scale minimum (§6.1.2) — and stays ≤3 words (§6.5.1)\n- [ ] Chip aligned to margin / grid / neighbouring elements by its background **field edge**, not its text — measured from the field's real rendered edge (§6.5.1, §6.4); whole chip mirrored to the opposite margin in RTL (§6.5.1, §6.6)\n- [ ] Single-accent (or neutral) fill, no new color; label contrast checked on the fill, not the page (§6.5.1, §6.2)\n- [ ] All chips in the design share one radius family, one padding scale, one label treatment (§6.5.1)\n\n### 6.6 Localization & RTL\n\nExpands the localization axis. The base typographic couplings live above: font coverage and no-tofu (§6.1.7), mirrored alignment (§6.1.5), and the content-area line-height conversion (§6.1.3). Everything language-specific — per-script leading bands and diacritic rules (the `localize` skill's `reference/` family files: `latin-expansion.md`, `arabic.md`, `cjk.md`, `vietnamese.md`, `cyrillic.md`), the snap-to-reference workflow, per-language conventions, RTL mirroring, and non-Latin font loading — lives in the `localize` skill.\n\n### 6.7 Font URIs & pairing (CoDesign)\n\n#### Font URIs\n\nPre-validated **static (non-variable)** TTF URLs. Variable fonts with `[wght]` in the URL load (HEAD 200) but fail to render glyphs in CE.SDK — always use the static URLs below.\n\n| Family | URI |\n| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |\n| Roboto | `https://cdn.img.ly/packages/imgly/cesdk-js/latest/assets/extensions/ly.img.cesdk.fonts/fonts/Roboto/Roboto-Regular.ttf` |\n| Open Sans | `https://cdn.img.ly/packages/imgly/cesdk-js/latest/assets/extensions/ly.img.cesdk.fonts/fonts/OpenSans/OpenSans-Regular.ttf` |\n| Montserrat | `https://cdn.img.ly/packages/imgly/cesdk-js/latest/assets/extensions/ly.img.cesdk.fonts/fonts/Montserrat/Montserrat-Regular.ttf` |\n| Caveat | `https://cdn.img.ly/packages/imgly/cesdk-js/latest/assets/extensions/ly.img.cesdk.fonts/fonts/Caveat/Caveat-Regular.ttf` |\n| Great Vibes | `https://raw.githubusercontent.com/google/fonts/main/ofl/greatvibes/GreatVibes-Regular.ttf` |\n| Playfair Display | `https://fonts.gstatic.com/s/playfairdisplay/v40/nuFvD-vYSZviVYUb_rj3ij__anPXJzDwcbmjWBN2PKdFvXDXbtY.ttf` |\n| Source Sans 3 | `https://fonts.gstatic.com/s/sourcesans3/v19/nwpBtKy2OAdR1K-IwhWudF-R9QMylBJAV3Bo8Ky462EK9C4.ttf` |\n| Cormorant Garamond | `https://fonts.gstatic.com/s/cormorantgaramond/v21/co3umX5slCNuHLi8bLeY9MK7whWMhyjypVO7abI26QOD_v86KnTOjw.ttf` |\n| EB Garamond | `https://fonts.gstatic.com/s/ebgaramond/v32/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-6_RkBI96.ttf` |\n| Inter | `https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjQ.ttf` |\n\n**Use these URLs verbatim — do not \"fix\" them.** Three CDNs back this list:\n\n- `cdn.img.ly/...` — img.ly's bundled font assets (most stable).\n- `raw.githubusercontent.com/google/fonts/main/ofl/...` — only works for fonts that ship as `<Name>-Regular.ttf` in the repo root (Great Vibes, Pacifico). Most other families in this repo are variable-only and will fail.\n- `fonts.gstatic.com/s/...` — Google's font CDN. URLs are versioned (e.g. `/v40/`) but stable for years.\n\nFor a family not listed, query Google Fonts CSS API for the canonical static URL:\n\n```js\n// Returns a `url(https://fonts.gstatic.com/s/.../...ttf)` you can use directly.\nconst css = await fetch(\n `https://fonts.googleapis.com/css?family=${encodeURIComponent(name)}:400`,\n { headers: { 'User-Agent': 'Mozilla/5.0' } }\n).then((r) => r.text());\nconst uri = css.match(/url\\(([^)]+\\.ttf)\\)/)?.[1];\n```\n\nSilent font load failures (variable fonts, wrong path) produce blank text blocks — geometry is correct but glyphs never draw. Always use the table above; only HEAD-check when adding a new family from the CSS API.\n\n#### Context → pairing\n\n| Context | Style | Pairing |\n| ------------------- | ----------------------------- | ---------------------------------------------- |\n| Wedding invitation | Elegant serif + script accent | Playfair Display + Great Vibes + Source Sans 3 |\n| Restaurant menu | Serif heading + sans body | Cormorant Garamond + Inter |\n| Tech / startup | Clean modern sans | Inter (single family, weight variation) |\n| News / editorial | Traditional, authoritative | EB Garamond + Source Sans 3 |\n| Luxury brand | High-contrast serif | Cormorant Garamond + Montserrat |\n| Children's brand | Rounded, playful | Caveat + Open Sans |\n| E-commerce | Readable, trustworthy | Montserrat + Open Sans |\n| Portfolio / agency | Bold, creative | Playfair Display + Inter |\n| Academic / research | Classic, formal | EB Garamond + Source Sans 3 |\n\nFor contexts not listed: pick from the URI table above with the contrast principle (serif heading + sans body, or single-family with weight variation). Never use more than 3 families.\n",
577
- "docs/handbook/quirks.md": "# Handbook quirks — cryptic errors, runtime symptoms, diagnosis order\n\nThe handbook's quirk reference. CE.SDK errors are short and opaque. Two tables — error-message → cause, and runtime-symptom → workaround. Check both before bisecting; search this file with `query` using a fragment of the error message.\n\n## Cryptic errors\n\n| Error | Cause | Fix |\n| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |\n| `Cannot read properties of undefined (reading 'name')` | `setFont` called without the `typeface` object (3rd arg). | Pass `setFont(id, uri, { name, fonts: [{ uri, subFamily, weight, style }] })`. |\n| `The block doesn't have a background color.` | `setBackgroundColor` was called on a `page` (or other block kind that lacks the prop). | Pages don't accept `setBackgroundColor`. Use a full-bleed `graphic` block with a color fill instead. |\n| Text block exists with correct geometry but **glyphs don't draw** | Font URI returned HTTP 200 but the file isn't a renderable static TTF (variable, etc.). | Swap to a known-good static URI from the font table (design-rules.md §6.7). HEAD 200 ≠ renders. |\n| `Couldn't find property \"<name>\" on block.` | Property doesn't exist on this block type, or you guessed the property name. | Read `cesdk-guide/api/BlockAPI.md` for the exact property name and which block types accept it. |\n| `Invalid color reference …` | A color/fill value was passed without first calling `createFill`/`setColor`. | Always `createFill('color')` → `setColor(fill, 'fill/color/value', {r,g,b,a})` → `setFill(block, fill)`. |\n| `Setting the \"text/fontFileUri\" property directly is unsupported.` | Tried to bypass `setFont` via `setString(id, \"text/fontFileUri\", uri)`. | Always go through `setFont(id, uri, typeface)`. There is no shortcut. |\n\nWhen a new error type shows up, isolate it with **one** small `edit` (a single block, the failing call), not a series of trial-and-error rebuilds. The `cesdk-guide/api/BlockAPI.md` reference is the markdown explainer; for the authoritative TypeScript signature, read `cesdk-api/index.d.ts`.\n\n## Runtime quirks\n\nThese are **runtime semantics that don't show up in the .d.ts**. Check this list before re-bisecting.\n\n| Symptom | What's happening | Workaround |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `setFont(id, newUri, …)` doesn't change the rendered font on an existing block. | Engine treats two typeface objects with the same `name` as identical and skips the swap. | Use a unique `name` per swap (e.g. `\"Playfair Display v2\"`), OR delete and recreate that single block with the new font. |\n| Text doesn't reflow after `setFont` / `setTextFontSize`. Old glyph layout sticks. | The text shape is cached and only invalidated when the string content changes. | After the font/size change, force a re-shape: `engine.block.setString(id, \"text/text\", engine.block.getString(id, \"text/text\"))`. |\n| `preview` immediately after `setFont` shows blank glyphs even though the URL is good. | Font fetch hasn't completed when export runs. | `await engine.block.forceLoadResources([page])` inside your edit before reading frame dims or capturing. Resolves when all fonts on the page (and descendants) are loaded. |\n| Text block exists with the right font, string, size, and position — but glyphs don't draw. | Block height is too small for the font's ascender + descender extent. The engine silently drops the render rather than overflowing or clipping. | Set height ≥ `fontSize × 1.6` for sans-serif body, `≥ 1.8` (better 2.0) for display serifs (Playfair, Cormorant) and scripts (Great Vibes, Caveat). Or use Auto + getFrameWidth (SKILL.md §4). |\n| `getWidth(id)` / `getHeight(id)` return `0` on an Auto-sized text block. Tempting to read this as \"font didn't load → text broken\". | `getWidth`/`getHeight` return what you set; in Auto mode that's `0`. They are **not** measurement APIs and **not** a font-load signal. | Use `engine.block.getFrameWidth(id)` / `getFrameHeight(id)` for the measured shaped extent. `getFrameWidth(id) > 0` is the actual \"font loaded + shaped\" probe. |\n| Pages attached via `engine.block.appendChild(scene, page)` don't show up, render at the wrong position, or break multi-page layout. Or `findByType(\"stack\")` returns `[]` and `appendChild(stack, page)` errors with `parent: Expected number, received undefined`. | `engine.scene.create()` (no argument) creates a scene WITHOUT a stack. Pages must attach to a stack, not the scene root. | Always pass the layout type: `engine.scene.create(\"VerticalStack\")` (or `\"HorizontalStack\"`). Then `engine.block.findByType(\"stack\")[0]` returns the auto-created stack, and `engine.block.appendChild(stack, page)` works. The hierarchy is always **scene → stack → page**. |\n| Anthropic returns `400 ... image dimensions exceed max allowed size for many-image requests: 2000 pixels`. | An image from something other than `preview` (e.g. an image your edit `code` returned inline) exceeded 2000 px on a dimension. `preview` itself is capped at 0.25 megapixels and 1568 px per dimension, so it can no longer trigger this. | Check images your own `code` returns inline; keep them under 2000 px per dimension. For seeing your work, prefer `preview` on a single page — it is always within limits. |\n| Accent shape covers text it should sit behind (pill hides headline; frame hides photo edge). | Child block created and appended AFTER the block it should sit under. `appendChild` places later children on top — there is no z-index property. | Create the background shape first, append it, THEN create+append the foreground text. To fix an existing scene without destroying anything: `engine.block.insertChild(page, bgId, 0)` to push back-most, or insert before a sibling via `getChildren(page).indexOf(siblingId)`. See SKILL.md §4 \"Z-order via child order\". |\n\n<!-- feature:video -->\n\n| A track child's `setTimeOffset` \"doesn't stick\" — a video/shape renders at the wrong moment, or clips with deliberately overlapping offsets (for a crossfade) instead play back-to-back with a hard cut and no overlap. | `track/automaticallyManageBlockOffsets` defaults to `true`. The track auto-arranges its children back-to-back by cumulative duration and silently overwrites any manual `setTimeOffset` — no error. Coincidentally correct with a single child at offset 0, which is why it hides until you need custom/overlapping timing. | `engine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false)` right after creating the track, before setting any child's `setTimeOffset`. Verify with `engine.block.getTimeOffset(child)` if in doubt. |\n\n<!-- /feature:video -->\n\n## Diagnosis order — text doesn't render\n\nCheap → expensive:\n\n1. **Did you await fonts?** — `await engine.block.forceLoadResources([page])` inside the same edit before measuring or capturing.\n2. **Height vs font size** — `getHeight(id) >= getTextFontSizes(id)[0] * 1.6`?\n3. **Position on page** — block within `[0, pageWidth] × [0, pageHeight]`?\n4. **Font URI valid** — was setFont called with the 3-arg form? Did the URI return 200?\n\nAfter `forceLoadResources`, every text block's `getFrameWidth(id)` returns its measured shaped width. If that's still `0`, the URI didn't deliver a renderable static TTF — swap to a known-good entry from the font table (design-rules.md §6.7).\n",
577
+ "docs/handbook/quirks.md": "# Handbook quirks — cryptic errors, runtime symptoms, diagnosis order\n\nThe handbook's quirk reference. CE.SDK errors are short and opaque. Two tables — error-message → cause, and runtime-symptom → workaround. Check both before bisecting; search this file with `query` using a fragment of the error message.\n\n## Cryptic errors\n\n| Error | Cause | Fix |\n| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |\n| `Cannot read properties of undefined (reading 'name')` | `setFont` called without the `typeface` object (3rd arg). | Pass `setFont(id, uri, { name, fonts: [{ uri, subFamily, weight, style }] })`. |\n| `The block doesn't have a background color.` | `setBackgroundColor` was called on a `page` (or other block kind that lacks the prop). | Pages don't accept `setBackgroundColor`. Use a full-bleed `graphic` block with a color fill instead. |\n| Text block exists with correct geometry but **glyphs don't draw** | Font URI returned HTTP 200 but the file isn't a renderable static TTF (variable, etc.). | Swap to a known-good static URI from the font table (design-rules.md §6.7). HEAD 200 ≠ renders. |\n| `Couldn't find property \"<name>\" on block.` | Property doesn't exist on this block type, or you guessed the property name. | Read `cesdk-guide/api/BlockAPI.md` for the exact property name and which block types accept it. |\n| `Invalid color reference …` | A color/fill value was passed without first calling `createFill`/`setColor`. | Always `createFill('color')` → `setColor(fill, 'fill/color/value', {r,g,b,a})` → `setFill(block, fill)`. |\n| `Setting the \"text/fontFileUri\" property directly is unsupported.` | Tried to bypass `setFont` via `setString(id, \"text/fontFileUri\", uri)`. | Always go through `setFont(id, uri, typeface)`. There is no shortcut. |\n\nWhen a new error type shows up, isolate it with **one** small `edit` (a single block, the failing call), not a series of trial-and-error rebuilds. The `cesdk-guide/api/BlockAPI.md` reference is the markdown explainer; for the authoritative TypeScript signature, read `cesdk-api/index.d.ts`.\n\n## Runtime quirks\n\nThese are **runtime semantics that don't show up in the .d.ts**. Check this list before re-bisecting.\n\n| Symptom | What's happening | Workaround |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `setFont(id, newUri, …)` doesn't change the rendered font on an existing block. | Engine treats two typeface objects with the same `name` as identical and skips the swap. | Use a unique `name` per swap (e.g. `\"Playfair Display v2\"`), OR delete and recreate that single block with the new font. |\n| Text doesn't reflow after `setFont` / `setTextFontSize`. Old glyph layout sticks. | The text shape is cached and only invalidated when the string content changes. | After the font/size change, force a re-shape: `engine.block.setString(id, \"text/text\", engine.block.getString(id, \"text/text\"))`. |\n| Text block exists with the right font, string, size, and position — but glyphs don't draw. | Block height is too small for the font's ascender + descender extent. The engine silently drops the render rather than overflowing or clipping. | Set height ≥ `fontSize × 1.6` for sans-serif body, `≥ 1.8` (better 2.0) for display serifs (Playfair, Cormorant) and scripts (Great Vibes, Caveat). Or use Auto + getFrameWidth (SKILL.md §4). |\n| `getWidth(id)` / `getHeight(id)` return `0` on an Auto-sized text block. Tempting to read this as \"font didn't load → text broken\". | `getWidth`/`getHeight` return what you set; in Auto mode that's `0`. They are **not** measurement APIs and **not** a font-load signal. | Use `engine.block.getFrameWidth(id)` / `getFrameHeight(id)` for the measured shaped extent. `getFrameWidth(id) > 0` is the actual \"font loaded + shaped\" probe. |\n| Pages attached via `engine.block.appendChild(scene, page)` don't show up, render at the wrong position, or break multi-page layout. Or `findByType(\"stack\")` returns `[]` and `appendChild(stack, page)` errors with `parent: Expected number, received undefined`. | `engine.scene.create()` (no argument) creates a scene WITHOUT a stack. Pages must attach to a stack, not the scene root. | Always pass the layout type: `engine.scene.create(\"VerticalStack\")` (or `\"HorizontalStack\"`). Then `engine.block.findByType(\"stack\")[0]` returns the auto-created stack, and `engine.block.appendChild(stack, page)` works. The hierarchy is always **scene → stack → page**. |\n| Anthropic returns `400 ... image dimensions exceed max allowed size for many-image requests: 2000 pixels`. | An image from something other than `preview` (e.g. an image your edit `code` returned inline) exceeded 2000 px on a dimension. `preview` itself is capped at 0.25 megapixels and 1568 px per dimension, so it can no longer trigger this. | Check images your own `code` returns inline; keep them under 2000 px per dimension. For seeing your work, prefer `preview` on a single page — it is always within limits. |\n| Accent shape covers text it should sit behind (pill hides headline; frame hides photo edge). | Child block created and appended AFTER the block it should sit under. `appendChild` places later children on top — there is no z-index property. | Create the background shape first, append it, THEN create+append the foreground text. To fix an existing scene without destroying anything: `engine.block.insertChild(page, bgId, 0)` to push back-most, or insert before a sibling via `getChildren(page).indexOf(siblingId)`. See SKILL.md §4 \"Z-order via child order\". |\n\n<!-- feature:video -->\n\n| A track child's `setTimeOffset` \"doesn't stick\" — a video/shape renders at the wrong moment, or clips with deliberately overlapping offsets (for a crossfade) instead play back-to-back with a hard cut and no overlap. | `track/automaticallyManageBlockOffsets` defaults to `true`. The track auto-arranges its children back-to-back by cumulative duration and silently overwrites any manual `setTimeOffset` — no error. Coincidentally correct with a single child at offset 0, which is why it hides until you need custom/overlapping timing. | `engine.block.setBool(track, 'track/automaticallyManageBlockOffsets', false)` right after creating the track, before setting any child's `setTimeOffset`. Verify with `engine.block.getTimeOffset(child)` if in doubt. |\n\n<!-- /feature:video -->\n\n## Diagnosis order — text doesn't render\n\nCheap → expensive:\n\n1. **Did you await fonts?** — `await engine.block.forceLoadResources([page])` inside the same edit before measuring. (Capturing is covered: `preview`/`export`/`save` and the commit itself run this barrier server-side.)\n2. **Height vs font size** — `getHeight(id) >= getTextFontSizes(id)[0] * 1.6`?\n3. **Position on page** — block within `[0, pageWidth] × [0, pageHeight]`?\n4. **Font URI valid** — was setFont called with the 3-arg form? Did the URI return 200?\n\nAfter `forceLoadResources`, every text block's `getFrameWidth(id)` returns its measured shaped width. If that's still `0`, the URI didn't deliver a renderable static TTF — swap to a known-good entry from the font table (design-rules.md §6.7).\n",
578
578
  "docs/judge/SKILL.md": "---\nname: judge\ndescription: >-\n Use to quality-gate a design before reporting it done — the mandatory step 5 of the\n handbook Loop; when the user asks for a design review, score, audit, or QA; or when a\n design needs remediation after a failed review. Scores the rendered design on 7 axes\n (baseline, hierarchy, typography, color, composition, craft, localization) against the\n handbook §6 rubric: every applicable axis must reach 8/10 — no averaging, one weak axis\n fails the design. Reports a per-axis scorecard with violations and concrete fixes, then\n drives the remediate → re-preview → re-judge loop until the gate passes.\n---\n\n# judge — the build → judge → remediate loop\n\nYou are the design's QA judge. Judging is scored against what you SEE in `preview` renders of the **latest revision** of the current design — never against the code that produced it, and never from memory. If you are unsure which revision is current, re-ground via `list()` / `history({ revision })` first (handbook §1).\n\n## The 7 axes\n\nScore each axis 0–10 against the render. `baseline` is the execution floor; the other six map to handbook §6 sections:\n\n| Axis | What it scores | Rubric |\n| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |\n| **baseline** | Nothing broken: no overflow, no blank/placeholder text, no tofu (□), no clipped ascenders/descenders, nothing off-canvas, no unintended overlap | §6.1.7, §6.4 |\n| **hierarchy** | One dominant element; unambiguous reading order; ≈3 (≤4) levels; rank carried by size > weight > color > spacing > position | §6.3 |\n| **typography** | Families/weights, scale bands, leading (incl. the content-area conversion and diacritic/non-Latin rules), measure, rag, tracking, micro-typography | §6.1 (checklist §6.1.10) |\n| **color** | Palette ≤ 3 + neutrals; exactly one consistent accent; contrast on the actual background | §6.2 |\n| **composition** | Margins 8–12% of the shortest dimension; shared grid; spacing ladder; balanced whitespace — no dead void; nothing off-canvas | §6.4 |\n| **craft** | Chips/pills/badges built per the chip system; intentional crops; clean decoration; consistent component geometry | §6.5 (checklist §6.5.2) |\n| **localization** | Localized variants only: correct per-language typography, no drift from the source layout, RTL mirrored, script-correct fonts | §6.6 + the `localize` skill |\n\n## The gate\n\n**Every applicable axis ≥ 8. No averaging. N/A = 10.** One axis at 7 fails the design. An axis is N/A only when it genuinely cannot apply (e.g. `localization` on a single-language design) — score it 10 and mark it N/A in the scorecard.\n\nThe caller may override the default: narrow the axes (\"judge typography and composition only\") or move the threshold (\"pass ≥ 9\"). Apply the override for that run; otherwise the full gate is the default.\n\n## The loop\n\n1. **Preview** — `preview` every page that matters of the latest revision. Judging an old render is judging the wrong design. A render is invalidated by **content** changes, not by revision ids: if the latest revision is content-identical to one you already rendered this conversation (a no-op edit, a diagnostic/read-only pass, a rename), that render is still current — reuse it instead of re-rendering.\n2. **Judge** — score each applicable axis against the render. For `typography` and `craft`, dry-run the §6.1.10 and §6.5.2 checklists line by line (read them via skill({ name: 'handbook', path: 'design-rules.md' })); any \"no\" on an applicable line caps that axis below 8.\n3. **Scorecard** — report the result in this shape (conversational markdown; no files):\n\n | Axis | Score | Violations | Fix |\n | ----------- | ----- | ---------------------------------------------------------------- | ---------------------------------------------------- |\n | typography | 6 | all-caps eyebrow untracked (§6.1.6); body leading unset (§6.1.3) | set letterSpacing 0.12; set converted lineHeight 1.5 |\n | composition | 8 | — | — |\n\n One row per applicable axis, N/A rows marked. Below the table: the verdict (`PASS` / `FAIL — remediating`) and, on failure, the remediation order (worst axis first).\n\n4. **Remediate** — apply the fixes via `edit` on the latest revision. Remediation is surgical: fix the flagged blocks in place (handbook §5 \"Prefer surgical fixes\"); respect the reshape trap (line-height/font changes → reshape → re-apply every color/style range, §6.1.3). Never rebuild the scene to fix a spacing violation.\n5. **Re-preview → re-judge** — repeat from step 1 until the gate passes. Then report done, with the final scorecard.\n\n## Judging discipline\n\n- **Judge the render, not the intent.** A rule satisfied in code but broken on screen is broken.\n- **Be harsh on your own work.** The common failure is scoring 8 by charity. If you hesitated, it is a 7.\n- **Score honestly on partial remediation.** If a violation cannot be fixed without re-laying-out the design, say so and recommend the `resize` skill (a layout problem) or a rebuild — do not inflate the score.\n- **The brief wins where the rubric is silent**; the rubric wins on hard-threshold conflicts (§6 intro).\n",
579
579
  "docs/localize/SKILL.md": "---\nname: localize\ndescription: >-\n Use when creating or fixing translated / localized variants of an existing CoDesign / CE.SDK\n design (e.g. de/ja/ru/ar editions); when a localized edition's text blocks or\n decorative elements have drifted, re-centered, or resized away from the source layout; when\n translated copy is tofu, overflowing, mis-tracked, or the wrong font after a language swap; or\n when loading a non-Latin script (Cyrillic / CJK / Arabic RTL / Vietnamese) font into a scene.\n---\n\n# localize\n\nPoint this skill at the current design and the target locales, and it produces or repairs the localized editions. It operates on the **latest revision of the current design** (re-ground via `list()` / `history()` if unsure — handbook §1); each locale lands as its own revision, forked from the source. Single home for localizing CoDesign designs: the **operational workflow**, the **font-loading mechanics**, and the **per-language quality rules**. It builds on two things it does _not_ restate: handbook §6.1 (general typographic thresholds — leading, tracking, all-caps, the content-area line-height conversion (`getFontMetrics`), smart punctuation, reshape-recolor) and handbook §6.7 (general Latin font mechanics — `setFont` single-URI, static-vs-variable, blank-glyph diagnosis). This skill is the non-Latin / localization extension of both.\n\n**Scoring:** localizing is judged with the `judge` skill — every applicable axis ≥ 8, no averaging, N/A = 10. A single tofu glyph, an overflow after expansion, un-mirrored RTL, or a block that drifted off the source grid fails the edition (the `localization` axis).\n\n## Reference files (load on demand)\n\nThis `SKILL.md` holds the always-needed spine: inputs, core principle, workflow, block\nclassification, and the snap-to-reference routine. Two situational bodies of rules live beside it\nand are pulled in only when the task needs them:\n\n- **`reference/font-loading.md`** — getting a non-Latin / non-handbook face to render (jsDelivr\n picks, the variable-bold trap, `setFont` gotchas, the silent-sans failure). Read it whenever the\n target locale needs a Cyrillic / CJK / Arabic / Vietnamese font, or copy came out tofu / wrong\n face.\n- **`reference/language-rules.md`** — the cross-language spine: preserve-vs-localize, translation\n quality, layout integrity after translation, and the localization pass checklist.\n- **`reference/<family>.md`** — per-script-family conventions, numeric leading bands, and\n quotation marks, one file per family: `latin-expansion.md` (de/fr/es…), `arabic.md`, `cjk.md`\n (ja/zh/ko), `vietnamese.md`, `cyrillic.md` (ru…). Read the file(s) for the **target locale(s)**\n you're building — or pull just the rule you need:\n `skill({ name: 'localize', query: 'leading', path: 'reference/arabic.md' })`.\n\n## Inputs — what to localize, and into what\n\n**1. The source (reference)** — the latest revision of the current design in its source language (usually `en`). That revision's geometry is the grid every target locale snaps to. Never localize _from_ a non-reference locale — always fork each locale from the source revision. If the user means an earlier design, find it via `list()` / `history()` first.\n\n**2. The locales** — the target languages, e.g. `de ja ru ar`. If omitted, ask. Announce the resolved set (\"1 design × 3 locales\") before building.\n\n**Per locale, then:** run the Workflow below (fork the source revision, or reuse+snap an existing localized revision). Give each locale's revision a `note` naming the locale. Preview every locale before reporting done.\n\n## Core principle — localize, then snap back to the reference\n\nLocalizing is **surgical**: change the words + the font the new script forces, and **nothing\nelse**. Then **snap every block back to the reference variant's geometry** so the localized\nedition is positionally identical to the source.\n\n- The **reference** is the source-locale variant (usually `en`) **of the same size**. Each size\n has its own reference grid.\n- **Never re-center or re-size a block per variant \"to fit better.\"** That independent re-fitting\n is exactly what makes a localized set drift — the eyebrow creeps up, the quote grows, the rule\n and footer slide down, and the editions stop matching. If there is room for the translated text\n at the reference position/size, keep the reference position/size.\n- Deviate from the reference **only** when the translated text genuinely overflows (expansion\n languages) — then shrink _that block's_ font within its band; do not move its neighbours.\n\n## When to use\n\n- Producing new locale editions (de/ja/ru/ar/…) of a finished design, across one or more sizes.\n- Fixing a localized set whose blocks drifted / re-centered / resized vs the source.\n- Post-translation breakage: tofu, overflow, wrong tracking, Latin-looking sans where a script\n font should be.\n- Loading a non-Latin or non-handbook typeface into a scene.\n\n**Not for:** the original design build (that's the handbook + §6); pure copy edits in the\nsame language. To change\nthe **size/format** — re-compose a design for a new aspect rather than translate it — use the **resize** skill\n(localize snaps geometry; resize re-derives it).\n\n## Reuse an existing localized build vs build fresh\n\n- **The workspace already has the locale** (an earlier pass built it — check `history()`): **reuse its\n build** — the translated copy, script fonts, and RTL are already solved — but still **snap it to\n the reference geometry**. Earlier builds commonly drift; reuse the assets, fix the positions.\n- **No existing build:** fork the reference revision and swap text + font.\n\nEither way the last step is the same: every block lands on the reference grid.\n\n## Workflow\n\n1. **Capture the reference geometry.** Load the source-locale variant of that size and read, per\n block: `x, y, width, widthMode, heightMode, fontSize (Pixel), lineHeight, letterSpacing,\nalignment`, plus page `w/h`, the ghost's anchor (left / centered), and the rule position.\n2. **Load the target** — the localized scene to fix, or fork the reference for a fresh build.\n3. **Classify blocks by role** (see below).\n4. **Apply the reference geometry** to each corresponding block; keep the localized **text** and\n the **script font** (for a non-Latin/non-handbook face see `reference/font-loading.md`). Set\n per-locale: `letterSpacing = 0` for CJK & Arabic; RTL ghost mirrored to the right; alignment as\n the reference (centered-symmetric stays centered — see `reference/arabic.md`).\n5. **Reconcile ghost count.** Keep the reference's number of decorative glyphs. If the localized\n scene has extra ones (e.g. an Arabic closing-quote bookend the English lacks), **destroy the\n extras**; mirror the kept one for RTL.\n6. **Overflow guard.** After setting the reference font size, measure the quote's frame height; if\n it collides with the element below, step the font size down (only that block) until it fits.\n Never move neighbours to make room.\n7. **Reshape, then re-apply colors.** A font/size/line-height change needs a reshape\n (`setString` with the same text) to take visual effect — and reshape **wipes per-character\n color**. Capture each block's color first, re-apply after.\n8. **`forceLoadResources` → `preview`.** One preview per locale — tofu / RTL / overflow /\n position match are only visible in the render.\n\nThen apply the per-locale quality rules in `reference/language-rules.md` **and the family\nfile(s) for the target locale(s)** (`reference/latin-expansion.md`, `arabic.md`, `cjk.md`,\n`vietnamese.md`, `cyrillic.md`), and dry-run the localization pass checklist against the render\nbefore reporting done.\n\n## Block-role classification\n\nClassify by measurable properties, not hard-coded ids (ids are session-scoped). For a\nquote-card family the roles are:\n\n- **ghost** (oversized decorative quote) — largest font size **and** trimmed string length ≤ 2.\n A design may have **more than one** (opening + closing bookend); collect all.\n- **footer / handle** — text containing `@`.\n- **quote / headline** — the largest remaining text after removing ghosts + footer.\n- **eyebrow** vs **attribution** — the two small texts; eyebrow sits **above** the quote,\n attribution **below**.\n- **rule / divider** — the graphic (non-text) block.\n\nRoles are design-specific. The general rule: **map each localized block to its reference block by\nrole, or by index across the shared fork lineage** (localized variants forked from the same base\nkeep the same child order), then restore that reference block's geometry.\n\n## The snap-to-reference routine\n\nA single CoDesign `edit` body. Fill the `EN` reference geometry (from step 1) and the locale\nflags, then it classifies, restores geometry, reconciles ghosts, applies per-locale tracking /\nRTL mirroring, guards overflow, and re-applies colors across the reshape. Adapt the role\nclassification to the design.\n\n```js\n// --- config: reference geometry (read from the source-locale variant of THIS size) ---\nconst PW = 1080,\n PH = 1350; // page dims\nconst EN = {\n ghost: { x: 70, y: 55, fs: 300, lh: 1 }, // Auto-sized; keep size+lh, reposition\n eyebrow: { x: 110, y: 395, w: 860, fs: 16, lh: 1, ls: 0.25 },\n quote: { x: 110, y: 529, w: 860, fs: 62, lh: 0.85 }, // ls always 0\n rule: { x: 504, y: 930, w: 72 }, // graphic\n attribution: { x: 110, y: 960, w: 860, fs: 16, lh: 1, ls: 0.25 },\n footer: { x: 110, y: 1225, w: 860, fs: 15, lh: 1, ls: 0.12 }\n};\nconst RTL = false; // true for Arabic\nconst trackOK = true; // false for CJK/Arabic → force letterSpacing 0 on eyebrow/attr/footer\nconst ghostAnchor = 'left'; // 'left' (post/square) mirrors to the right for RTL; 'center' (story) stays centered\nconst belowQuoteY = EN.rule.y; // element the quote must stay above\n\nconst page = engine.scene.getPages()[0];\nconst all = [];\n(function walk(id) {\n all.push(id);\n for (const c of engine.block.getChildren(id)) walk(c);\n})(page);\nconst blocks = all.filter((id) => id !== page);\nconst texts = blocks.filter((id) => engine.block.getType(id).includes('text'));\nconst graphic = blocks.find((id) =>\n engine.block.getType(id).includes('graphic')\n);\nawait engine.block.forceLoadResources([page]);\n\nconst meta = (id) => ({\n id,\n s: engine.block.getString(id, 'text/text'),\n fs: engine.block.getTextFontSizes(id, { unit: 'Pixel' })[0],\n y: engine.block.getPositionY(id)\n});\nconst tm = texts.map(meta);\n\n// classify\nconst ghosts = tm.filter((t) => t.fs >= 150 && t.s.trim().length <= 2);\nlet ghost =\n ghosts.find((t) => t.s.includes('“')) ||\n ghosts.slice().sort((a, b) => a.y - b.y)[0];\nfor (const g of ghosts.filter((t) => t !== ghost)) engine.block.destroy(g.id); // drop extra bookend glyphs\nconst footer = tm.find((t) => t.s.includes('@'));\nconst rest = tm.filter((t) => !ghosts.includes(t) && t !== footer);\nconst quote = rest.reduce((a, b) => (b.fs > a.fs ? b : a));\nconst others = rest.filter((t) => t !== quote);\nconst eyebrow = others.reduce((a, b) => (b.y < a.y ? b : a));\nconst attribution = others.find((t) => t !== eyebrow);\n\n// capture colors (reshape wipes per-character styling)\nconst colorOf = (id) => {\n try {\n return engine.block.getTextColors(id)[0];\n } catch (e) {\n return null;\n }\n};\nconst cols = Object.fromEntries(\n [ghost, eyebrow, quote, attribution, footer].map((t) => [t.id, colorOf(t.id)])\n);\nconst reshape = (id) =>\n engine.block.setString(\n id,\n 'text/text',\n engine.block.getString(id, 'text/text')\n );\nconst applyText = (t, g, ls) => {\n engine.block.setWidthMode(t.id, 'Absolute');\n engine.block.setWidth(t.id, g.w);\n engine.block.setHeightMode(t.id, 'Auto');\n engine.block.setTextFontSize(t.id, g.fs, { unit: 'Pixel' });\n engine.block.setFloat(t.id, 'text/lineHeight', g.lh);\n engine.block.setFloat(t.id, 'text/letterSpacing', ls);\n engine.block.setEnum(t.id, 'text/horizontalAlignment', 'Center');\n reshape(t.id);\n};\n\nengine.block.setTextFontSize(ghost.id, EN.ghost.fs, { unit: 'Pixel' });\nengine.block.setFloat(ghost.id, 'text/lineHeight', EN.ghost.lh);\nreshape(ghost.id);\napplyText(eyebrow, EN.eyebrow, trackOK ? EN.eyebrow.ls : 0);\napplyText(attribution, EN.attribution, trackOK ? EN.attribution.ls : 0);\napplyText(footer, EN.footer, trackOK ? EN.footer.ls : 0);\napplyText(quote, EN.quote, 0);\nengine.block.setWidth(graphic, EN.rule.w);\nfor (const t of [ghost, eyebrow, quote, attribution, footer])\n if (cols[t.id]) engine.block.setTextColor(t.id, cols[t.id]);\nawait engine.block.forceLoadResources([page]);\n\n// position (ghost measured for mirroring; others straight from EN)\nconst gw = engine.block.getFrameWidth(ghost.id);\nengine.block.setPositionX(\n ghost.id,\n ghostAnchor === 'center'\n ? (PW - gw) / 2\n : RTL\n ? PW - EN.ghost.x - gw\n : EN.ghost.x\n);\nengine.block.setPositionY(ghost.id, EN.ghost.y);\nfor (const [t, g] of [\n [eyebrow, EN.eyebrow],\n [quote, EN.quote],\n [attribution, EN.attribution],\n [footer, EN.footer]\n]) {\n engine.block.setPositionX(t.id, g.x);\n engine.block.setPositionY(t.id, g.y);\n}\nengine.block.setPositionX(graphic, EN.rule.x);\nengine.block.setPositionY(graphic, EN.rule.y);\n\n// overflow guard — shrink ONLY the quote, never move neighbours\nawait engine.block.forceLoadResources([page]);\nlet qfs = EN.quote.fs;\nconst avail = belowQuoteY - EN.quote.y - 16;\nwhile (engine.block.getFrameHeight(quote.id) > avail && qfs > 44) {\n qfs -= 2;\n engine.block.setTextFontSize(quote.id, qfs, { unit: 'Pixel' });\n reshape(quote.id);\n if (cols[quote.id]) engine.block.setTextColor(quote.id, cols[quote.id]);\n await engine.block.forceLoadResources([page]);\n}\nreturn { type: 'text', text: JSON.stringify({ page, qfs }, null, 1) };\n```\n\nReference-capture read (run on the source variant to fill `EN`):\n\n```js\nconst page = engine.scene.getPages()[0];\nconst out = [];\n(function walk(id) {\n const t = engine.block.getType(id),\n isText = t.includes('text');\n const r = {\n t: t.split('/').pop(),\n x: Math.round(engine.block.getPositionX(id)),\n y: Math.round(engine.block.getPositionY(id)),\n w: Math.round(engine.block.getWidth(id)),\n wm: engine.block.getWidthMode(id)\n };\n if (isText) {\n r.s = engine.block.getString(id, 'text/text').slice(0, 14);\n r.fs = Math.round(engine.block.getTextFontSizes(id, { unit: 'Pixel' })[0]);\n r.lh = +engine.block.getFloat(id, 'text/lineHeight').toFixed(2);\n r.ls = +engine.block.getFloat(id, 'text/letterSpacing').toFixed(2);\n }\n if (id !== page) out.push(r);\n for (const c of engine.block.getChildren(id)) walk(c);\n})(page);\nreturn {\n type: 'text',\n text: JSON.stringify({\n pw: engine.block.getWidth(page),\n ph: engine.block.getHeight(page),\n b: out\n })\n};\n```\n\n## Common mistakes / red flags\n\n- **Per-variant re-centering / re-sizing** → drift. If you're re-centering a block because the\n translation is a different length, STOP — snap it to the reference position instead.\n- **\"Bold\" from a variable TTF** → renders Regular. Use a static Bold file (`reference/font-loading.md`).\n- **Reshape wiped the accent color** → capture colors before reshape, re-apply after.\n- **Letter-spacing on Arabic** → severs joins. `letterSpacing = 0`.\n- **Plausible page, wrong font** → silent sans fallback / tofu. Preview every locale.\n- **Extra or missing decorative glyph** vs the reference → reconcile the count.\n- **Non-native export size** → export the page at its own pixel dimensions.\n\n## Cross-references\n\n- **`reference/font-loading.md`** — non-Latin / non-handbook font mechanics (this skill's font body).\n- **`reference/language-rules.md`** — cross-language quality rules + pass checklist.\n- **`reference/latin-expansion.md` / `arabic.md` / `cjk.md` / `vietnamese.md` / `cyrillic.md`** — per-script-family conventions, leading bands, quotation marks.\n- handbook §6.1 — general typographic thresholds (leading, tracking, all-caps & diacritic leading, the content-area line-height conversion (`getFontMetrics`), smart punctuation, reshape-then-recolor); §6.7 — the Latin font baseline; and the handbook's reshape / `forceLoadResources` / dirty-on-throw runtime quirks.\n- **`judge`** — the acceptance gate (the `localization` axis scores this skill's output).\n- **`resize`** — the sibling skill for changing size/format (re-compose for a new aspect).\n",
580
580
  "docs/localize/reference/arabic.md": "# Arabic (RTL)\n\nFamily file for the `localize` skill: conventions, leading bands, and quotation marks for Arabic.\nEvery leading value here is a **visual** multiplier — apply the handbook §6.1.3 content-area\nconversion (divide by the font's content area via `engine.editor.getFontMetrics`) before setting\n`text/lineHeight`.\n\n## Conventions\n\n- **MSA** (Modern Standard Arabic), not dialect; **Western numerals**.\n- **Mirror the layout, not just the text.** Right-align running text; flow directional &\n decorative elements right-to-left (a leading decorative quote glyph → mirror `“`→`”`/`»`). A\n **centered, symmetric** composition is already mirror-invariant — it **stays centered**; only\n text direction and the ghost's side change (the snap routine mirrors a `ghostAnchor:'left'`\n ghost to the right, leaves a `'center'` ghost centered).\n- **Never letter-space Arabic** — joined script; tracking severs the joins. `text/letterSpacing = 0`\n (handbook §6.1.6).\n- Keep names/brands as-is by default (Latin runs stay LTR via bidi); transliteration optional.\n\n## Leading\n\nTall ascenders + deep descenders demand more vertical room than Latin — set explicitly:\n\n- **Body 1.6–1.8**\n- **Display / headline 1.3–1.5**\n\nNo all-caps concept applies (Arabic has no case).\n\n## Quotation marks\n\n| Locale | Primary quotes | Notes |\n| ------ | -------------- | ------------------------------ |\n| AR | « » | RTL; opening mark on the right |\n\nSecondary: „ “.\n\n## Fonts\n\nUse a script face with full Arabic coverage — **Amiri** is the verified pick; loading mechanics\nand the variable-bold trap are in `font-loading.md`. A Latin face silently falling back to a sans\nis the classic failure — `preview` every Arabic edition.\n\n## Cross-references\n\n- `language-rules.md` — preserve-vs-localize, translation quality, layout integrity, the RTL rows\n of the pass checklist.\n- handbook §6.1.5 — mirrored alignment; §6.1.6 — the no-tracking rule; §6.1.3 — the content-area\n conversion.\n- `judge` — the `localization` axis scores mirroring, bidi, and tofu.\n",