From motley
Create and validate BrandConfig JSON files that define the complete visual identity for HTML slide presentations. Use when setting up a new brand, onboarding a new client, or debugging styling issues in the frontend-slides system.
How this skill is triggered — by the user, by Claude, or both
Slash command
/motley:brand-config-authoringThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create BrandConfig JSON files that define the complete visual identity for the frontend-slides presentation system. The server-side enrichment pipeline uses the BrandConfig to wrap body-only HTML (generated by an LLM) with full CSS, JS, fonts, logos, and chrome.
Create BrandConfig JSON files that define the complete visual identity for the frontend-slides presentation system. The server-side enrichment pipeline uses the BrandConfig to wrap body-only HTML (generated by an LLM) with full CSS, JS, fonts, logos, and chrome.
storyline/storyline/api/schemas/report_style_schemas.py (source of truth)storyline/playground/styles/samplead/samplead-save-style-args.json (most complete)storyline/playground/styles/evalart/evalart-save-style-args.jsonstoryline/playground/styles/cledara/cledara-save-style-args.jsonThese are real bugs discovered during development. Every one of them caused broken presentations in production. Read this section BEFORE writing any config.
Wrong -- bare reset with no font or scroll-snap declarations:
* { margin: 0; padding: 0; box-sizing: border-box; }
Right -- must include html scroll-snap, body font, and heading font rules at minimum:
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow-x: hidden; }
html { scroll-snap-type: y mandatory; scroll-behavior: smooth; }
.slide { width: 100vw; height: 100vh; height: 100dvh; overflow: hidden;
scroll-snap-align: start; display: flex; flex-direction: column;
position: relative; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
html { scroll-behavior: auto; }
}
body { font-family: var(--font-body); font-size: var(--body-size);
line-height: 1.4; color: var(--text-primary); background: var(--bg-deep);
overflow-x: hidden; }
h1, h2, h3 { font-family: var(--font-display); font-weight: 700; line-height: 1.15; }
h1 { font-size: var(--title-size); }
h2 { font-size: var(--h2-size); }
Without this, no fonts are applied and text renders unstyled.
Wrong -- css_block set to null:
{
"name": "Content Slide",
"css_class": "slide slide-content",
"css_block": null
}
Right -- every slide type MUST have a populated css_block with at minimum its background and text styling:
{
"name": "Content Slide",
"css_class": "slide slide-content",
"css_block": ".slide-content { background: var(--bg-white); color: var(--text-primary); padding: var(--sp); } .slide-content h2 { color: var(--brand-primary); margin-bottom: var(--sp-sm); }"
}
Without this, slides have no background, no text colors, no layout.
Wrong -- descriptive string:
"stagger_delay": "0.09s per child (nth-child(1): 0.04s, nth-child(2): 0.13s, ...)"
Right -- a bare CSS duration value only:
"stagger_delay": "0.09s"
The server parses this as a float and generates nth-child rules automatically. Anything other than a pattern matching ^\d+(\.\d+)?(s|ms)$ will crash the validator. Valid examples: "0.1s", "100ms", "0.09s".
Wrong -- flex-based centering without explicit height:
.chart-container { flex: 1; min-height: 0; display: flex;
align-items: center; justify-content: center; }
Right -- explicit height, no flex centering:
.chart-container { width: 100%; height: min(60vh, 500px); position: relative; }
eCharts needs an explicit pixel/viewport height to render its canvas. Flex centering conflicts with the canvas positioning.
Wrong -- auto overflow causes scrollbars inside slides:
.tbl-wrap { overflow-y: auto; overflow-x: auto; }
Right -- hidden overflow, size tables to fit:
.tbl-wrap { overflow: hidden; }
Tables should be sized to fit the slide. If a table does not fit, reduce rows or split across slides. Never scroll.
Every CSS variable used anywhere in any css_block must be defined in shared_css.brand_tokens_css. If a slide type css_block references var(--font-body), var(--bg-gradient), or var(--text-primary), and those variables are not in brand_tokens_css, the styling breaks silently -- CSS treats undefined variables as empty strings.
Checklist: After writing the config, search all css_block values, reset_and_base, utility_classes_css, and responsive_overrides_css for var(-- references. Every variable name found must appear in brand_tokens_css.
If javascript.controller_js is null, the server uses a built-in default navigation controller. This is fine and often preferred.
If you DO provide a custom controller_js, it must be a complete, self-contained script that handles:
.visible class to slideschrome.has_progress_bar is true)chrome.has_nav_dots is true)Wrong -- hardcoded fill colors prevent color switching on different backgrounds:
<path d="M10 20..." fill="#6D22E0"/>
Right -- use currentColor so CSS controls the fill:
<path d="M10 20..." fill="currentColor"/>
Then define color variants in CSS:
.logo.on-light { color: #6D22E0; }
.logo.on-dark { color: rgba(255,255,255,0.9); }
Set uses_current_color: true in the logo config. If the SVG has multiple colors that cannot be simplified to currentColor, set uses_current_color: false and skip color_variants.
The server resolves data-table-block containers by injecting <table> elements with the brand's table_css_class. The table CSS must use colors that are readable against the background where tables actually render — NOT the global theme colors.
Wrong — dark-theme brand using global color tokens for tables that render on a white panel:
.dtbl th { color: var(--text-muted); } /* --text-muted is rgba(255,255,255,0.6) → invisible on white */
.dtbl td { color: var(--text-primary); } /* --text-primary is #ffffff → invisible on white */
Right — use colors appropriate for the table's actual background context:
.dtbl th { color: #6B7280; } /* gray, readable on white */
.dtbl td { color: var(--bg-deep); } /* dark brand color, readable on white */
.dtbl td { border-bottom: 1px solid #E5E7EB; } /* light gray border on white */
Rule: If your brand is dark-themed (global --text-primary is white/light) but tables appear on light panels (e.g. inside .content-card on a white .split-light panel), the table CSS must use dark text colors — either hardcoded hex values or dark-side brand tokens like var(--bg-deep). Check where tables will actually be placed and style accordingly.
The LLM sees the css_block in the slim config and SHOULD use the CSS classes defined there, but it tends to write inline styles instead. The html_template field on each slide type is the primary way to steer the LLM toward correct class usage.
Wrong -- template with no class examples:
<section class="slide slide-data"><div class="content">...</div></section>
Right -- template showing exact class names for tables, charts, metrics:
<section class="slide wslide" id="slide-N">
<div class="topbar"><div class="logo on-white rv"><!-- logo --></div><span class="pg-num rv">03 / 07</span></div>
<div class="s-hdr rv"><span class="sect-bar"></span><span class="s-title">Section Title</span></div>
<div class="tbl-wrap rv">
<table class="dtbl">
<thead><tr><th>Name</th><th class="n">Value</th></tr></thead>
<tbody><tr><td>Row</td><td class="n hi">42</td></tr></tbody>
</table>
</div>
</section>
The JSON file that gets uploaded has this outer structure:
{
"scope": "organization",
"style_name": "my-brand",
"payload": {
"brand_name": "...",
"brand_description": "...",
...all BrandConfig fields...
}
}
scope is either "organization" (org-wide default) or "user" (user-specific override).
| Field | Type | Required | Description |
|---|---|---|---|
brand_name | string | Yes | Display name of the brand |
brand_description | string | Yes | 1-2 sentence description of the visual style |
colors | ColorPalette | Yes | Complete color system |
typography | Typography | Yes | Font definitions and settings |
logo | LogoConfig | Yes | SVG logo with color/size variants |
slide_types | SlideTypeSystem | Yes | All slide archetypes |
decorative_elements | list | No | Brand-specific visual motifs (waves, tiles, grids) |
animations | AnimationConfig | Yes | Allowed animation patterns |
footer | FooterConfig | Yes | Footer appearance |
chrome | ChromeConfig | No | Navigation chrome (progress bar, nav dots, topbar) |
shared_css | SharedCSS | No | CSS that applies across all slides |
javascript | JavaScriptConfig | No | Navigation controller and counter animations |
workflow | WorkflowConfig | No | Which workflow phases are enabled |
default_chart_color_scheme | string | No | Named color scheme for eCharts |
table_css_class | string | No | CSS class applied to server-resolved <table> elements (e.g. "dtbl", "data-table") |
forbidden_patterns | list[string] | No | Rules the LLM must never violate |
{
"tokens": [
{ "name": "brand-blue", "value": "#016FFF", "usage": "Primary brand color for headings and accents" },
{ "name": "text-primary", "value": "#1e1b4b", "usage": "Main body text color" },
{ "name": "bg-deep", "value": "#0A0F1C", "usage": "Dark slide backgrounds" }
],
"gradients": [
{ "name": "brand", "css_value": "linear-gradient(135deg, #016FFF 0%, #7C3AED 100%)", "usage": "Cover slide background" }
],
"gradient_direction_constraint": null
}
Each token becomes a CSS variable --<name> in brand_tokens_css. The usage field helps the LLM choose the right color for each context.
{
"fonts": [
{ "role": "display", "family": "'Space Grotesk', sans-serif", "weights": [700, 800], "source": "google_fonts" },
{ "role": "body", "family": "'Inter', sans-serif", "weights": [400, 500, 600], "source": "google_fonts" },
{ "role": "mono", "family": "'JetBrains Mono', monospace", "weights": [400], "source": "google_fonts" }
],
"import_html": "<link href=\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700;800&family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">",
"heading_letter_spacing": "-0.022em",
"body_line_height": 1.62
}
role: "display" (headings), "body" (paragraph text), "mono" (code/data)source: "google_fonts", "fontshare", or "system"import_html: the <link> tag for loading the fonts. Set to null for system fonts.{
"primary_svg": "<svg width=\"80\" height=\"15\" viewBox=\"...\" fill=\"none\" xmlns=\"...\"><path d=\"...\" fill=\"currentColor\"/></svg>",
"icon_only_svg": null,
"uses_current_color": true,
"color_variants": [
{ "on_background": "light", "css_color": "var(--brand-blue)", "css_class": "on-light" },
{ "on_background": "dark", "css_color": "rgba(255,255,255,0.9)", "css_class": "on-dark" }
],
"size_variants": [
{ "context": "topbar", "css_width": "80px" },
{ "context": "cover", "css_width": "clamp(250px, 45vw, 550px)" },
{ "context": "closing", "css_width": "80px" }
],
"placement_css": null
}
primary_svg: full inline SVG markup, not a URL. Must use fill="currentColor" if uses_current_color is true (see Pitfall 8).color_variants: how the logo appears on light, dark, and gradient backgrounds. The css_class is used in HTML: <div class="logo on-light"><!-- logo --></div>.size_variants: width for each placement context (topbar, cover, closing, footer).{
"types": [
{
"name": "Cover",
"css_class": "slide slide-cover",
"background_kind": "gradient",
"background_value": "var(--grad-brand)",
"layout_description": "Full-bleed gradient background with centered logo and title",
"when_to_use": "Always the first slide",
"has_decorative_elements": false,
"has_footer": false,
"logo_placement": "center-top",
"logo_variant": "on-dark",
"css_block": ".slide-cover { background: var(--grad-brand); color: #fff; ... }",
"html_template": "<section class=\"slide slide-cover\" id=\"slide-1\">..."
}
],
"first_slide_type": "slide slide-cover",
"last_slide_type": "slide slide-closing"
}
Key rules:
first_slide_type and last_slide_type must match the css_class of a type in the types array.css_block (see Pitfall 2).background_kind: one of "solid_color", "gradient", "gradient_split", "white".html_template: example HTML that demonstrates proper class usage for the LLM (see Pitfall 9).[
{
"name": "Wave Footer",
"description": "An SVG wave that sits at the bottom of white slides",
"css_block": ".wave { position: absolute; bottom: 0; left: 0; width: 100%; ... }",
"html_template": "<div class=\"wave\"><!-- wave --></div>",
"variation_instructions": "Each wave must use a unique SVG gradient ID",
"applies_to_slide_types": ["slide wslide"]
}
]
applies_to_slide_types: list of css_class values this element should appear on. Empty list means all slides.html_template: must include the marker the server replaces (e.g. <!-- wave -->).{
"presets": [
{ "name": "Reveal Up", "css_class": "rv", "css_block": ".rv { opacity: 0; transform: translateY(40px); ... }" },
{ "name": "Reveal Left", "css_class": "rv-l", "css_block": ".rv-l { opacity: 0; transform: translateX(-40px); ... }" }
],
"stagger_delay": "0.09s",
"forbidden_effects": ["bounce", "spin", "3D transforms", "parallax scroll", "background video"],
"general_guidance": "All animations are reveal-on-scroll only, triggered by IntersectionObserver adding .visible class."
}
stagger_delay: MUST be a bare CSS duration like "0.1s" or "100ms" (see Pitfall 3).css_block on each preset: the full CSS rule. The server injects this; the LLM only needs the class name.forbidden_effects: the LLM is told to never use these.{
"kind": "svg_wave",
"text_content": null,
"css_block": ".wave { position: absolute; bottom: 0; ... }",
"html_template": "<div class=\"wave\"><!-- wave --></div>",
"variation_instructions": "Each wave needs a unique SVG gradient ID (wf1, wf2, wf3...)"
}
kind: "text" (static text footer), "svg_wave" (decorative SVG wave), or "none".{
"has_progress_bar": true,
"progress_bar_css": ".progress-bar { ... }",
"has_nav_dots": true,
"nav_dots_css": ".nav-dots { ... }",
"has_topbar": true,
"topbar_css": ".topbar { ... }",
"topbar_html": "<div class=\"topbar\">...</div>"
}
Boolean flags tell the server what chrome to inject. CSS/HTML fields define the styling. All are optional -- defaults are used if omitted.
{
"reset_and_base": "* { margin: 0; ... } html { scroll-snap-type: ... } body { font-family: ... } h1, h2, h3 { ... }",
"brand_tokens_css": ":root { --brand-blue: #016FFF; --text-primary: #1e1b4b; ... }",
"utility_classes_css": ".visually-hidden { ... }",
"responsive_overrides_css": "@media (max-height: 700px) { ... }"
}
reset_and_base: MUST include full typography setup (see Pitfall 1).brand_tokens_css: MUST define every CSS variable referenced anywhere in the config (see Pitfall 6).<style> block.{
"controller_js": null,
"has_counter_animation": true
}
controller_js: set to null to use the built-in default controller (see Pitfall 7). If provided, must be a complete navigation controller script.has_counter_animation: if true, the server adds counter animation JS for KPI numbers.{
"include_style_discovery": false,
"include_ppt_conversion": true,
"include_pdf_generation": false,
"include_inline_editing": true,
"output_path_template": "/sessions/.../mnt/outputs/<client-name>-<topic>.html",
"html_title_template": "<Client> -- <Topic> | BrandName",
"content_discovery_questions": [
"What is the title/subject of this report?",
"What are the key metrics to feature?",
"How many slides should this be?"
]
}
Controls which phases of the frontend-slides skill workflow are enabled for this brand.
[
"Never substitute fonts -- Brand Font only",
"Never modify the brand color tokens",
"Never allow scrolling within a slide",
"Cover slide must always be first, Closing slide must always be last"
]
These are passed to the LLM as hard constraints during HTML generation.
Collect from the client or brand guidelines:
currentColor fills)Create the color token list. At minimum you need:
clamp() for responsiveness)Write these as colors.tokens. Each token becomes a CSS variable --<name>.
If the brand uses gradients, define them in colors.gradients. Include the full CSS gradient value with direction and color stops.
Define fonts with roles (display, body, optionally mono). Generate the import_html link tag for Google Fonts or Fontshare. Set heading_letter_spacing and body_line_height.
Paste the full SVG inline as primary_svg. Convert hardcoded fill colors to currentColor (see Pitfall 8). Define color_variants for light and dark backgrounds, and size_variants for each placement context.
Plan 4-8 slide types. Typical set:
For each type, write:
css_block with all CSS rules (NEVER leave null -- see Pitfall 2)html_template showing proper class usage (see Pitfall 9)layout_description and when_to_use for the LLMreset_and_base: Start from the template in Pitfall 1. Include the .slide base rule, prefers-reduced-motion media query, body font declaration, and heading rules.
brand_tokens_css: Define a :root { } block with EVERY CSS variable referenced in any css_block (see Pitfall 6). Map color tokens to --<name> variables, add font-family variables (--font-display, --font-body), font-size variables (--title-size, --h2-size, --body-size), spacing variables, and gradient variables.
utility_classes_css: Optional helper classes (e.g. .visually-hidden).
responsive_overrides_css: Media queries for max-height: 700px, 600px, 500px.
Define 2-4 animation presets (typically reveal-up, reveal-left, reveal-right). Set stagger_delay to a simple duration like "0.1s" (see Pitfall 3). List forbidden_effects.
Choose footer kind ("svg_wave", "text", or "none"). If using svg_wave, provide the css_block and html_template with the <!-- wave --> marker.
Set chrome boolean flags and provide CSS for progress bar, nav dots, and topbar if needed.
If any slide type includes charts, make sure the css_block includes:
.chart-container { width: 100%; height: min(60vh, 500px); position: relative; }
Never use flex centering for chart containers (see Pitfall 4).
If any slide type includes tables:
table_css_class at the top level (e.g. "dtbl" or "data-table").dtbl, .dtbl th, .dtbl td)overflow: hidden on table wrappers (see Pitfall 5)The server resolves <div class="table-container" data-table-block="BLOCK_NAME"> containers by looking up the table block in the source document and injecting a <table class="<table_css_class>"> element with the rendered data.
Before uploading, verify:
shared_css.reset_and_base includes body font, html scroll-snap, heading rulescss_blockstagger_delay matches the pattern ^\d+(\.\d+)?(s|ms)$overflow: hiddentable_css_class is set and its CSS is defined in a slide type css_blockvar(--...) reference in any CSS field is defined in brand_tokens_cssfill="currentColor" (if uses_current_color is true)html_template fields demonstrate correct class usage for tables, charts, metricsfirst_slide_type and last_slide_type match a css_class in the types arrayWrap the BrandConfig payload in the file wrapper:
{
"scope": "organization",
"style_name": "my-brand",
"payload": { ... }
}
Upload via curl:
curl -sk -X POST \
"https://localhost:5173/api/v1/admin/style/upload?clerk_id=CLERK_ID" \
-H "Content-Type: application/json" \
-d @path/to/config.json
Replace CLERK_ID with the Clerk user ID for the target organization. The endpoint upserts -- if a style with the same (org, style_name) already exists, it is updated.
A valid BrandConfig must have at least:
brand_name and brand_descriptioncolors.tokenstypography.fonts (role "body")logo.primary_svg with inline SVGfirst_slide_type, one matching last_slide_typecss_blockanimations.presetsfooter.kind set to one of "text", "svg_wave", "none"shared_css.reset_and_base with full typography setupshared_css.brand_tokens_css defining all referenced CSS variablesSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub motleyai/motley-skills --plugin motley