Casca ThemesLink to section
Casca ships four named moods that recolor every primitive at once. Each mood is
a small --casca-* token overlay; the rest of the library stays the same.
Built-In MoodsLink to section
| Mood | Light surface | Light accent | Dark surface | Dark accent |
|---|---|---|---|---|
| Solar | #F2EBDA | #C8633A | #1C1812 | #E68A60 |
| Cyber | #E8F0E9 | #1E7A4F | #0B0E0A | #80FF8A |
| Lunar | #E8E6F0 | #5D44C7 | #0F1820 | #B9A6FF |
| Arcade | #FFF1E8 | #C8246F | #191028 | #FF4A6B |
Every mood has a matched dark palette that the OS triggers via
prefers-color-scheme: dark. The user picks the mood; the OS picks the
brightness.
Canonical mood names map to the bundled source files as follows:
| Mood | Source file |
|---|---|
| Solar | src/themes/default.css |
| Cyber | src/themes/high-contrast.css |
| Lunar | src/themes/muted.css |
| Arcade | src/themes/vibrant.css |
For <casca-layout>, the layout host must carry class="casca" so the mood
overlays and no-JS picker preview can repaint the shadow-hosted chrome.
Prose Callout PaletteLink to section
Prose callouts do not require new theme tokens. The default role colors fall through the existing series and grayscale tokens:
| Callout | Default role token |
|---|---|
| Note | --casca-gray-7 light, --casca-gray-3 dark |
| Tip | --casca-color-2 |
| Warning | --casca-color-3 |
| Danger | --casca-color-8 |
| Important | --casca-color-1 |
Those defaults stay in-palette for Solar, Cyber, Lunar, and Arcade, but they are
mood colors rather than universal semantic colors. A host theme may override
--casca-prose-callout-note, --casca-prose-callout-tip,
--casca-prose-callout-warning, --casca-prose-callout-danger, or
--casca-prose-callout-important on .casca-prose without changing the HTML
contract.
Picker MarkupLink to section
dist/casca.css carries Solar as the base mood plus manifest-registered
overlays for the other moods. The no-JS picker is a GET form:
<form class="casca-theme-picker" data-casca-theme-picker method="get" action="/_casca/theme">
<input type="hidden" name="return_to" value="/docs/">
<fieldset class="casca-theme-picker-fieldset" aria-label="Theme">
<legend class="sr-only">Theme</legend>
<label class="casca-theme-picker-button" data-mood="solar" style="--_swatch-a:#f2ebda;--_swatch-b:#c8633a">
<input type="radio" name="casca-theme" value="solar" checked>
<span class="sr-only">Solar</span>
</label>
<label class="casca-theme-picker-button" data-mood="cyber" style="--_swatch-a:#e8f0e9;--_swatch-b:#1e7a4f">
<input type="radio" name="casca-theme" value="cyber">
<span class="sr-only">Cyber</span>
</label>
</fieldset>
</form>
Checking a radio previews the mood on the current page in browsers with
:has(). The form wrapping is forward-compatibility scaffolding for the
future casca serve persistence endpoint; until that ships, the picker is
preview-only and selection auto-applies via CSS without a submit step.
PersistenceLink to section
casca dev and casca preview serve the picker action when a
theme-picker widget manifest is configured. The request-time state order is:
?casca-theme=<id>for a one-request URL override.- The
casca-theme=<id>cookie for durable preference. [theme_state].defaultfrom the manifest.
Unknown ids fall back to the manifest default. HTML responses are transformed
at request time, so the files written by casca site are unchanged. The
serving layer sets body[data-casca-theme="<id>"], refreshes the checked
picker radio, updates the hidden return_to input to the current path plus
query, and emits Vary: Cookie on transformed HTML responses.
The setter endpoint is GET /_casca/theme. It accepts the manifest parameter,
currently casca-theme, and optional return_to. A valid id redirects to the
validated same-origin return path and sets:
Set-Cookie: casca-theme=<id>; Path=/; SameSite=Lax; HttpOnly; Max-Age=31536000
Secure is added when the local server is running over HTTPS. An empty
casca-theme= clears the cookie with Max-Age=0. The cookie is host-only
because Domain is omitted.
return_to must be a same-origin path beginning with /. Protocol-relative
URLs, absolute scheme URLs, CR, LF, NUL, backslash, over-length values, and
the setter path itself fall back to /.
The setter rejects cross-origin writes with Fetch Metadata when available:
Sec-Fetch-Site: same-origin and none are allowed, while same-site and
cross-site are rejected. If Fetch Metadata is absent and Referer is
present, the Referer origin must match the request origin. The response also
sets X-Frame-Options: DENY and
Content-Security-Policy: frame-ancestors 'none'.
Production Serving RecipesLink to section
Casca still publishes static files. Hosts that need durable theme persistence can add a small edge worker or reverse proxy in front of those files:
- Read the manifest ids from deployment config or copy them from
site/casca-themes.toml. - On
GET /_casca/theme, validatecasca-theme, validatereturn_to, set or clear the host-only cookie with the attributes above, and redirect. - On HTML responses, resolve query override, cookie, or default, then parse
the HTML and set
body[data-casca-theme], the checked picker radio, andVary: Cookieor a narrower cache key for the theme cookie. - Bypass assets, feeds, sitemaps, HMR routes, and other reserved operational paths.
Strict static-only hosts can publish per-mood URL spaces such as /solar/...,
/cyber/..., /lunar/..., and /arcade/.... Casca does not generate those
spaces in 4.7b because they change URLs and canonical policy.
Scoping SelectorsLink to section
Non-base moods are scoped through two roots. Server state comes first and does
not depend on :has():
body[data-casca-theme="cyber"].casca,
body[data-casca-theme="cyber"] .casca {
/* Cyber overrides */
}
Current-page preview is guarded by @supports selector(:has(*)):
@supports selector(:has(*)) {
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked).casca,
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked) .casca {
/* Cyber overrides */
}
}
The .casca-theme-picker[data-casca-theme-picker] ancestor and
name="casca-theme" input selector prevent unrelated page forms from
hijacking mood state.
Theme ManifestLink to section
The all-in-one bundle is assembled from a TOML manifest:
[theme_state]
param = "casca-theme"
cookie = "casca-theme"
picker_root = ".casca-theme-picker[data-casca-theme-picker]"
setter_path = "/_casca/theme"
default = "solar"
[[themes]]
id = "solar"
label = "Solar"
kind = "css"
source = "../src/themes/default.css"
base = true
swatch_light = ["#f2ebda", "#c8633a"]
swatch_dark = ["#251e16", "#e68a60"]
[[themes]]
id = "ember"
label = "Ember"
kind = "tokens"
source = "themes/ember.toml"
swatch_light = ["#fff2df", "#b94e2f"]
swatch_dark = ["#211713", "#ff9a68"]
Manifest paths resolve relative to the manifest file. Theme ids must match
[a-z0-9][a-z0-9-]{0,47}. Exactly one theme may set base = true; if none
does, [theme_state].default selects the base. The base is emitted first even
when it appears later in the manifest. Duplicate ids, duplicate labels, missing
sources, invalid swatch colors, and unknown keys in strict mode are errors.
Build with:
casca scope-theme \
--core dist/casca-core.css \
--extension dist/casca-extended-charts.css \
--extension dist/casca-extended-controls.css \
--extension dist/casca-extended-layout.css \
--theme-manifest site/casca-themes.toml \
--output dist/casca.css
The compatibility CLI with --base, --overlay, and --picker-root remains
available for direct bundle assembly.
Token ThemesLink to section
kind = "tokens" compiles a TOML token file into a .casca overlay. Required
fields are [light] and [dark] gray ramps with 10 colors, surface_1,
series with 8 colors, and on_accent. Role tables map gray indices to the
semantic tokens:
[roles.light]
page_bg = "gray-0"
ink = "gray-9"
muted = "gray-7"
rule = "gray-3"
ink_inverse = "gray-0"
label_color = "gray-7"
axis_color = "gray-5"
grid_color = "gray-4"
The compiler emits --casca-gray-0 through --casca-gray-9,
--casca-surface-1, role tokens, --casca-color-1 through
--casca-color-8, chart label/axis/grid tokens, and --casca-on-accent.
--casca-scatter-grid-color and --casca-radar-grid-color derive from
grid_color unless scatter_grid_color or radar_grid_color is supplied in
the role table.
Direct CSS ThemesLink to section
kind = "css" points at a source CSS overlay. The file must be rooted at
.casca, may declare only --casca-* or --_ custom properties, and must not
author :root, body, html, universal selectors, bare primitive roots,
.casca-theme-picker, or body[data-casca-theme] roots. The scoper generates
server-state and picker-preview roots.
Picker AccessibilityLink to section
The picker remains a native form with a fieldset, legend, labels, and radios. Keyboard users can Tab into the group and use arrow keys between radios; selection applies the mood preview immediately via CSS. Forced-colors mode uses system colors, and reduced motion removes the hover translate and checked glow.
Picker VariablesLink to section
The picker exposes these public variables:
--casca-theme-picker-size--casca-theme-picker-size-touch--casca-theme-picker-gap--casca-theme-picker-radius--casca-theme-picker-border--casca-theme-picker-border-dark--casca-theme-picker-border-checked--casca-theme-picker-border-checked-dark
Manifest-generated buttons set --_swatch-a and --_swatch-b inline. The
built-in [data-mood] swatch rules remain as fallback styling.