Brand config authoring
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.From its SKILL.md
npx -y skills add MotleyAI/motley-skills --skill brand-config-authoringAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
23.7 KB, as published. Nobody here has run it
Brand Config Authoring
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.
Where things live
- Pydantic schema:
storyline/storyline/api/schemas/report_style_schemas.py(source of truth) - Working examples:
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.json
Common Pitfalls
These are real bugs discovered during development. Every one of them caused broken presentations in production. Read this section BEFORE writing any config.
Pitfall 1: reset_and_base missing body/heading typography
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.
Pitfall 2: Slide types with null css_block
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.
Pitfall 3: stagger_delay must be a simple CSS duration
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".
Pitfall 4: Chart containers need explicit height
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.
Pitfall 5: Table overflow must be hidden
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.
Pitfall 6: brand_tokens_css must define ALL referenced CSS variables
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.
Pitfall 7: controller_js can be null
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:
- IntersectionObserver to add
.visibleclass to slides - Keyboard navigation (arrow keys, Space, Page Up/Down)
- Touch and wheel navigation
- Progress bar updates (if
chrome.has_progress_baris true) - Nav dot updates (if
chrome.has_nav_dotsis true)
Pitfall 8: Logo SVG must use currentColor
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.
Pitfall 9: Table CSS must use colors visible on the table's background
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.
Pitfall 10: html_template must demonstrate proper class usage
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>
BrandConfig Structure Reference
File wrapper
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).
Top-level fields
| 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 |
colors (ColorPalette)
{
"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.
typography (Typography)
{
"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 tonullfor system fonts.
logo (LogoConfig)
{
"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 usefill="currentColor"ifuses_current_coloris 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).
slide_types (SlideTypeSystem)
{
"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_typeandlast_slide_typemust match thecss_classof a type in thetypesarray.- Every type MUST have a non-null
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).
decorative_elements
[
{
"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 -->).
animations (AnimationConfig)
{
"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_blockon 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.
footer (FooterConfig)
{
"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".
chrome (ChromeConfig)
{
"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.
shared_css (SharedCSS)
{
"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).- Both are injected server-side into the final HTML
<style>block.
javascript (JavaScriptConfig)
{
"controller_js": null,
"has_counter_animation": true
}
controller_js: set tonullto 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.
workflow (WorkflowConfig)
{
"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.
forbidden_patterns
[
"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.
Step-by-Step: Creating a New BrandConfig
Step 1: Gather brand assets
Collect from the client or brand guidelines:
- Logo as SVG (ideally with
currentColorfills) - Primary and secondary brand colors (hex values)
- Font choices (Google Fonts names, or system font families)
- Any gradient definitions
- Example presentations or style references
Step 2: Define color tokens
Create the color token list. At minimum you need:
- Primary brand color
- Text colors (primary, secondary, muted)
- Background colors (for light and dark slides)
- Spacing tokens (using
clamp()for responsiveness) - Border/accent colors
Write these as colors.tokens. Each token becomes a CSS variable --<name>.
Step 3: Define gradients
If the brand uses gradients, define them in colors.gradients. Include the full CSS gradient value with direction and color stops.
Step 4: Set up typography
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.
Step 5: Prepare the logo
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.
Step 6: Design slide types
Plan 4-8 slide types. Typical set:
- Cover -- gradient or hero background, first slide
- Content -- white/light background, general content
- Data/Table -- optimized for tables and metrics
- Chart -- optimized for chart containers
- Split -- two-column layout
- Closing -- gradient background, last slide
For each type, write:
css_blockwith all CSS rules (NEVER leave null -- see Pitfall 2)html_templateshowing proper class usage (see Pitfall 9)layout_descriptionandwhen_to_usefor the LLM
Step 7: Write shared_css
reset_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.
Step 8: Configure animations
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.
Step 9: Configure footer and chrome
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.
Step 10: Set chart and table container CSS
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:
- Set
table_css_classat the top level (e.g."dtbl"or"data-table") - Define CSS for that class in a slide type's css_block (e.g.
.dtbl,.dtbl th,.dtbl td) - Use
overflow: hiddenon table wrappers (see Pitfall 5) - Critical: Make sure the table CSS uses colors that are visible against the background where tables render — NOT the global theme color tokens if they're designed for dark backgrounds (see Pitfall 9)
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.
Step 11: Validate
Before uploading, verify:
-
shared_css.reset_and_baseincludes body font, html scroll-snap, heading rules - Every slide type has a non-null
css_block -
stagger_delaymatches the pattern^\d+(\.\d+)?(s|ms)$ - Chart containers have explicit height, no flex centering
- Table wrappers use
overflow: hidden - If tables are supported,
table_css_classis set and its CSS is defined in a slide type css_block - Table CSS colors are visible against the background where tables actually render (see Pitfall 9)
- Every
var(--...)reference in any CSS field is defined inbrand_tokens_css - Logo SVG uses
fill="currentColor"(ifuses_current_coloris true) -
html_templatefields demonstrate correct class usage for tables, charts, metrics -
first_slide_typeandlast_slide_typematch acss_classin the types array
Step 12: Upload
Wrap 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.
Quick Reference: Mandatory Minimums
A valid BrandConfig must have at least:
brand_nameandbrand_description- At least one color token in
colors.tokens - At least one font in
typography.fonts(role"body") - A
logo.primary_svgwith inline SVG - At least two slide types: one matching
first_slide_type, one matchinglast_slide_type - Every slide type with a non-null
css_block - At least one animation preset in
animations.presets footer.kindset to one of"text","svg_wave","none"shared_css.reset_and_basewith full typography setupshared_css.brand_tokens_cssdefining all referenced CSS variables