agentsclimarketplace

Brand config authoring

Skill MotleyAI/motley-skills/skills/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

Install
npx -y skills add MotleyAI/motley-skills --skill brand-config-authoring

Assembled 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.json
    • storyline/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 .visible class to slides
  • Keyboard navigation (arrow keys, Space, Page Up/Down)
  • Touch and wheel navigation
  • Progress bar updates (if chrome.has_progress_bar is true)
  • Nav dot updates (if chrome.has_nav_dots is 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

FieldTypeRequiredDescription
brand_namestringYesDisplay name of the brand
brand_descriptionstringYes1-2 sentence description of the visual style
colorsColorPaletteYesComplete color system
typographyTypographyYesFont definitions and settings
logoLogoConfigYesSVG logo with color/size variants
slide_typesSlideTypeSystemYesAll slide archetypes
decorative_elementslistNoBrand-specific visual motifs (waves, tiles, grids)
animationsAnimationConfigYesAllowed animation patterns
footerFooterConfigYesFooter appearance
chromeChromeConfigNoNavigation chrome (progress bar, nav dots, topbar)
shared_cssSharedCSSNoCSS that applies across all slides
javascriptJavaScriptConfigNoNavigation controller and counter animations
workflowWorkflowConfigNoWhich workflow phases are enabled
default_chart_color_schemestringNoNamed color scheme for eCharts
table_css_classstringNoCSS class applied to server-resolved <table> elements (e.g. "dtbl", "data-table")
forbidden_patternslist[string]NoRules 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 to null for 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 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).

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_type and last_slide_type must match the css_class of a type in the types array.
  • 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_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.

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 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.

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 currentColor fills)
  • 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:

  1. Cover -- gradient or hero background, first slide
  2. Content -- white/light background, general content
  3. Data/Table -- optimized for tables and metrics
  4. Chart -- optimized for chart containers
  5. Split -- two-column layout
  6. Closing -- gradient background, last slide

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 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:

  1. Set table_css_class at the top level (e.g. "dtbl" or "data-table")
  2. Define CSS for that class in a slide type's css_block (e.g. .dtbl, .dtbl th, .dtbl td)
  3. Use overflow: hidden on table wrappers (see Pitfall 5)
  4. 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_base includes body font, html scroll-snap, heading rules
  • Every slide type has a non-null css_block
  • stagger_delay matches 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_class is 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 in brand_tokens_css
  • Logo SVG uses fill="currentColor" (if uses_current_color is true)
  • html_template fields demonstrate correct class usage for tables, charts, metrics
  • first_slide_type and last_slide_type match a css_class in 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:

  1. brand_name and brand_description
  2. At least one color token in colors.tokens
  3. At least one font in typography.fonts (role "body")
  4. A logo.primary_svg with inline SVG
  5. At least two slide types: one matching first_slide_type, one matching last_slide_type
  6. Every slide type with a non-null css_block
  7. At least one animation preset in animations.presets
  8. footer.kind set to one of "text", "svg_wave", "none"
  9. shared_css.reset_and_base with full typography setup
  10. shared_css.brand_tokens_css defining all referenced CSS variables

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.