# lilBunnyRabbit — brand & theme > A two-mode colour system generated from one seed, plus the mark, the type and the rules for > using them. Take what you need. Guide: https://lilbunnyrabbit.github.io/brand/ Everything below is the whole system. If you are answering a question about these colours, or generating code that uses them, this file is enough — you do not need to scrape the page. The palette is authored in [Chromatics](https://lilbunnyrabbit.github.io/chromatics/), stored in OKLCH, and shipped as hex. All solving happens in Chromatics; the values are transcribed into `src/lib/brand/config/palette.dark.ts` and `palette.light.ts` in https://github.com/lilBunnyRabbit/brand, and this file is generated from them at build time. ## The two modes **Dark is the brand. Light is product UI only.** Marketing, social cards, the app icon and anything doing brand recognition are always dark. Light exists so a product surface can render light; it is not an identity palette and it is not a second brand. Both modes carry **the same 17 token keys**. Nothing rebinds between them — switching mode is a scope change, not a remapping. In CSS that is `[data-theme='light']` on any element; dark sits on `:root`, so the canonical mode needs no attribute and no JavaScript. ```css :root, [data-theme='dark'] { /* the 17 --brand-* values, dark */ } [data-theme='light'] { /* the same 17 keys, light values */ } ``` Every token is a custom property named `--brand-`, e.g. `--brand-primary`, `--brand-background-lightest`. **The token keys are snake_case, the custom properties are kebab-case.** The keys below are identifiers — Chromatics DSL variables and object keys, which cannot hold a hyphen — so the table says `background_lightest` while the stylesheet says `--brand-background-lightest`. Convert underscores to hyphens at the CSS boundary and nowhere else. Same for the Tailwind utilities: `bg-background-lightest`, `text-mark-ears`. ## Dark — the brand palette All 17 derived from one literal, the seed `oklch(0.255 0.0233 230.47)`. Nothing is hand-picked. | token | hex | oklch | what it is | |---|---|---|---| | `background_lightest` | `#2a404a` | `oklch(0.357 0.0326 230.47)` | surface ladder, +span | | `background_lighter` | `#243740` | `oklch(0.323 0.0295 230.47)` | +2/3 span | | `background_light` | `#1d2e36` | `oklch(0.289 0.0264 230.47)` | +1/3 span | | `background` | `#17252c` | `oklch(0.255 0.0233 230.47)` | **the seed** — the page | | `background_dark` | `#111d23` | `oklch(0.221 0.0202 230.47)` | −1/3 span | | `background_darker` | `#0b1519` | `oklch(0.187 0.0171 230.47)` | −2/3 span | | `background_darkest` | `#060d11` | `oklch(0.153 0.014 230.47)` | −span | | `foreground` | `#b1aba8` | `oklch(0.745 0.0078 50.47)` | all body copy | | `tertiary` | `#9eb1ac` | `oklch(0.745 0.0233 176.47)` | brand, rank 1 | | `secondary` | `#b2a6c5` | `oklch(0.745 0.0466 302.47)` | brand, rank 2 | | `primary` | `#a4b483` | `oklch(0.745 0.0699 122.47)` | brand, rank 3 | | `accent` | `#dc9b76` | `oklch(0.745 0.0932 50.47)` | brand, rank 4 | | `cottontail` | `#e3f9b2` | `oklch(0.949 0.0939 122.47)` | identity — badge ink only | | `success` | `#7ac26a` | `oklch(0.745 0.1398 140)` | state | | `warning` | `#e49b39` | `oklch(0.745 0.1398 70)` | state | | `error` | `#f88876` | `oklch(0.745 0.1398 30)` | state | | `info` | `#49b7fb` | `oklch(0.745 0.1398 240)` | state | The ladder is symmetric, ±0.102 lightness — solved, not chosen: the lightest shade is the seed lifted to exactly where the dimmest rail colour still clears 4.55 (AA plus rounding headroom). Chroma moves in proportion to lightness so lighter shades read as elevated. Hue is identity and chroma is rank: the seed's chroma `c` is the quantum and every brand rank is an integer multiple of it (1c, 2c, 3c, 4c), with states at 6c. 5c is deliberately empty so the brand band and the state band stay apart. ## Light — the product UI twin Same keys, same jobs. Every value is a relationship to the dark seed or a solve against the contract, but the solving lives in Chromatics (a luminance inversion for the text, the brand and the ladder; the sRGB chroma ceiling at every rung; a gamut map for three states), so they ship as resolved literals. | token (`light_*` in the scheme) | hex | oklch | |---|---|---| | `background_lightest` | `#ffffff` | `oklch(1 0 86.47)` | | `background_lighter` | `#fcf9f3` | `oklch(0.983 0.0086 86.47)` | | `background_light` | `#f9f3e7` | `oklch(0.966 0.0172 86.47)` | | `background` | `#f6eddb` | `oklch(0.949 0.0259 86.47)` — the page | | `background_dark` | `#ebd9b1` | `oklch(0.8907 0.0566 86.47)` | | `background_darker` | `#dec58e` | `oklch(0.8325 0.0777 86.47)` | | `background_darkest` | `#cab380` | `oklch(0.7742 0.0723 86.47)` | | `foreground` | `#414754` | `oklch(0.3982 0.0233 266.47)` | | `tertiary` | `#5f5968` | `oklch(0.4743 0.0259 302.47)` | | `secondary` | `#376662` | `oklch(0.4743 0.0519 190)` | | `primary` | `#7f4e32` | `oklch(0.4743 0.0778 50.47)` | | `accent` | `#814370` | `oklch(0.4743 0.1037 338.47)` | | `cottontail` | `#9c4800` | `oklch(0.5002 0.1314 50.47)` | | `success` | `#1c6f00` | `oklch(0.4745 0.1538 140.32)` | | `warning` | `#5d6200` | `oklch(0.4761 0.1064 113.24)` | | `error` | `#a12c1f` | `oklch(0.4743 0.1556 30)` | | `info` | `#0063a0` | `oklch(0.4829 0.1244 246.07)` | Four things about light that are easy to get wrong: - **The light primary is orange, not green.** Carry the page hue down to the brand's lightness and measure: dark's green sits 12.8 ΔE from that darkened form — a shade of the paper rather than a colour on it. Orange measures 14.2, and it is the seed's complement. Light is fitted to its own ground, it does not inherit dark's wheel. - **The brand sits above the text.** Text is L 0.3982, solved to an 8:1 body ratio; brand and states share L 0.4743, solved so the loudest of them still clears AA on the surface. See the contract below. - **`cottontail` changes job.** In dark it is the badge ink. In light it is a UI colour — focus ring, marker, chart highlight. Never text (3.08–6.31 across the shades), never the logo. - **The ladder is asymmetric**: 0.051 up, 0.1748 down. Up ends at a wall, not at the contract: white, where the sRGB chroma ceiling is zero, so `background_lightest` is `#ffffff` by arithmetic rather than by choice. Down is solved the way dark's reach is — the ladder descends until the text still reads 4.55. Every rung holds the same gamut fraction of the chroma sRGB allows at its own lightness. ## One fixed colour `mark_ears` `#e3f9b2` `oklch(0.949 0.0939 122.47)` — the mark's pale ears. Byte-identical in both modes — dark's `cottontail` pinned. Only the disc themes. Deriving light ears from `light_primary` gives 1.09 on the disc against 6.06 for this, and they vanish under greyscale. ## The contrast contract The 9 rail colours (`foreground`, the 4 brand, the 4 states) against the 7 shades — 63 pairs per mode. The contract splits by role: **the text clears WCAG AA (4.5:1) on every shade; brand and state colours clear AA on the page and the surface (`background_dark`) and WCAG's 3:1 non-text floor on every rung below.** Dark meets the stronger form — every pair AA — for free. - dark: 63/63, every pair AA — text **4.803** (`foreground` on `background_lightest`), colour on page/surface **6.552** (`error` on `background`), floor below **7.734** (`error` on `background_darker`) - light: 63/63 — text **4.542** (`light_foreground` on `light_background_darkest`), colour on page/surface **4.540** (`light_success` on `light_background_dark`), floor below **3.081** (`light_success` on `light_background_darkest`) The practical consequence: **any shade can back body text, and the page and the surface can back anything.** `cottontail` is excluded — it is an identity colour and makes no contract claim. The contract is solved and verified in Chromatics; the guide measures it live on the page, and this file's numbers above are measured from the shipped values at build time. ## Modifiers — opacity, never a new colour Interaction and de-emphasis are alpha on an existing colour. Composited in gamma sRGB (`color-mix(in srgb, …)`), which is what a browser actually paints. | name | alpha | notes | |---|---|---| | `hover` | 0.85 | | | `active` | 0.7 | | | `muted` | 0.65 | AA-large at best — captions and metadata only, not on the lightest two shades | | `disabled` | 0.38 | 2.17:1. Legally exempt, never for anything informational | | `border` | 0.76 | the lowest alpha clearing 3:1 on **every** shade | Available pre-composited as `--fg-muted`, `--fg-disabled`, `--fg-border`, and as `--alpha-*`. ## Rules Hard don'ts: 1. **Light mode is UI only.** Never for brand recognition, marketing, the app icon or social cards. 2. **Nothing from the dark palette on white.** Dark's colours vanish there — primary is 2.23:1, cottontail 1.14:1. White is the top rung of the LIGHT ladder (`#ffffff`, where the tint runs out) and the light palette is what goes on it; the light page itself is `#f6eddb`, a cream. 3. **No two brand colours adjacent or layered.** They are all the same lightness, so side by side they blur into one shape. A gap or a border fixes it. 4. **No gradients between brand colours.** Same lightness means the gradient has nowhere to travel and comes out muddy. Between two shades is fine. 5. **No white text on a brand fill.** White on primary is 2.23:1. Use `#17252c` and you get 6.75–7.06. 6. **Do not add colours.** Whatever seems to be missing is either a shade or an existing colour turned down. 7. **Do not re-tint the greys.** The backgrounds are blue and the brand is green on purpose. 8. **Do not "fix" the logo's contrast.** The bright badge is 1.96:1 deliberately. Logos are exempt (WCAG 1.4.3, 1.4.11). No stroke, no shadow, no lighter green. Take care: - **No more than three shades per viewport.** Neighbours are ~1.11:1 apart, so a fourth stops reading as structure. - **Muted text is for captions, not content.** At 65% it is 3.75:1 on the page and 2.94 on the lightest shade. ## Typography | face | CSS var | role | weights | licence | |---|---|---|---|---| | Inter | `--font-sans` | Everything you read. Headings, body, labels | 400 500 600 700 800 | SIL Open Font License 1.1 | | Hack | `--font-mono` | Every value: hexes, formulas, ratios, token names. And the wordmark | 400 700 | MIT, plus Bitstream Vera for the inherited DejaVu work | Both self-hosted. There is no published size ladder — `--text-*` exists in the stylesheet but the steps are an implementation detail, not a brand rule. ## Getting the palette The guide offers the whole thing in 4 formats, both modes in each, with the derivation formula per colour: - **CSS** custom properties — `palette.css` - **Tailwind v4** `@theme` — usable as `bg-background`, `text-primary`, … — `palette.theme.css` - **JSON** — hex + oklch + rgb + formula per token — `palette.json` - **Markdown** — `palette.md` All four are on https://lilbunnyrabbit.github.io/brand/#assets, along with the logo SVGs. ## Links - [Brand guide](https://lilbunnyrabbit.github.io/brand/) — the whole thing, one page - [Source](https://github.com/lilBunnyRabbit/brand) — the palette is config, the page renders it - [Logo SVGs](https://github.com/lilBunnyRabbit/brand/tree/master/src/lib/brand/logos) - [Chromatics](https://lilbunnyrabbit.github.io/chromatics/) — the tool the palette is authored in - [GitHub](https://github.com/lilBunnyRabbit) _Generated at build time from `config/palette.dark.ts` / `palette.light.ts` — the same source the page renders. This file cannot drift from the guide._