Chapter 01 · Dreambase Data Presentation
Subchapter 1.1
references/brand-theming.mdMarkdown14 KBView on GitHub
How to turn whatever the user hands over into a deck system — and what to do when they hand over nothing.
The goal is always the same output shape regardless of input: a token set plus a short brand report recording provenance and confidence per field. That way a later, better brand input is a drop-in replacement rather than a redesign.
Read it, and also glob for brand.md, BRANDING.md, design-system.md, tokens.json, theme.ts, tailwind.config.*, and :root blocks in CSS. Extract by regex first (hex, rgb(), hsl(), oklch(), --custom-property: <color>, font-family declarations), then use judgement for the prose rules. Named roles in the file always beat inferred ones.
In order of reliability:
document.styleSheets for :root rules and read the declarations. Don’t try to iterate getComputedStyle for this — you can’t enumerate custom properties that way reliably.prefers-color-scheme: dark block, if present — this hands you their dark pairing for free, already authored by their designers.Also pull the logo (prefer the SVG), and the actual font stacks rather than the display names.
Brand PDFs almost always print their hex values as text next to each swatch, so a regex over the text layer catches most of the palette without touching pixels. Then read 3–6 key pages as images — “color”, “typography”, “logo usage”, “incorrect usage” — for swatch chips, the clear-space diagram, and the do-not grid.
Two cautions: map Pantone to sRGB only with an explicit warning (it isn’t lossless, and guessing silently is the kind of error a brand team notices), and watch for CMYK-derived hexes that will look dull on screen. Extract the embedded font table to learn exact family names even when the files aren’t extractable. Capture the do-not rules verbatim into the report.
Confidence: high for rules, medium for color values.
A .potx is a ZIP. Unzip and read ppt/theme/theme1.xml:
a:clrScheme children in fixed order: dk1, lt1, dk2, lt2, accent1–accent6, hlink, folHlink. The six accents are the chart palette — that’s what PowerPoint uses for series colors.dk1/lt1 are usually system colors (<a:sysClr val="windowText"/>), so read the lastClr attribute, not val.a:fontScheme gives you major (heading) and minor (body) fonts, which map directly onto the same split in every other target.If you have the actual file, prefer building into it via pptx-automizer (see build-paths.md) over reconstructing a lookalike.
If SVG, parse it — every fill, stroke, stop-color, and inline style is an exact brand value, not an estimate. Deduplicate, drop pure white/black (usually the knockout), and rank by path area where computable.
If raster, quantize — but strip the transparent or white matte first, or the dominant color comes back as “the background.” Downsample and skip anti-aliased edge pixels, which are blends rather than brand colors. Prefer median cut over k-means: it’s deterministic, which matters when the same deck must build twice identically. Then filter clusters by chroma (C > 0.04 in OKLCH) to separate brand hues from neutrals.
Take the highest-chroma, highest-area cluster as the primary and derive the rest. A monochrome logo gives you exactly one color — which is the case the derivation below is designed for.
Do not guess a brand. Ship a deliberately neutral, high-quality system and say so.
Sensible default: a near-neutral surface pair (not pure white/black — around L 0.98 and L 0.16 in OKLCH, to avoid glare and smear), one restrained accent, and the Okabe–Ito palette for charts so the data is correct-by-default even with no brand. Mark every token as a default in the report, so brand ingestion later is a swap with zero template changes.
Then ask one good question rather than six: “Do you have a logo, a website, or a PowerPoint template?” — in increasing order of value to you.
Given N extracted colors and no stated roles:
C < 0.03. Sort by lightness: lightest becomes surface, darkest becomes text. If there are fewer than two, generate them.C × area among non-neutrals; break ties by “appears in the logo.”Record every inference and its confidence. Never silently invent a role.
All of this is implementable in plain JS with no dependencies. Work in OKLCH — it’s perceptually uniform, so holding lightness constant across hues actually produces colors that look equally light, which is the thing HSL gets wrong and why HSL-derived ramps show the classic bright band in the middle.
Step 0. #RRGGBB → linear sRGB → Oklab → OKLCH, giving (L₀, C₀, H₀).
Step 1 — build an 11-step ramp. Hold hue constant, walk a fixed lightness curve:
step: 50 100 200 300 400 500 600 700 800 900 950
L: 0.97 0.94 0.89 0.82 0.74 0.65 0.57 0.49 0.41 0.33 0.24Scale chroma by a bell that peaks near 500–600 and falls to ~15% at the ends, so the extremes desaturate naturally. Gamut-map every step by holding L and H and reducing C to the sRGB boundary via binary search — never by clipping channels, which shifts hue. Finally re-anchor: find the step whose lightness is nearest L₀ and overwrite it with the exact original hex, so the user’s brand color appears verbatim in the output.
Step 2 — assign roles by target contrast, not by eye. This is Leonardo’s central idea and it’s the right one: generate colors from contrast ratios rather than picking colors and checking afterward. Binary-search L at fixed (C, H) until the contrast target is hit — the same search as gamut mapping with a different objective.
| Role | Against | Target |
|---|---|---|
| Primary text | Surface | ≥ 7:1 (decks get projected in bright rooms) |
| Secondary text | Surface | ≥ 4.5:1 |
| Muted text / captions | Surface | ≥ 4.5:1 — don’t relax this for footnotes |
| Solid accent | Surface | ≥ 3:1 (it’s a graphical object) |
| Text on accent | Accent | ≥ 4.5:1 |
| Chart series | Plot background | ≥ 3:1 (SC 1.4.11) |
| Meaning-bearing borders/axes | Adjacent | ≥ 3:1 |
Build to WCAG 2.2 AA. APCA was marked exploratory and removed from WCAG 3 in July 2023, and no replacement has been chosen — the contrast algorithm for WCAG 3 is still undetermined, and WCAG 3 itself is plausibly around 2030. Treat APCA as an optional advisory check; never present it as a standard.
Start from Okabe–Ito when there’s no brand constraint — eight colors designed for Color Universal Design, deliberately avoiding the yellow-green range where CVD confusion is worst, and spanning a wide luminance range so brightness works as a second separation channel when hues collapse:
#000000 #E69F00 #56B4E9 #009E73 #F0E442 #0072B2 #D55E00 #CC79A7Note #F0E442 (yellow) fails 3:1 against white — darken or swap it on light backgrounds.
For a brand-flavored categorical palette, don’t just recolor everything in the brand hue — series have to be distinguishable:
If brand-derived series fail CVD validation, fall back to Okabe–Ito with the brand color substituted into the nearest-hue slot, and record the substitution.
Sequential ramps: monotonic L, single hue. Diverging: two hues with L peaking at the neutral midpoint. Both are straightforward in OKLCH.
Keep primitives (ramps, spacing, radii, type scale) mode-independent and vary only the semantic layer. Note that the design-tokens format defines no mode or theme mechanism despite common belief, so file-per-mode is the correct implementation, and it’s what both token pipelines and Figma variable modes reduce to anyway.
Dark is not an inversion:
Never alter brand colors for accessibility. WCAG explicitly exempts logotypes from contrast minimums, and 1.4.11 exempts logos too. The logo renders in its exact brand colors, always.
When the brand primary fails 4.5:1 as body text, add rather than alter:
The chart-only extended palette is the key move. Most brands ship 1–3 colors; six series need six. Create a namespaced chart palette that is explicitly declared as an extension, not part of the brand: series 1 is the brand primary, the rest are derived and CVD-validated, and the extension never appears in chrome, headers, or near the logo. Brand teams object to altered brand colors; a declared data-visualization extension is standard, defensible practice.
[ ] every text/background pair ≥ 4.5:1 (≥ 3:1 large), per mode
[ ] every chart series vs. plot background ≥ 3:1, per mode
[ ] every series pair distinguishable under 3 CVD simulations
[ ] ≤ 7 categorical series, or the chart form changes
[ ] logo at or above minimum size, clear space unobstructed
[ ] logo in unaltered brand colors, correct mode variant
[ ] every color traces to a token — no silently approximated brand hex
[ ] every figure traces to a data source
[ ] every exhibit carries a source line
[ ] brand font embedded, or the fallback documented
[ ] no placeholder residue: "Lorem", "20XX", "$XXM", "[Company]"That last one matters more than it looks. Placeholder residue in a shipped deck is the single most visible failure mode of generated presentations, and it’s trivially checkable.