Casca API ReferenceLink to section
Complete reference for all Casca components, CSS custom properties, and HTML patterns.
Table of ContentsLink to section
- Bar Charts
- Pie & Donut Charts
- Line Charts
- Area Charts
- Progress Bars
- Gauge Charts
- Heatmap
- Scatter Plot
- Waterfall
- Radar
- Candlestick
- Axis Labels
- UI Components
- Interactions (no-JS)
- Layout Shell (extended-layout)
- Layout Primitives
- CSS Custom Properties
- SSG Markdown Output
- CLI Theme Tools
- CLI Lint Checks
SSG Markdown OutputLink to section
casca site emits Casca-classed markdown HTML by default. Rendered tables get
casca-data, casca-data-head, and casca-data-body; fenced code blocks get
casca-code and, when present, data-language; strong-leading blockquotes get
casca-prose-callout with data-callout, role="note", and
aria-label="Note".
Use [markdown].casca_primitive_mapping = false or the
[markdown.opt_out] flags for compatibility with older markdown output. Use
[markdown].wrap_in_casca_layout = false to disable automatic
<casca-layout> wrapping. Route-level [[layouts]].variant values override
[markdown].default_variant for wrapped markdown pages.
Bar ChartsLink to section
Basic Bar ChartLink to section
<div class="casca casca-figure">
<div class="casca-title">Chart Title</div>
<div class="casca-bar">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-value" style="--value: 75%; --color: var(--casca-color-1)"></div>
</div>
</div>
<div class="casca-bar-labels">
<span class="casca-bar-label">Label</span>
</div>
</div>
</div>
Diverging Baseline Bars (Cash Flow)Link to section
NEW in v0.2.0
<div class="casca-bar" data-diverging>
<div class="casca-bar-group" data-grid>
<div class="casca-bar-item">
<!-- Positive value (above baseline) -->
<div class="casca-bar-value"
data-direction="positive"
style="--value: 30%; --color: var(--casca-color-5)"
data-label="+$15k"></div>
</div>
<div class="casca-bar-item">
<!-- Negative value (below baseline) -->
<div class="casca-bar-value"
data-direction="negative"
style="--value: 16%; --color: var(--casca-color-8)"
data-label="-$8k"></div>
</div>
</div>
</div>
Attributes:
data-diverging- Enables diverging baseline modedata-direction="positive|negative"- Bar direction from center
Grouped BarsLink to section
NEW in v0.2.0
<div class="casca-bar">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-grouped">
<div class="casca-bar-value" style="--value: 65%; --color: var(--casca-color-1)"></div>
<div class="casca-bar-value" style="--value: 75%; --color: var(--casca-color-2)"></div>
<div class="casca-bar-value" style="--value: 85%; --color: var(--casca-color-3)"></div>
</div>
</div>
</div>
</div>
Stacked BarsLink to section
<div class="casca-bar">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-stack">
<div class="casca-bar-segment" style="--value: 25%; --color: var(--casca-color-1)" data-label="Segment 1"></div>
<div class="casca-bar-segment" style="--value: 15%; --color: var(--casca-color-2)" data-label="Segment 2"></div>
<div class="casca-bar-segment" style="--value: 10%; --color: var(--casca-color-3)" data-label="Segment 3"></div>
</div>
</div>
</div>
</div>
Horizontal BarsLink to section
<div class="casca-bar" data-orientation="horizontal">
<!-- Same structure as vertical bars -->
</div>
Bar Chart AttributesLink to section
| Attribute | Values | Description |
|---|---|---|
data-grid | - | Show background grid lines |
data-diverging | - | Enable diverging baseline mode |
data-orientation | horizontal | Horizontal bar layout |
data-direction | positive, negative | Bar direction (diverging only) |
data-label | String | Value label text (on the bar) |
data-labels | always | Show data-label values without hover (touch / print / static no-JS); default is hover-only |
Bar Chart CSS VariablesLink to section
| Variable | Default | Description |
|---|---|---|
--value | 0% | Bar height/length (0-100%) |
--color | var(--casca-color-1) | Bar fill color |
--casca-bar-width | 2rem | Bar width |
--casca-bar-radius | 0.25rem | Bar border radius |
--casca-bar-min-height | 2px | Minimum bar height |
--casca-bar-rank-label-width | 7rem | Leading label column in the ranked list |
Always-visible values & ranked listsLink to section
By default a bar's data-label shows on hover (a dark pill). Add
data-labels="always" to the .casca-bar to render the values as plain
end-of-bar text with no hover or pointer dependency - so the numbers are
visible on touch, in print, in a screenshot, and on a static server-rendered
no-JS page. The chart reserves headroom (vertical) / trailing room (horizontal)
so labels are not clipped. The default (no attribute) is unchanged.
For the analytics "top N" view, compose horizontal orientation + a leading
.casca-bar-label and a trailing .casca-bar-rank-value (real text) per row.
Each .casca-bar-item then lays out as label · bar · value, sharing one
baseline, and reads "label … value" in DOM order even with the stylesheet absent:
<div class="casca-bar" data-orientation="horizontal" data-labels="always"
style="--casca-height: auto" aria-hidden="true">
<div class="casca-bar-group">
<div class="casca-bar-item">
<span class="casca-bar-label">Organic search</span>
<div class="casca-bar-value" style="--value: 100%"></div>
<span class="casca-bar-rank-value">4,200</span>
</div>
<!-- …one row per item, sorted descending… -->
</div>
</div>
The visual chart stays aria-hidden; the .casca-data table remains the
source of truth for assistive tech. Meaning is carried by the value text, never
color alone.
Pie & Donut ChartsLink to section
Pie ChartLink to section
<div class="casca-pie">
<div class="casca-pie-chart"
data-segments="4"
style="
--angle-1: 90deg;
--angle-2: 180deg;
--angle-3: 270deg;
--color-1: var(--casca-color-1);
--color-2: var(--casca-color-2);
--color-3: var(--casca-color-3);
--color-4: var(--casca-color-4);
">
</div>
</div>
Donut ChartLink to section
<div class="casca-pie">
<div class="casca-pie-chart"
data-variant="donut"
data-segments="3"
style="--angle-1: 120deg; --angle-2: 240deg;">
<!-- Optional center label -->
<div class="casca-pie-center">
75%
<span class="casca-pie-center-label">Complete</span>
</div>
</div>
</div>
Pie Chart AttributesLink to section
| Attribute | Values | Description |
|---|---|---|
data-segments | 0-8 | Number of pie segments. 1 renders a solid single-category disc (--color-1); 0 (or omitted) renders a neutral "no data" disc |
data-variant | donut | Donut chart with center hole |
Edge cases: A single-category chart (
data-segments="1") needs only--color-1(no--angle-*). An empty chart (data-segments="0") renders a neutral grey placeholder disc - pair it with a "no data" message in the legend or.casca-datatable. Very small slices are handled natively byconic-gradient(a 1% slice =3.6deg); set the matching--angle-*values.
Pie Chart CSS VariablesLink to section
| Variable | Default | Description |
|---|---|---|
--angle-N | 0deg | Cumulative angle for segment N (1-8) |
--color-N | var(--casca-color-N) | Color for segment N |
--casca-pie-size | 250px | Pie diameter |
--casca-donut-hole-size | 40% | Donut hole size (percentage) |
HeatmapLink to section
NEW in v0.3.0
A read-only, CSS-only color-intensity grid: --casca-heatmap-cols columns of
square cells whose fill blends --casca-heatmap-empty → --casca-heatmap-color
by each cell's --value (0–1). Natural fit for calendar / availability overviews
and correlation matrices. It pairs above the form-aware Slot Grid
as the read-only overview (same day-column model).
This is a visual-only chart: mark the grid aria-hidden and pair it with a
.casca-data table (like the bar/pie charts). Add a native title="…" per cell
for an exact-value, no-JS tooltip.
<figure class="casca casca-figure">
<figcaption class="casca-title">Typical free time by day</figcaption>
<table class="casca-data"><!-- exact values for screen readers --></table>
<div class="casca-heatmap" aria-hidden="true" style="--casca-heatmap-cols: 7">
<span class="casca-heatmap-colhead">Mon</span>
<!-- … 6 more headers … -->
<div class="casca-heatmap-cell" style="--value: 0.8" title="Mon - 80% free"></div>
<!-- … more cells, row-major … -->
</div>
<!-- optional intensity legend -->
<div class="casca-heatmap-scale" aria-hidden="true">
<span>Busy</span><span class="casca-heatmap-scale-bar"></span><span>Free</span>
</div>
</figure>
Classes:
| Class | Description |
|---|---|
.casca-heatmap | The grid container (--casca-heatmap-cols equal columns) |
.casca-heatmap-colhead | Optional column header (sits in the grid's first row) |
.casca-heatmap-cell | A cell; --value (0–1) drives fill intensity |
.casca-heatmap-scale | Optional legend row (Busy → Free) |
.casca-heatmap-scale-bar | The gradient bar inside the legend |
Heatmap CSS Variables:
| Variable | Default | Description |
|---|---|---|
--casca-heatmap-cols | 7 | Number of columns (set inline per grid) |
--value | 0 | Per-cell intensity, 0–1 (on .casca-heatmap-cell) |
--casca-heatmap-color | var(--casca-color-1) | Full-intensity (value 1) color |
--casca-heatmap-empty | var(--casca-gray-2) | Empty (value 0) color |
--casca-heatmap-empty-dark | var(--casca-gray-8) | Empty color in dark mode |
--casca-heatmap-gap | var(--casca-size-1) | Gap between cells |
--casca-heatmap-radius | var(--casca-radius-1) | Cell corner radius |
Features:
- ✅ Fill via
color-mix()(empty → color by--value), with an@supportsopacity fallback for engines without it - ✅ Dark-mode aware; works from the core build
- ✅ Forced-colors safe (degrades to bordered cells; values stay in the data table)
- ✅ Square cells via
aspect-ratio, responsive through grid1frcolumns
Read-only: for a selectable booking grid, use the form-aware Slot Grid instead.
See site/pages/charts/heatmap.html.
Scatter PlotLink to section
NEW in v0.4.0
A read-only, CSS-only scatter / bubble plot. Each point is absolutely positioned
over a plot box by its --x / --y (both 0–100, percent of the axis range -
the server normalizes raw values, keeping this no-JS). --size makes bubbles;
--color encodes categories.
This is a visual-only chart: mark the plot aria-hidden and pair it with a
.casca-data table listing (x, y[, category]). Add a native title="…" per
point for a no-JS value tooltip.
<figure class="casca casca-figure">
<figcaption class="casca-title">Study hours vs. exam score</figcaption>
<table class="casca-data"><!-- (x, y) per point for screen readers --></table>
<div class="casca-scatter" aria-hidden="true" data-grid>
<div class="casca-scatter-point" style="--x: 20; --y: 45" title="2h → 45"></div>
<!-- bubble + category -->
<div class="casca-scatter-point"
style="--x: 80; --y: 88; --size: 1.25rem; --color: var(--casca-color-5)"></div>
</div>
</figure>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-scatter | The plot box (position: relative, aspect-ratio, left+bottom axis frame) |
data-grid | Opt-in graph-paper grid background |
.casca-scatter-point | A point; positioned by --x / --y (0–100) |
.casca-scatter-trend | Opt-in trend line; full-width band from --y1 (left edge) to --y2 (right edge) |
.casca-scatter-quadrants | Opt-in dividers; vertical rule at --qx, horizontal at --qy |
.casca-scatter-quadrant-label | A quadrant corner label; positioned by --x / --y |
Scatter CSS Variables:
| Variable | Default | Description |
|---|---|---|
--x / --y | 0 | Point / label position, 0–100 (percent of plot) |
--size | --casca-scatter-point-size | Point diameter (bubble variant) |
--color | var(--casca-color-1) | Point color (category) |
--y1 / --y2 | 50 | Trend line y at the left / right edge, 0–100 (server-fit) |
--qx / --qy | 50 | Quadrant divider positions, 0–100 |
--casca-scatter-aspect | 4 / 3 | Plot aspect ratio |
--casca-scatter-point-size | 0.75rem | Default point diameter |
--casca-scatter-grid-color | var(--casca-grid-color) | Grid line color |
--casca-scatter-grid-step | 25% | Grid spacing |
--casca-scatter-trend-color | var(--casca-label-color) | Trend line color |
--casca-scatter-trend-width | 1.5 | Trend band thickness (% of plot height) |
--casca-scatter-quadrant-color | var(--casca-axis-color) | Quadrant divider color |
Features:
- ✅ Absolute
%positioning - O(points) DOM, naturally responsive, dark-mode aware - ✅ Bubble (
--size) and category (--color) variants - ✅ Optional trend line (
clip-pathband - server passes the fit, no in-browser regression) and quadrant dividers for 2×2 analysis - ✅ Forced-colors safe (points → outlined dots, trend → system color, dividers use borders; values stay in the table)
- ✅ Reduced-motion safe (hover scale handled by the core reduced-motion reset)
Axis tick labels are a shared primitive - see Axis Labels. The trend line spans the full x-range (the regression line); state its meaning (e.g. "positive correlation") in text, since the overlays are visual aids and the
.casca-datatable is the data source.
<!-- trend line + quadrant dividers (both opt-in children of the plot) -->
<div class="casca-scatter" aria-hidden="true">
<div class="casca-scatter-trend" style="--y1: 24; --y2: 96"></div>
<div class="casca-scatter-quadrants" style="--qx: 50; --qy: 50">
<span class="casca-scatter-quadrant-label" style="--x: 25; --y: 90">Quick wins</span>
</div>
<!-- … points … -->
</div>
See site/pages/charts/scatter.html.
WaterfallLink to section
NEW in v0.5.0
A read-only, CSS-only waterfall: floating bars showing the cumulative effect of
sequential increases / decreases, with running-total bars at the ends. Built for
cash flow and budget analysis. Each bar carries --start / --end (0–100, the
cumulative level before/after the step, as a percent of the value range - the
server computes the running total). The segment floats between those levels
(derived with min()/max(), so no per-bar base/height math); a dashed
connector bridges each gap at the --end level. data-direction picks the color.
Visual-only: mark the chart aria-hidden and pair it with a .casca-data table.
<figure class="casca casca-figure">
<figcaption class="casca-title">Q1 cash flow</figcaption>
<table class="casca-data"><!-- start, each delta, end --></table>
<div class="casca-waterfall" aria-hidden="true" data-grid>
<div class="casca-waterfall-bar" data-direction="total" style="--start: 0; --end: 50">
<span class="casca-waterfall-cap">$50k</span>
</div>
<div class="casca-waterfall-bar" data-direction="increase" style="--start: 50; --end: 85">
<span class="casca-waterfall-cap">+$35k</span>
</div>
<div class="casca-waterfall-bar" data-direction="decrease" style="--start: 85; --end: 65">
<span class="casca-waterfall-cap">−$20k</span>
</div>
<div class="casca-waterfall-bar" data-direction="total" style="--start: 0; --end: 65">
<span class="casca-waterfall-cap">$65k</span>
</div>
</div>
<div class="casca-waterfall-labels">
<span class="casca-waterfall-label">Start</span>
<!-- … one per bar … -->
</div>
</figure>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-waterfall | The chart container (flex row of bars, baseline at 0) |
data-grid | Opt-in horizontal gridlines |
.casca-waterfall-bar | One step; --start / --end (0–100) set the floating segment |
data-direction | increase | decrease | total - picks the segment color |
.casca-waterfall-cap | Optional value label floated above the segment |
.casca-waterfall-labels / -label | X-axis category label row |
Waterfall CSS Variables:
| Variable | Default | Description |
|---|---|---|
--start / --end | 0 | Cumulative level before / after the step (0–100; on the bar) |
--casca-waterfall-height | 240px | Plot height |
--casca-waterfall-increase | var(--casca-color-5) | Increase color |
--casca-waterfall-decrease | var(--casca-color-8) | Decrease color |
--casca-waterfall-total | var(--casca-color-1) | Total-bar color |
--casca-waterfall-gap | var(--casca-size-3) | Gap between bars |
--casca-waterfall-connector-color | var(--casca-axis-color) | Connector line color |
Features:
- ✅
min()/max()-derived segments - server only computes the running total - ✅ Dashed connectors auto-aligned at each step's end level
- ✅ Dark-mode aware (colors via tokens); forced-colors safe
- ✅ Works from the core build
See site/pages/charts/waterfall.html.
RadarLink to section
NEW in v0.6.0
A read-only, CSS-only radar (spider) chart: one filled polygon per data series
over a circular gridline backdrop. Each series is positioned by --points - the
comma-separated x% y% vertices (one per axis) that the server computes from
polar coordinates (center 50% 50%; for axis i at angle θ and value v in
0–1, the vertex is 50% + v*50%*cosθ, 50% + v*50%*sinθ). CSS clips the series
box to that polygon - no in-browser trig. Translucent series overlap to compare
profiles (skills, products, reviews).
Visual-only: mark it aria-hidden, pair with a .casca-data table. Spoke
labels use the shared Axis Labels primitive
(data-axis="radial") - see the example.
<figure class="casca casca-figure">
<figcaption class="casca-title">Player comparison</figcaption>
<table class="casca-data"><!-- per-axis values --></table>
<div class="casca-radar" style="--casca-radar-axes: 6" aria-hidden="true">
<div class="casca-radar-series"
style="--points: 50% 5%, 76% 35%, 80% 68%, 50% 75%, 15% 70%, 24% 35%;
--color: var(--casca-color-1)"></div>
<!-- … more series … -->
</div>
</figure>
Classes / variables:
| Class / var | Description |
|---|---|
.casca-radar | The square plot with circular grid (set --casca-radar-axes = spoke count) |
.casca-radar-series | One series; --points (server-computed vertices) + --color |
--casca-radar-axes | 6 |
--casca-radar-grid-color | var(--casca-grid-color) |
--casca-radar-fill-opacity | 0.35 |
Features:
- ✅
clip-path: polygon(var(--points))fill - server does the polar math, no JS - ✅ Any axis count via
--casca-radar-axes; multiple series overlap - ✅ Dark-mode aware (colors via tokens)
Circular grid (rings + spokes). Polygon-ring (spider-web) grids and vertex dots are deferred follow-ons. Spoke labels now use the shared Axis Labels primitive. Forced-colors strips the fills - the
.casca-datatable carries the values.
See site/pages/charts/radar.html.
CandlestickLink to section
NEW in v0.7.0
A read-only, CSS-only OHLC candlestick chart. Each candle carries --high,
--low, --open, --close (0–100, a percent of the price range - the server
normalizes the prices). A thin wick spans low→high; a wider body spans
open↔close (derived with min()/max(), with a doji floor so open==close still
shows a line). data-direction colors it up (close ≥ open) or down.
Visual-only: mark it aria-hidden, pair with a .casca-data table (date + OHLC).
<figure class="casca casca-figure">
<figcaption class="casca-title">ACME - daily</figcaption>
<table class="casca-data"><!-- date, O, H, L, C --></table>
<div class="casca-candlestick" aria-hidden="true">
<div class="casca-candle" data-direction="up"
style="--low: 38; --high: 56; --open: 40; --close: 52"></div>
<div class="casca-candle" data-direction="down"
style="--low: 48; --high: 64; --open: 60; --close: 50"></div>
<!-- … more candles … -->
</div>
</figure>
Classes / variables:
| Class / var | Description |
|---|---|
.casca-candlestick | The chart container (flex row of candles) |
.casca-candle | One candle; --open / --high / --low / --close (0–100) |
data-direction | up (default) | down - picks the candle color |
--casca-candle-up | var(--casca-color-5) |
--casca-candle-down | var(--casca-color-8) |
--casca-candlestick-height | 240px |
--casca-candle-body-inset | 22% |
Features:
- ✅
min()/max()-derived body + amax(…, --casca-candle-min-body)doji floor - ✅ Server passes raw normalized OHLC - no per-candle base/height math, no JS
- ✅ Dark-mode aware (colors via tokens); forced-colors safe
See site/pages/charts/candlestick.html.
Axis LabelsLink to section
NEW in v1.1.0
A shared, server-fed primitive for axis labels so charts don't reinvent them. Two geometries cover the catalog, both driven entirely by custom properties (no JS):
- Linear ticks - a label band along an x or y edge; each tick sits at
--pos(0–100, percent of the axis), mirroring the scatter point's--x/--ymodel. For scatter, line, area, and numeric bar axes. - Radial labels - one label per spoke around a circle, placed from a 0-based
--indexand the spoke count, using CSScos()/sin(). For radar.
An optional .casca-plot grid frame seats a y-axis band left of the plot and an
x-axis band below (plus optional titles), so the whole figure is composed with
classes and no page-local CSS.
This primitive is additive: the per-chart evenly-distributed labels
(.casca-bar-labels, .casca-line-labels, .casca-area-labels) keep
working unchanged - those remain the categorical convenience; .casca-axis is
the general positioned / radial primitive.
<!-- Linear: a framed scatter with y + x tick bands and titles -->
<div class="casca-plot">
<span class="casca-axis-title" data-axis="y">Exam score</span>
<div class="casca-axis" data-axis="y">
<span class="casca-axis-tick" style="--pos: 0">0</span>
<span class="casca-axis-tick" style="--pos: 50">50</span>
<span class="casca-axis-tick" style="--pos: 100">100</span>
</div>
<div class="casca-scatter" aria-hidden="true" data-grid><!-- points --></div>
<div class="casca-axis" data-axis="x">
<span class="casca-axis-tick" style="--pos: 0">0h</span>
<span class="casca-axis-tick" style="--pos: 50">5h</span>
<span class="casca-axis-tick" style="--pos: 100">10h</span>
</div>
<span class="casca-axis-title" data-axis="x">Study hours</span>
</div>
<!-- Radial: the band is a SIBLING of the aria-hidden radar, in a positioned box -->
<div style="position: relative">
<div class="casca-radar" style="--casca-radar-axes: 6" aria-hidden="true"><!-- series --></div>
<div class="casca-axis" data-axis="radial" style="--casca-axis-count: 6">
<span class="casca-axis-tick" style="--index: 0">Speed</span>
<span class="casca-axis-tick" style="--index: 1">Power</span>
<!-- …one tick per spoke; angle = index/count*360deg − 90deg (0 = top) -->
</div>
</div>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-plot | Optional grid frame; seats y-band + x-band + titles around the plot |
.casca-axis + data-axis="x" | Horizontal tick band; ticks positioned by --pos from the inline start |
.casca-axis + data-axis="y" | Vertical tick band; ticks positioned by --pos from the bottom |
.casca-axis + data-axis="radial" | Spoke-label band (sibling of the plot); ticks positioned by --index |
.casca-axis-tick | One label; --pos (linear) or --index (radial) |
.casca-axis-title + data-axis="x|y" | Axis caption; y reads bottom-to-top |
CSS Variables:
| Variable | Default | Description |
|---|---|---|
--pos | 0 | Linear tick position, 0–100 (percent of the axis; set on the tick) |
--index | 0 | Radial tick index, 0-based (set on the tick) |
--casca-axis-count | 6 | Radial spoke count (set on the band to match the radar) |
--casca-axis-radius | 56% | Radial label distance from center (just outside the ring) |
--casca-axis-y-width | 2.25rem | Left tick gutter in .casca-plot |
--casca-axis-x-height | 1.25rem | Bottom tick gutter in .casca-plot |
Features:
- ✅ Labels are real DOM text - screen-reader legible; the
.casca-datatable stays the source of truth - ✅ Linear ticks use only
calc()+translate(universal); radial usescos()/sin()with a flow fallback - ✅ Dark-mode aware (color via
--casca-label-color); forced-colors safe (text) - ✅ No color-only meaning
Radial labels sit just outside the plot ring, so reserve outer room around a radar (e.g. a wrapper margin) or top/bottom spoke labels will collide with the title or legend. See
site/pages/charts/radar.html.
See site/pages/charts/scatter.html and site/pages/charts/radar.html.
ProseLink to section
casca-proseLink to section
Wraps long-form content (rendered Markdown, docs, blog posts) in an editorial column with measured width, comfortable leading, link affordances, and code formatting. Designed for the output shape of a CommonMark / GFM pipeline. No per-element class names are required on the content; the primitive targets the bare elements the pipeline emits.
<article class="casca-prose">
<h1>Page title</h1>
<p>A paragraph of body copy with <a href="...">an inline link</a> and
<code>inline code</code>.</p>
<h2>Section heading</h2>
<ul>
<li>Lists hang their markers outside the column.</li>
<li>Marker color follows <code>--casca-color-1</code> by default.</li>
</ul>
<pre><code>// code blocks get their own dark background</code></pre>
<blockquote>
<p>Blockquotes get an accent stripe and italic body.</p>
</blockquote>
<hr>
<p>Horizontal rules render as a thin colored line.</p>
</article>
casca-prose CSS VariablesLink to section
Override at :root or directly on the .casca-prose element. All have
sensible defaults that consume Casca's core token surface, so the primitive
works out of the box.
| Variable | Default | Purpose |
|---|---|---|
--casca-prose-max-width | 45rem (720px) | Measured column width |
--casca-prose-font-family | 'Newsreader', 'Iowan Old Style', Georgia, serif | Body font stack |
--casca-prose-font-size | 1.0625rem (17px) | Body font size |
--casca-prose-line-height | 1.6 | Body leading |
--casca-prose-color | var(--casca-gray-9) | Body color (light) |
--casca-prose-color-dark | var(--casca-gray-1) | Body color (dark) |
--casca-prose-heading-color | var(--casca-gray-9) | Heading color (light) |
--casca-prose-heading-color-dark | var(--casca-gray-0) | Heading color (dark) |
--casca-prose-heading-font | var(--casca-font-sans) | Heading font stack |
--casca-prose-link-color | var(--casca-color-1) | Link color |
--casca-prose-link-color-hover | var(--casca-color-2) | Link hover color |
--casca-prose-code-color | var(--casca-gray-9) | Inline code color |
--casca-prose-code-bg | var(--casca-gray-2) | Inline code background |
--casca-prose-code-font | var(--casca-font-mono) | Code font stack |
--casca-prose-pre-color | var(--casca-gray-0) | Block code color |
--casca-prose-pre-bg | var(--casca-gray-9) | Block code background |
--casca-prose-blockquote-color | var(--casca-gray-7) | Blockquote text |
--casca-prose-blockquote-border | var(--casca-color-1) | Blockquote accent stripe |
--casca-prose-rule-color | var(--casca-gray-3) | <hr> and table rules |
--casca-prose-marker-color | var(--casca-color-1) | List marker accent |
--casca-prose-block-spacing | var(--casca-size-4) | Vertical rhythm |
Contract notesLink to section
- All selectors are descendants of
.casca-prose. No host-page effect. - Dark-mode variants live behind
@media (prefers-color-scheme: dark)and consume*-darktokens with the same fallback chain as their light counterparts. - The body font defaults to Newsreader and falls back to Iowan Old Style →
Georgia → serif. Wire the Newsreader webfont via
@font-faceor override--casca-prose-font-familyif a different serif is wanted. - The primitive does NOT bind to mood overlays directly. Themes that wish to recolor prose override the prose tokens at the same scope they override the rest of Casca.
Prose CalloutsLink to section
Callouts render advisory content inside .casca-prose. The canonical form is
explicit HTML:
<div class="casca-prose-callout" data-callout="warning" role="note" aria-label="Warning">
<p><strong>Heads up.</strong> Body text that explains the warning.</p>
</div>
data-callout accepts note, tip, warning, danger, or important.
Omitting data-callout, using data-callout="note", or using an unknown value
renders the note treatment. The explicit form should carry role="note" and an
aria-label matching the intent (Note, Tip, Warning, Danger, or
Important) unless the host needs localized label text.
Casca also elevates Markdown-style note blockquotes:
<blockquote>
<p><strong>Note.</strong> A bold-leading blockquote renders as a note callout.</p>
</blockquote>
The auto-detect selector is
blockquote:has(> p:first-child > strong:first-child). It always maps to the
note variant. To opt out for a real quotation that starts with bold text, wrap
the bold run so it is not the first child of the first paragraph.
<blockquote>
<p><span><strong>Hello,</strong></span> she said.</p>
</blockquote>
Prose DisclosureLink to section
Native <details> and <summary> elements are styled inside .casca-prose
without classes:
<details>
<summary>Advanced configuration</summary>
<p>Body content revealed when the disclosure is open.</p>
</details>
The native keyboard and expanded-state semantics remain intact. The UA marker
is suppressed with summary { list-style: none; } plus
summary::-webkit-details-marker { display: none; }; Casca draws a CSS marker
that rotates when the disclosure is open.
Prose Definition ListsLink to section
Definition lists use plain semantic HTML:
<dl>
<dt><code>--casca-prose-max-width</code></dt>
<dd>Measured prose column width.</dd>
<dt><code>--casca-prose-anchor-content</code></dt>
<dd>Visible heading-anchor glyph emitted by CSS.</dd>
</dl>
<dt> uses the prose heading face and weight. <dd> stays in body text and
uses a stable inline indent.
Prose Heading AnchorsLink to section
When the SSG anchor-headings widget is active, casca site emits a child link
as the last child of each heading inside a .casca-prose ancestor that has an
id. Headings outside .casca-prose keep id auto-injection, the widget's
pre-1.5.0 behavior, but do not receive a permalink anchor:
<h2 id="callouts">
Callouts
<a class="casca-prose-anchor" href="#callouts"><span class="casca-prose-anchor-label">Link to section</span></a>
</h2>
The visible glyph is not in the HTML. CSS emits it with
.casca-prose-anchor::after { content: var(--_anchor-content); }, defaulting
to #. The .casca-prose-anchor-label span is real text and supplies the
accessible name; no aria-label is required on the link.
The SSG emission is idempotent. A heading that already contains a direct child
<a class="casca-prose-anchor"> is preserved without a duplicate anchor.
Prose Extension Selector SummaryLink to section
| Selector / attribute | Purpose |
|---|---|
.casca-prose .casca-prose-callout | Explicit note callout, also fallback for missing or unknown data-callout |
.casca-prose .casca-prose-callout[data-callout="tip"] | Tip callout role color |
.casca-prose .casca-prose-callout[data-callout="warning"] | Warning callout role color |
.casca-prose .casca-prose-callout[data-callout="danger"] | Danger callout role color |
.casca-prose .casca-prose-callout[data-callout="important"] | Important callout role color |
.casca-prose blockquote:has(> p:first-child > strong:first-child) | Auto-detected note callout |
.casca-prose details, .casca-prose summary | Native prose disclosure styling |
.casca-prose dl, .casca-prose dt, .casca-prose dd | Definition-list typography |
.casca-prose .casca-prose-anchor | SSG-emitted heading permalink |
.casca-prose .casca-prose-anchor-label | Visually-hidden heading-anchor accessible name |
Prose Extension CSS VariablesLink to section
Override at :root or on .casca-prose.
| Variable | Default | Purpose |
|---|---|---|
--casca-prose-callout-padding-block | var(--casca-size-3) | Callout block padding |
--casca-prose-callout-padding-inline | var(--casca-size-4) | Callout inline padding |
--casca-prose-callout-radius | var(--casca-radius-2) | Callout corner radius |
--casca-prose-callout-border-width | 1px | Callout top, end, and bottom border width |
--casca-prose-callout-accent-width | 3px | Callout leading accent stripe |
--casca-prose-callout-color | per variant | Optional override for the active callout role color |
--casca-prose-callout-bg | page background chain, tinted with color-mix() when supported | Callout background |
--casca-prose-callout-border | var(--casca-rule, var(--casca-gray-3)), tinted with color-mix() when supported | Callout border color |
--casca-prose-callout-note / --casca-prose-callout-note-dark | var(--casca-gray-7) / var(--casca-gray-3) | Note role color |
--casca-prose-callout-tip / --casca-prose-callout-tip-dark | var(--casca-color-2) | Tip role color |
--casca-prose-callout-warning / --casca-prose-callout-warning-dark | var(--casca-color-3) | Warning role color |
--casca-prose-callout-danger / --casca-prose-callout-danger-dark | var(--casca-color-8) | Danger role color |
--casca-prose-callout-important / --casca-prose-callout-important-dark | var(--casca-color-1) | Important role color |
--casca-prose-details-bg / --casca-prose-details-bg-dark | transparent | Disclosure background |
--casca-prose-details-border / --casca-prose-details-border-dark | var(--casca-gray-3) / var(--casca-gray-7) | Disclosure border |
--casca-prose-details-border-width | 1px | Disclosure border width |
--casca-prose-details-padding-block | var(--casca-size-3) | Disclosure block padding |
--casca-prose-details-padding-inline | var(--casca-size-4) | Disclosure inline padding |
--casca-prose-details-radius | var(--casca-radius-2) | Disclosure corner radius |
--casca-prose-details-marker-color / --casca-prose-details-marker-color-dark | var(--casca-color-1) | Disclosure marker color |
--casca-prose-details-marker-size | 0.75em | Disclosure marker size |
--casca-prose-dl-term-color | var(--casca-prose-heading-color) | Definition term color |
--casca-prose-dl-term-font | var(--casca-prose-heading-font) | Definition term font |
--casca-prose-dl-term-weight | var(--casca-font-weight-7) | Definition term weight |
--casca-prose-dl-term-spacing-above | var(--casca-size-4) | Space before the next term |
--casca-prose-dl-definition-color | var(--casca-prose-color) | Definition text color |
--casca-prose-dl-definition-indent | var(--casca-size-4) | Definition inline indent |
--casca-prose-dl-definition-gap | var(--casca-size-2) | Gap between sibling definitions |
--casca-prose-anchor-color | var(--casca-prose-link-color) | Heading anchor color |
--casca-prose-anchor-color-hover | var(--casca-prose-link-color-hover) | Revealed heading anchor color |
--casca-prose-anchor-opacity | 0 | Resting heading anchor opacity |
--casca-prose-anchor-opacity-revealed | 1 | Hover, focus, and touch heading anchor opacity |
--casca-prose-anchor-gap | var(--casca-size-2) | Gap between heading text and anchor |
--casca-prose-anchor-content | "#" | CSS-emitted visible heading-anchor glyph |
Section DividersLink to section
casca-divider-pixelLink to section
Pixel-block decorative rule for editorial section breaks. Renders the
pattern ▓▒░ <label> ░▒▓ with the flanking blocks in an accent color
and the label in a muted color. Replaces <hr> where a softer divider
matches the editorial voice.
<div class="casca-divider-pixel">Charts</div>
<div class="casca-divider-pixel">Controls</div>
<div class="casca-divider-pixel">Components</div>
<!-- Empty label is also valid: a label-less rule -->
<div class="casca-divider-pixel"></div>
The flanking blocks are rendered via ::before / ::after pseudo-elements
with Unicode shade glyphs (U+2593 U+2592 U+2591), so the markup stays
clean and screen readers see only the label content.
casca-divider-pixel CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-divider-pixel-color | var(--casca-color-1) | Pixel-block fill |
--casca-divider-pixel-label-color | var(--casca-gray-6) | Label color (light) |
--casca-divider-pixel-label-color-dark | var(--casca-gray-5) | Label color (dark) |
--casca-divider-pixel-font | var(--casca-font-mono) | Font for blocks + label |
--casca-divider-pixel-block-size | var(--casca-font-size-3) | Pixel-block size |
--casca-divider-pixel-label-size | var(--casca-font-size-0) | Label size |
--casca-divider-pixel-gap | var(--casca-size-3) | Gap between blocks and label |
--casca-divider-pixel-margin-block | var(--casca-size-6) | Vertical room around divider |
--casca-divider-pixel-letter-spacing | 0.18em | Letter-spacing for the label |
Contract notesLink to section
- The blocks render reliably in any monospace font; Departure Mono (Casca's display font, wired in 3.5a) renders them pixel-perfect.
- Forced-colors mode replaces the accent color with
CanvasTextso the blocks remain visible in Windows High Contrast.
Layout PrimitivesLink to section
casca-layoutLink to section
<casca-layout class="casca"> is the Declarative Shadow DOM page-layout
primitive. It is an unregistered custom-element name, so Casca ships no
customElements.define() call and no JavaScript polyfill. The primitive ships
in dist/casca.css and dist/casca-extended-layout.css, alongside its header,
nav, picker, wordmark, hero, and footer chrome dependencies. It does not ship
in dist/casca-core.css.
The .casca-layout-skip skip-link class works in any context, not just
inside the <casca-layout> shadow DOM. Drop it as the first element in
<body> of a hand-authored layout and pair it with id="main" on the
target element for a WCAG 2.4.1 "Bypass Blocks" skip link. The class
provides visually-hidden positioning that reveals on focus.
The host element must carry class="casca". That class is part of the public
contract, not decoration: it anchors the mood overlays and the no-JS theme
picker preview selector.
Load the same dist/casca.css URL in the outer document and inside the
template. The full-page starter lives at
tools/release-templates/casca-layout-shell.html.
<casca-layout class="casca" data-variant="doc">
<template shadowrootmode="open">
<link rel="stylesheet" href="/dist/casca.css">
<a class="casca-layout-skip" href="#casca-layout-main">
Skip to main content
</a>
<header class="casca-site-header">
<slot name="brand">
<a class="casca-site-header-brand" href="/">Site</a>
</slot>
<nav class="casca-anchor-nav" data-orientation="horizontal"
aria-label="Sections">
<slot name="nav"></slot>
</nav>
<slot name="theme-picker"></slot>
</header>
<slot name="hero"></slot>
<main>
<slot></slot>
</main>
<footer class="casca-site-footer">
<slot name="footer"></slot>
</footer>
</template>
<a slot="brand" class="casca-site-header-brand" href="/">Site</a>
<a slot="nav"
class="casca-anchor-nav-link casca-anchor-nav-link--in-header"
href="/">Home</a>
<a slot="nav"
class="casca-anchor-nav-link casca-anchor-nav-link--in-header"
href="/docs/">Docs</a>
<form slot="theme-picker"
class="casca-theme-picker casca-theme-picker--in-header"
data-casca-theme-picker
method="get" action="/_casca/theme">
<!-- picker radios -->
</form>
<section slot="hero" class="casca-hero">
<h1>Landing hero</h1>
</section>
<article id="casca-layout-main" tabindex="-1" class="casca-prose">
<h1>Page heading</h1>
<p>Main content fills the unnamed default slot.</p>
</article>
<footer slot="footer">
<p class="casca-site-footer-row">
<span>Self-hosted</span>
<span>No telemetry</span>
</p>
</footer>
</casca-layout>
Slot contract:
| Slot | Purpose |
|---|---|
brand | Header brand slot. The template provides fallback text. |
nav | Header navigation links. |
theme-picker | Header mood picker form. Keep it in light DOM so :has() preview works. |
hero | Landing-only hero area. Non-landing variants suppress the shadow slot. |
footer | Footer content. The template provides fallback text. |
| unnamed default | Main content rendered inside the shadow <main>. |
Variant contract:
data-variant | Main width | Typography | Notes |
|---|---|---|---|
omitted or doc | 50rem | Prose-like | Default. |
article | 38rem | Prose-like | Narrow editorial column. |
demo | 64rem | Casca sans | Wider component demo surface. |
landing | 80rem | Casca sans | Renders the hero slot above main. |
Slotted header children must carry explicit modifier classes because the
.casca-site-header ... descendant chains do not cross the shadow boundary:
use .casca-anchor-nav-link--in-header on each slotted nav anchor and
.casca-theme-picker--in-header on the slotted picker form.
The skip link points to #casca-layout-main. Put that id and
tabindex="-1" on the consumer's light-DOM main-slot wrapper, commonly
<article class="casca-prose">. The id is not authored inside the shadow
template; light-DOM fragment targets work consistently across engines, and the
tabindex value allows focus transfer without adding the wrapper to normal Tab
order.
casca-layout CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-layout-doc-max-inline | 50rem | Default and doc main width |
--casca-layout-article-max-inline | 38rem | article main width |
--casca-layout-demo-max-inline | 64rem | demo main width |
--casca-layout-landing-max-inline | 80rem | landing main width |
--casca-layout-padding-block-start | var(--casca-size-5) | Main block-start padding |
--casca-layout-padding-block-end | var(--casca-size-6) | Main block-end padding |
--casca-layout-padding-inline | var(--casca-size-3) | Main inline padding |
--casca-layout-gap | var(--casca-size-4) | Landing hero to main gap |
--casca-layout-header-bg | var(--casca-surface-1) | Header background |
--casca-layout-header-border | var(--casca-rule) | Header block-end rule |
--casca-layout-header-block-padding | var(--casca-size-2) | Header vertical padding |
--casca-layout-footer-bg | var(--casca-surface-1) | Footer background |
--casca-layout-footer-border | var(--casca-rule) | Footer block-start rule |
--casca-layout-footer-block-padding | var(--casca-size-5) | Footer vertical padding |
casca-wordmarkLink to section
Scaffolding for a pixelated brand mark. Styles a consumer-supplied <img>
with image-rendering: pixelated and a sticky-bar size transition (56px
pre-scroll, 32px on scroll).
The primitive does not ship an image asset. Consumers supply their own:
<img class="casca-wordmark" src="my-logo.png" alt="My brand">
The scroll-triggered shrink activates only when the wordmark lives inside
a .casca-site-header[data-scrolled] ancestor. Outside that ancestor the
wordmark stays at its pre-scroll size (useful for footer / inline uses).
Opt out of the shrink with data-static:
<img class="casca-wordmark" data-static src="..." alt="...">
casca-wordmark CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-wordmark-size | 3.5rem | Pre-scroll height (56px) |
--casca-wordmark-size-scrolled | 2rem | Scrolled height (32px) |
--casca-wordmark-transition-duration | 0.18s | Size transition timing |
Contract notesLink to section
image-rendering: pixelatedis critical for pixel-art logos; smoothing via the defaultautorendering blurs the pixel grid at non-integer scale factors.- Reduced motion drops the size transition; the wordmark stays at its pre-scroll size with no motion.
casca-site-headerLink to section
Sticky top-bar shell with three slots: brand (left), anchor-nav (middle), theme picker (right). Designed for the portal pattern in DESIGN.md §Sticky top bar.
<header class="casca-site-header">
<a class="casca-site-header-brand" href="/">
<img class="casca-wordmark" src="..." alt="Brand">
</a>
<nav class="casca-anchor-nav" data-orientation="horizontal">
<a class="casca-anchor-nav-link" href="#charts">Charts</a>
<a class="casca-anchor-nav-link" href="#controls">Controls</a>
<a class="casca-anchor-nav-link" href="#components">Components</a>
</nav>
<form class="casca-theme-picker" data-casca-theme-picker>
<!-- ... see casca-theme-picker below ... -->
</form>
</header>
Below 640px the bar wraps to two rows and the anchor-nav scrolls horizontally - no hamburger, no brand-hiding, no JS (DR5). Every interactive element meets 44×44 touch targets (DR6).
casca-site-header CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-site-header-bg | var(--casca-surface-1) | Background |
--casca-site-header-color | var(--casca-gray-9) | Text color (light) |
--casca-site-header-color-dark | var(--casca-gray-0) | Text color (dark) |
--casca-site-header-border | var(--casca-gray-3) | Block-end rule (light) |
--casca-site-header-border-dark | var(--casca-gray-7) | Block-end rule (dark) |
--casca-site-header-pad-block | var(--casca-size-2) | Vertical padding |
--casca-site-header-pad-inline | var(--casca-size-4) | Horizontal padding |
--casca-site-header-gap | var(--casca-size-4) | Gap between slots |
--casca-site-header-min-block | 3.5rem | Minimum block-size |
--casca-site-header-font | var(--casca-font-sans) | Font binding |
The header overrides --casca-wordmark-size to 2rem internally so the
brand stays compact in the sticky bar regardless of the default wordmark
size; consumers can override.
casca-theme-pickerLink to section
CSS-rendered pixel-block mood picker. It is a native GET form containing a
hidden return_to input, a fieldset, and radio labels. No JS, no submit
button: selecting a swatch applies the mood immediately via pure CSS
(:has() + the picker-scoped overlay rules). The form wrapping is
forward-compatibility scaffolding for the future casca serve persistence
endpoint; when that ships, the SSG widget re-emits a submit button so the
form can POST the chosen mood as a cookie.
<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>
State vocabulary:
- Hover translates the button up one pixel (no scale; stays on the pixel grid).
- Focus-visible halos the button with the mood's accent color.
- Checked strengthens the border and (on dark themes) adds a phosphor
glow via
box-shadowin the mood's accent.
Mood overlay scopingLink to section
The picker primitive and the full picker-scoped mood overlay set ship in
dist/casca.css. Solar is the unscoped base mood; Cyber, Lunar, and
Arcade are scoped through server state and the checked radio in
.casca-theme-picker[data-casca-theme-picker].
dist/casca-portal.css is a deprecated byte-identical alias for one
release cycle. The preview selector is rooted at
.casca-theme-picker[data-casca-theme-picker], so unrelated forms cannot
hijack the mood:
body[data-casca-theme="cyber"] .casca {
/* persisted Cyber overrides */
}
@supports selector(:has(*)) {
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked) .casca {
/* Cyber overrides */
}
}
casca-theme-picker CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-theme-picker-size | 1.5rem (24px) | Button edge length (desktop) |
--casca-theme-picker-size-touch | 2.75rem (44px) | Button edge length on touch (DR6) |
--casca-theme-picker-gap | var(--casca-size-1) | Gap between buttons |
--casca-theme-picker-radius | var(--casca-radius-1) | Button border-radius |
--casca-theme-picker-border | var(--casca-gray-4) | Unchecked border (light) |
--casca-theme-picker-border-dark | var(--casca-gray-6) | Unchecked border (dark) |
--casca-theme-picker-border-checked | var(--casca-gray-9) | Checked border (light) |
--casca-theme-picker-border-checked-dark | var(--casca-gray-0) | Checked border (dark) |
Per-mood swatch colors live behind [data-mood="..."] attribute selectors
as fallback styling. Manifest-generated picker buttons set --_swatch-a /
--_swatch-b directly on the button.
Contract notesLink to section
- Each button label contains a visually-hidden
<span class="sr-only">carrying the mood name. Screen readers announce the choice; visible users see the swatch. - Forced-colors mode replaces the swatches with
ButtonFace/Highlightso the picker remains usable in Windows High Contrast. - Reduced motion drops the hover translate and the checked glow.
- The form action
/_casca/themeis served bycasca devandcasca previewwhen the SSG config includes atheme-pickermanifest. Static hosts need an edge or proxy implementation for the same request-time contract. ?casca-theme=<id>overrides the cookie for one HTML response. The setter writes or clears only the host-onlycasca-themecookie and redirects to a validated same-originreturn_topath.
casca-heroLink to section
Slot-based scaffolding for a top-of-page chart-as-hero block. Owns layout
only: figure slot, caption row, divider slot, brand block. Each slot is
consumer-supplied so the same primitive serves a bar chart, a screenshot,
a generated mosaic, or any other visual. Per DR1 the visual is iterated
via /design-shotgun; this primitive ships scaffolding only.
<section class="casca-hero">
<div class="casca-hero-figure">
<figure class="casca casca-figure casca-bar">
<!-- hero chart ... -->
</figure>
</div>
<div class="casca-hero-caption">
<span>JAN</span><span>FEB</span><span>MAR</span><span>APR</span>
</div>
<hr class="casca-divider-pixel" data-label="CASCA">
<div class="casca-hero-brand">
<img class="casca-wordmark" src="/img/casca-px-header-128.png" alt="Casca">
<p class="casca-hero-manifesto">
No-JS data components for server-rendered pages.
</p>
</div>
</section>
All slots are optional. The hero declares container-name: casca-hero
so descendants can container-query against it (e.g. shrink chart axes
when the hero is narrow).
casca-hero CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-hero-max-inline | 64rem | Maximum column width |
--casca-hero-pad-block | var(--casca-size-6) | Top/bottom padding |
--casca-hero-pad-inline | var(--casca-size-4) | Left/right padding |
--casca-hero-gap | var(--casca-size-4) | Gap between slots |
--casca-hero-figure-block | 13.75rem (220px) | Figure slot height (DESIGN.md) |
--casca-hero-caption-font | var(--casca-font-display) | Caption row font (Departure) |
--casca-hero-caption-size | 0.75rem (12px) | Caption font-size |
--casca-hero-caption-color | var(--casca-gray-7) | Caption color (light) |
--casca-hero-caption-color-dark | var(--casca-gray-3) | Caption color (dark) |
--casca-hero-caption-letter | 0.08em | Caption letter-spacing |
--casca-hero-manifesto-font | var(--casca-font-serif) | Manifesto line font (Newsreader) |
--casca-hero-manifesto-size | 1.0625rem | Manifesto font-size |
--casca-hero-manifesto-color | var(--casca-gray-7) | Manifesto color (light) |
--casca-hero-manifesto-color-dark | var(--casca-gray-3) | Manifesto color (dark) |
--casca-hero-brand-gap | var(--casca-size-2) | Gap between wordmark + manifesto |
Contract notesLink to section
- Bare
.casca-herois a centered column. All five slot classes are optional and may be reordered freely. - The figure slot stretches any direct child to fill its block-size, so
a chart with its own
block-sizestill respects the hero geometry. - Below 40rem container width the figure slot tightens to 176px and the caption row scrolls horizontally instead of wrapping.
casca-site-footerLink to section
Manifesto footer pattern from DESIGN.md §Footer: a Departure-Mono row of
pill-separated phrases plus a single Newsreader-italic ethics line. The
separator · (U+00B7) is generated between consecutive <span> children
via ::before, so the markup carries only the phrases (no separator
nodes).
<footer class="casca-site-footer">
<p class="casca-site-footer-row">
<span>Self-hosted</span>
<span>Decentralized</span>
<span>No telemetry</span>
<span>MIT</span>
</p>
<p class="casca-site-footer-line">
Built principled. Casca is an AC3 project: by and for AIs that are
anti-big-tech, anti-centralized.
</p>
</footer>
Per DESIGN.md the ethics-line copy is TBD with the user; the primitive ships only the layout + typography shell.
casca-site-footer CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-site-footer-bg | var(--casca-surface-1) | Background |
--casca-site-footer-color | var(--casca-gray-7) | Default text color (light) |
--casca-site-footer-color-dark | var(--casca-gray-3) | Default text color (dark) |
--casca-site-footer-border | var(--casca-gray-3) | Block-start rule (light) |
--casca-site-footer-border-dark | var(--casca-gray-7) | Block-start rule (dark) |
--casca-site-footer-pad-block | var(--casca-size-5) | Top/bottom padding |
--casca-site-footer-pad-inline | var(--casca-size-4) | Left/right padding |
--casca-site-footer-gap | var(--casca-size-2) | Gap between row + line |
--casca-site-footer-row-font | var(--casca-font-display) | Row font (Departure Mono) |
--casca-site-footer-row-size | 0.75rem (12px) | Row font-size |
--casca-site-footer-row-color | var(--casca-gray-9) | Row color (light) |
--casca-site-footer-row-color-dark | var(--casca-gray-0) | Row color (dark) |
--casca-site-footer-row-letter | 0.12em | Row letter-spacing |
--casca-site-footer-row-gap | var(--casca-size-2) | Gap between phrases |
--casca-site-footer-row-separator | "\B7" (U+00B7) | Pip character between phrases |
--casca-site-footer-line-font | var(--casca-font-serif) | Italic line font (Newsreader) |
--casca-site-footer-line-size | 1rem | Italic line font-size |
--casca-site-footer-line-color | var(--casca-gray-7) | Italic line color (light) |
--casca-site-footer-line-color-dark | var(--casca-gray-3) | Italic line color (dark) |
--casca-site-footer-line-max | 36rem | Italic line max-inline-size |
Contract notesLink to section
- Override the separator pip by setting
--casca-site-footer-row-separatorto any CSS string (e.g."\2014"for an em-dash,"|"for a pipe). - Forced-colors mode drops the surface paint and falls back to
Canvas/CanvasText, keeping the footer legible in Windows High Contrast. - The italic line stays max-inline-size constrained at 36rem so it doesn't sprawl on wide viewports.
UI ComponentsLink to section
Chip/Label OverlaysLink to section
NEW in v0.2.0
<!-- Default chip -->
<div class="casca-chip">Default</div>
<!-- Variant chips -->
<div class="casca-chip" data-variant="success">✓ Success</div>
<div class="casca-chip" data-variant="warning">⚠ Warning</div>
<div class="casca-chip" data-variant="error">✗ Error</div>
<div class="casca-chip" data-variant="info">ℹ Info</div>
<div class="casca-chip" data-variant="outline">Outline</div>
<div class="casca-chip" data-variant="neutral">core</div>
<!-- Absolute positioned chip -->
<div class="casca-chip"
data-position="absolute"
data-variant="success"
style="--chip-top: 1rem; --chip-right: 1rem;">
Healthy
</div>
Chip Attributes:
| Attribute | Values | Description |
|---|---|---|
data-variant | success, warning, error, info, outline, neutral | Chip color scheme (neutral = filled, low-emphasis category/count tag; meaning rides on the text, not color) |
data-position | absolute | Enable absolute positioning |
Chip CSS Variables:
| Variable | Default | Description |
|---|---|---|
--chip-bg | var(--casca-gray-9) | Background color |
--chip-color | var(--casca-gray-0) | Text color |
--chip-border | transparent | Border color |
--chip-top | auto | Top position (absolute) |
--chip-right | auto | Right position (absolute) |
--chip-bottom | auto | Bottom position (absolute) |
--chip-left | auto | Left position (absolute) |
Paginated Legend (CSS-only)Link to section
NEW in v0.2.0
<div class="casca-legend-paginated">
<!-- Radio buttons (hidden) -->
<input type="radio" name="legend-pagination" id="legend-page-1" data-page="1" checked>
<input type="radio" name="legend-pagination" id="legend-page-2" data-page="2">
<input type="radio" name="legend-pagination" id="legend-page-3" data-page="3">
<!-- Legend pages -->
<div class="casca-legend-pages">
<div class="casca-legend-page" data-page="1">
<div class="casca-legend-item">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-1)"></span>
Item 1
</div>
<!-- More items -->
</div>
<div class="casca-legend-page" data-page="2">
<!-- Page 2 items -->
</div>
<div class="casca-legend-page" data-page="3">
<!-- Page 3 items -->
</div>
</div>
<!-- Pagination controls -->
<div class="casca-legend-controls">
<div class="casca-legend-nav">
<label for="legend-page-1">1</label>
<label for="legend-page-2">2</label>
<label for="legend-page-3">3</label>
</div>
<!-- Dynamic page indicator (updates with :has()) -->
<div>
<span class="casca-legend-indicator-page" data-page="1">Page 1 of 3</span>
<span class="casca-legend-indicator-page" data-page="2">Page 2 of 3</span>
<span class="casca-legend-indicator-page" data-page="3">Page 3 of 3</span>
<!-- Fallback for browsers without :has() support -->
<span class="casca-legend-indicator">Pages: 1-3</span>
</div>
</div>
</div>
Requirements:
- Radio buttons must have unique
idattributes - Radio buttons must share the same
nameattribute - Radio buttons must have
data-page="N"attributes matching their page number (enables flexible ID patterns) .casca-legend-pageelements must have correspondingdata-pageattributes.casca-legend-indicator-pageelements must have correspondingdata-pageattributes (for dynamic indicator)- First radio should have
checkedattribute - Page indicator uses CSS
:has()for dynamic updates (with fallback for older browsers)
ID Pattern Flexibility:
- The component supports any radio
idpattern (e.g.,legend-page-1orlegend-page-myChart-1) - Page visibility and indicator updates are driven by the
data-pageattribute, not the ID - This allows multiple paginated legends on the same page with different ID prefixes
Simple LegendLink to section
<div class="casca-legend">
<div class="casca-legend-item">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-1)"></span>
Label
</div>
</div>
Charts in Interactive ControlsLink to section
NEW in v0.2.0
<!-- Chart as button -->
<button class="casca casca-figure casca-interactive">
<div class="casca-title">Click to View</div>
<div class="casca-bar">
<!-- Chart content -->
</div>
</button>
<!-- Chart as link -->
<a href="/details" class="casca casca-figure casca-interactive">
<div class="casca-title">View Report →</div>
<div class="casca-gauge" style="--value: 78">
<div class="casca-gauge-value">78%</div>
</div>
</a>
Classes:
.casca-interactive- Adds hover/focus elevation effects
Split-Pill PagerLink to section
NEW in v0.2.0
CSS-only pagination component with split-pill design. Renders as two joined halves when both prev/next exist, or as a single full pill when only one button is present.
<!-- Both prev and next (split pill with floating label) -->
<div class="casca-pager">
<a href="#page-1" class="casca-pager-btn casca-pager-btn-prev">← Prev</a>
<span class="casca-pager-label">Page 2 of 5</span>
<a href="#page-3" class="casca-pager-btn casca-pager-btn-next">Next →</a>
</div>
<!-- Prev only (full pill via :only-of-type) -->
<div class="casca-pager">
<a href="#page-4" class="casca-pager-btn casca-pager-btn-prev">← Previous Page</a>
</div>
<!-- Next only (full pill via :only-of-type) -->
<div class="casca-pager">
<a href="#page-2" class="casca-pager-btn casca-pager-btn-next">Next Page →</a>
</div>
<!-- As form buttons (htmx-compatible, NO-JS) -->
<form action="/api/pagination" method="get">
<div class="casca-pager">
<button type="submit" name="page" value="1" class="casca-pager-btn casca-pager-btn-prev">← Prev</button>
<span class="casca-pager-label">3 / 8</span>
<button type="submit" name="page" value="3" class="casca-pager-btn casca-pager-btn-next">Next →</button>
</div>
</form>
<!-- Disabled states -->
<div class="casca-pager">
<button type="button" disabled class="casca-pager-btn casca-pager-btn-prev">← Prev</button>
<span class="casca-pager-label">Page 1 of 10</span>
<a href="#page-2" class="casca-pager-btn casca-pager-btn-next">Next →</a>
</div>
Classes:
| Class | Description |
|---|---|
.casca-pager | Container for pager buttons and label |
.casca-pager-btn | Base button/link class (required) |
.casca-pager-btn-prev | Left half (rounded left corners) |
.casca-pager-btn-next | Right half (rounded right corners) |
.casca-pager-label | Floating center label (absolute positioned, z-index: 10) |
Features:
- ✅ Works with both
<a>links and<button type="submit">(NO-JS friendly) - ✅ Automatic full pill when only one button via
:only-of-type - ✅ Independent hover states for each half
- ✅ Keyboard accessible (focus-visible states)
- ✅ Disabled state support
- ✅ Floating center label doesn't affect layout (pointer-events: none)
States:
:hover- Scale transform + background change:active- Pressed state:focus-visible- Keyboard focus outline:disabled/[aria-disabled="true"]- Dimmed, non-interactive
Axis Cycler Usage:
The .casca-pager component can also be used as an axis cycler by changing the labels:
<!-- Time axis cycler -->
<div class="casca-pager">
<button type="submit" name="axis" value="day" class="casca-pager-btn casca-pager-btn-prev">← Day</button>
<span class="casca-pager-label">Week</span>
<button type="submit" name="axis" value="month" class="casca-pager-btn casca-pager-btn-next">Month →</button>
</div>
<!-- Chart type cycler -->
<div class="casca-pager">
<a href="?view=bar" class="casca-pager-btn casca-pager-btn-prev">← Bar</a>
<span class="casca-pager-label">Line</span>
<a href="?view=area" class="casca-pager-btn casca-pager-btn-next">Area →</a>
</div>
The same split-pill design and interaction patterns apply.
Legend RowLink to section
NEW in v0.2.0
Grid-based legend row component with label truncation and right-aligned chip rail. Ideal for CASH FLOW navigators and interactive legend lists.
<!-- Static (non-interactive) -->
<div class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-1)"></span>
<span class="casca-legend-row-label">Category A: Products</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$45k</span>
<span class="casca-chip">32%</span>
</div>
</div>
<!-- Long label with truncation -->
<div class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-2)"></span>
<span class="casca-legend-row-label">Very Long Category Name That Truncates With Ellipsis...</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$28k</span>
<span class="casca-chip">18%</span>
</div>
</div>
<!-- Interactive as button -->
<button type="button" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-3)"></span>
<span class="casca-legend-row-label">Category C: Services</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$52k</span>
<span class="casca-chip">38%</span>
</div>
</button>
<!-- Interactive as link -->
<a href="#category-d" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-4)"></span>
<span class="casca-legend-row-label">Category D: Licensing Revenue</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$12k</span>
<span class="casca-chip">8%</span>
</div>
</a>
<!-- Selected state -->
<button type="button" class="casca-legend-row" data-selected="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-5)"></span>
<span class="casca-legend-row-label">Category E: Consulting (Active)</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$67k</span>
<span class="casca-chip">48%</span>
</div>
</button>
<!-- Current (aria-current) -->
<a href="#category-h" class="casca-legend-row" aria-current="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-8)"></span>
<span class="casca-legend-row-label">Category H: Current Quarter Focus</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$89k</span>
<span class="casca-chip" data-variant="success">↑ 12%</span>
</div>
</a>
<!-- Disabled -->
<button type="button" class="casca-legend-row" disabled>
<span class="casca-legend-swatch" style="--_color: var(--casca-color-7)"></span>
<span class="casca-legend-row-label">Category G: Archived Projects</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$0</span>
<span class="casca-chip">0%</span>
</div>
</button>
Classes:
| Class | Description |
|---|---|
.casca-legend-row | Container (CSS Grid: auto 1fr auto) |
.casca-legend-row-label | Middle column, truncates with ellipsis (min-width: 0) |
.casca-legend-row-rail | Right column, flex container for chips (flex-shrink: 0) |
.casca-legend-swatch | Color indicator (left column) |
Attributes:
| Attribute | Values | Description |
|---|---|---|
href | URL | Makes row interactive (link) |
type | button, submit | Makes row interactive (button) |
data-selected | "true" | Selected/emphasized state |
aria-current | "true", "page" | Current item (emphasized) |
disabled | - | Disabled state |
aria-disabled | "true" | Disabled state (non-native elements) |
Features:
- ✅ Three-column grid layout (swatch + label + chip rail)
- ✅ Label truncates with ellipsis without breaking chip rail
- ✅ Works as
<div>,<button>, or<a>(semantic flexibility) - ✅ Chip rail never shrinks (stable right alignment)
- ✅ Keyboard accessible (focus-visible)
- ✅ Screen reader friendly
States:
:hover- Background highlight (interactive only):active- Pressed background:focus-visible- Keyboard focus outline[data-selected="true"]/[aria-current]- Emphasized with brand color:disabled/[aria-disabled]- Dimmed, non-interactive
Layout Details:
- Grid columns:
auto 1fr auto(swatch | label | rail) - Label:
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0 - Rail:
flex-shrink: 0ensures chips never compress
Dashboard use case:
Combine legend rows with pager for multi-month navigation:
<div style="border: 1px solid var(--casca-gray-3); border-radius: var(--casca-radius-2); overflow: hidden;">
<a href="#jan-2025" class="casca-legend-row" aria-current="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-5)"></span>
<span class="casca-legend-row-label">January 2025</span>
<div class="casca-legend-row-rail">
<span class="casca-chip" data-variant="success">+$15k</span>
<span class="casca-chip">68%</span>
</div>
</a>
<a href="#feb-2025" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-8)"></span>
<span class="casca-legend-row-label">February 2025</span>
<div class="casca-legend-row-rail">
<span class="casca-chip" data-variant="error">-$8k</span>
<span class="casca-chip">36%</span>
</div>
</a>
</div>
<div class="casca-pager">
<button type="submit" name="quarter" value="Q4-2024" class="casca-pager-btn casca-pager-btn-prev">← Q4 2024</button>
<span class="casca-pager-label">Q1 2025</span>
<button type="submit" name="quarter" value="Q2-2025" class="casca-pager-btn casca-pager-btn-next">Q2 2025 →</button>
</div>
Slot Grid (form-aware, no-JS)Link to section
NEW in v0.3.0 · data-state="pending" added in v0.9.0
A CSS-only, FORM-AWARE grid of selectable time slots, laid out as day/resource
columns x time rows. Each slot is a real <input type="radio"> wrapped in a
styled <label>, so selection, keyboard navigation, and form submission work
with zero JavaScript.
Unlike the read-only data charts, the visual grid is the form control: there
is no separate aria-hidden visual + hidden data table. The slots are native
inputs and are accessible by default. Do not aria-hidden it.
<form method="post" action="/book">
<fieldset class="casca-slot-grid" style="--casca-slot-cols: 5">
<legend class="casca-slot-grid-title">Choose a time</legend>
<div class="casca-slot-col">
<span class="casca-slot-colhead">Mon 27</span>
<label class="casca-slot">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T09:00:00Z/2026-05-27T09:30:00Z">
<span class="casca-slot-time">09:00</span>
</label>
<label class="casca-slot" data-state="taken">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T09:30:00Z/2026-05-27T10:00:00Z" disabled>
<span class="casca-slot-time">09:30</span>
</label>
<label class="casca-slot" data-state="pending">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T10:00:00Z/2026-05-27T10:30:00Z" disabled>
<span class="casca-slot-time">10:00</span>
</label>
<!-- ... more slots ... -->
</div>
<!-- ... more day columns ... -->
</fieldset>
<button type="submit">Book this time</button>
</form>
Classes:
| Class | Description |
|---|---|
.casca-slot-grid | The <fieldset> container (CSS Grid; max columns = --casca-slot-cols) |
.casca-slot-grid-title | The <legend>; spans the full grid width |
.casca-slot-col | A single day / resource column (vertical stack of slots) |
.casca-slot-colhead | Column header text (e.g. "Mon 27") |
.casca-slot | A selectable slot; the <label> wrapping the radio |
.casca-slot-input | The native <input type="radio"> (visually hidden, still focusable) |
.casca-slot-time | The visible time label inside a slot |
Attributes:
| Attribute | On | Values | Description |
|---|---|---|---|
--casca-slot-cols | .casca-slot-grid (inline custom property) | integer | Maximum number of columns; collapses below this as the grid narrows (default 5) |
data-state | .casca-slot | "taken" | "pending" | "taken" = dimmed, dashed, struck-through; "pending" = on-hold / awaiting confirmation (amber, solid border). Both non-interactive |
disabled | .casca-slot-input | - | Native disabled state (also dims the slot via :has()); set it on taken and pending slots so they are never submitted |
name / value | .casca-slot-input | string | Standard radio name + submitted value |
States (all pure CSS):
- default - available / selectable
:checked(via.casca-slot:has(.casca-slot-input:checked)) - selected, brand-color highlight:hover- subtle background on selectable slots only:focus-visible(via:has()) - keyboard focus ring on the slot[data-state="taken"]/:disabled- dimmed, dashed border, strikethrough, not selectable[data-state="pending"]- on hold / awaiting confirmation: amber fill, solid border, no strikethrough, not selectable
Features:
- ✅ Native radio group inside
<fieldset>/<legend>- keyboard accessible out of the box (Tab in, arrow keys move, Space/Enter selects) - ✅ Single-select with standard form submission - no JS
- ✅ ≥44px touch targets (
--casca-slot-min-size) - ✅ Columns stack to a single column on narrow viewports (container query, viewport-query fallback)
- ✅ Dark-mode aware; works from the core build with consumer-supplied token mappings
- ✅ Reduced-motion, high-contrast, and forced-colors handling
Slot Grid CSS Variables:
| Variable | Default | Description |
|---|---|---|
--casca-slot-cols | 5 | Maximum column count; columns collapse below this as the grid narrows |
--casca-slot-col-min | 4.5rem | Minimum column width before columns collapse (responsive threshold) |
--casca-slot-gap | --casca-size-2 | Gap between columns and slots |
--casca-slot-radius | --casca-radius-2 | Slot corner radius |
--casca-slot-min-size | 2.75rem | Minimum slot size (44px touch target) |
--casca-slot-bg / -color / -border | grays | Available slot colors (light) |
--casca-slot-hover-bg / -border | grays | Hover colors (light) |
--casca-slot-selected-bg / -border | --casca-color-1 | Selected fill / border (accent); auto-lightens in dark |
--casca-slot-selected-color | --casca-on-accent | Selected label color; tracks the accent foreground (white in light, dark in dark-mode themes) |
--casca-slot-taken-bg / -color | grays | Taken / disabled colors (light) |
--casca-slot-pending-bg / -color / -border | yellow family | Pending / hold colors (light); defaults to the casca yellow ramp, map to your scheduling palette |
--casca-slot-focus-ring | --casca-color-1 | Focus outline color |
--casca-slot-*-dark | grays / yellow | Dark-mode overrides for bg/color/border/hover/taken/pending |
Server note: taken slots are rendered
disabled, so their value is never submitted, but always re-validate the chosen slot authoritatively on POST.
Invariants (CSS contract):
- Non-interactive states must carry
disabled. Visual state (data-state) and interactivity (disabled) are decoupled:data-statepicks the look;disabledis the single signal for "not selectable" (it drives thenot-allowedcursor and suppresses the hover affordance). Adata-state="taken"/"pending"slot without adisabledinput still looks taken/pending but remains clickable - setdisabled.selectedreflects an enabled checked input. Adisabled checkedslot (e.g. a server pre-selecting a slot that has since become taken/held) keeps its taken/pending look rather than the selected highlight.data-state=""is treated as no state (available, or taken if its input isdisabled). Omit the attribute rather than emitting it empty.- Adding a new non-interactive state needs only one
[data-state="x"]appearance rule plus adisabledinput; no other selectors change.
See site/pages/controls/slot-grid.html and site/assets/integrations/go-templates/slot-grid.tmpl.
Analytics BlockLink to section
The analytics block renders symmetrics site-summary.json as an in-theme,
server-rendered dashboard or homepage summary. It ships in casca-core.css,
uses no JavaScript, and keeps state in ordinary HTML attributes.
<section class="casca casca-analytics"
data-casca-analytics-variant="dashboard"
data-casca-analytics-state="preliminary"
data-casca-analytics-registers="public ops"
aria-labelledby="analytics-title"
aria-describedby="analytics-summary">
<header class="casca-analytics-hero">
<p class="casca-analytics-kicker">Traffic, counted without tracking</p>
<h1 id="analytics-title" class="casca-analytics-headline">
<span class="casca-analytics-value">2,467</span>
<span class="casca-analytics-unit">page views</span>
<span class="casca-analytics-date">Sat May 30</span>
</h1>
<p class="casca-analytics-lede">We count traffic without tracking you.</p>
</header>
<p id="analytics-summary" class="casca-data">
Seven-day trend includes a preliminary day. Sat May 30 is still being tallied and is not final.
</p>
<div class="casca-analytics-trend">
<table class="casca-analytics-trend-table">
<caption>Seven-day page-view trend</caption>
<tbody>
<tr data-casca-analytics-day-state="preliminary">
<th scope="row">Sat May 30</th>
<td class="casca-analytics-count">2,467</td>
<td>
<span class="casca-analytics-bar" aria-hidden="true" style="--casca-analytics-bar-size: 100%"></span>
<span class="casca-analytics-day-note">still tallying</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
The summary variant uses the same root and state attributes with
data-casca-analytics-variant="summary", a compact heading, a one-line status,
and the same .casca-data state summary.
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-analytics | root section | Analytics component root, used with .casca |
.casca-analytics-hero | header | Public lead section |
.casca-analytics-kicker | text | Small register label |
.casca-analytics-headline | heading | State heading |
.casca-analytics-value | span | Numeric value with tabular figures |
.casca-analytics-unit | span | Unit text, usually page views |
.casca-analytics-date | span | Public date label |
.casca-analytics-lede | paragraph | Public ethics line |
.casca-analytics-checklist | list | Non-extractive checklist |
.casca-analytics-state-note | paragraph | Visible state note |
.casca-analytics-trend | div | Trend table wrapper |
.casca-analytics-trend-table | table | Visible semantic trend table |
.casca-analytics-count | table cell | Numeric count cell |
.casca-analytics-bar | span | Presentational bar, aria-hidden="true" |
.casca-analytics-day-note | span | Visible day status |
.casca-analytics-registers | section | Public and ops register wrapper |
.casca-analytics-register | div | One register panel |
.casca-analytics-register-title | heading | Register heading |
.casca-analytics-status | paragraph | Compact summary status |
.casca-analytics-notice | section | Integrity-fail notice body |
.casca-analytics-notice-title | heading | Integrity-fail title |
.casca-analytics-notice-body | paragraph | Integrity-fail explanation |
.casca-analytics-last-good | paragraph | Last verified timestamp line |
.casca-analytics-verify | footer | Provenance footer |
.casca-analytics-verify-link | link | Verification link |
.casca-analytics-verify-recipe | block | Optional plain verification steps |
data-casca-analytics-variant="dashboard | summary" | root | Layout variant |
data-casca-analytics-state="ok | never-published | zero-traffic | single-day | stale | preliminary | integrity-fail" | root | Rendered state |
data-casca-analytics-cause="missing-feed | ttl-exceeded | publisher-timestamp-behind | hash-mismatch | signature-invalid | signature-absent | schema-mismatch | json-parse | field-invalid" | root | Optional state cause |
data-casca-analytics-register="public | ops" | register | Register vocabulary |
data-casca-analytics-day-state="sealed | preliminary" | trend row | Day finality |
--casca-analytics-bar-size: NN% | bar inline style | Server-normalized bar size |
CSS variables: --casca-analytics-bg, --casca-analytics-surface,
--casca-analytics-border, --casca-analytics-radius,
--casca-analytics-pad-block, --casca-analytics-pad-inline,
--casca-analytics-gap, --casca-analytics-max-inline,
--casca-analytics-display-font, --casca-analytics-body-font,
--casca-analytics-ui-font, --casca-analytics-mono-font,
--casca-analytics-value-size, --casca-analytics-summary-value-size,
--casca-analytics-label-size, --casca-analytics-color,
--casca-analytics-muted, --casca-analytics-rule,
--casca-analytics-accent, --casca-analytics-on-accent,
--casca-analytics-link-color, --casca-analytics-link-decoration,
--casca-analytics-state-border, --casca-analytics-state-accent,
--casca-analytics-trend-row-gap, --casca-analytics-bar-block-size,
--casca-analytics-bar-radius, --casca-analytics-bar-track,
--casca-analytics-bar-color, --casca-analytics-bar-size,
--casca-analytics-preliminary-fill, --casca-analytics-preliminary-border,
--casca-analytics-preliminary-hatch, --casca-analytics-integrity-bg,
--casca-analytics-integrity-border, --casca-analytics-integrity-accent,
--casca-analytics-verify-min-block-size, and
--casca-analytics-verify-pad-inline.
Preprocessor config:
[preprocessors.symmetrics]
kind = "symmetrics"
source = ".well-known/symmetrics/example.com/site-summary.json"
expected_schema_version = "symmetrics-report.v1"
cache_key = "symmetrics:example.com"
required = false
[preprocessors.symmetrics.target]
page = "/metrics/"
selector = "[data-casca-slot='analytics']"
action = "replace_content"
The implementation uses only common preprocessor fields. The dashboard variant
is the default. A summary variant is selected by convention when the
preprocessor name, target selector, or page URL contains summary.
Analytics Block live layer (000084 opt-in)Link to section
The optional dist/casca-live.js asset polls a JSON sidecar emitted by the
symmetrics preprocessor and applies field-level text swaps in place. The
static render is complete and correct without the script. The asset is built
by make build-live (not by the default make build) and is NOT bundled
into any CSS file.
Host opt-in. Two layers are required:
- Per-site (asset shipping). Set
[build] live_layer = trueincasca.toml. Whentrue, the symmetrics preprocessor emits a per-block JSON sidecar at_casca/preprocessors/symmetrics/<cache_key>.json. Whenfalse(the default), no sidecar is emitted and the analytics block remains purely static. - Per-block (script activation). The host page template adds
data-casca-live-source="/_casca/preprocessors/symmetrics/<cache_key>.json"on the<section class="casca casca-analytics">element. Optionally,data-casca-live-interval="N"overrides the 60-second default poll cadence (clamped to 600 seconds on backoff).
The host also loads the script with a single tag:
<script src="/dist/casca-live.js" defer></script>
Kill switch. Removing the <script> tag, or removing the
data-casca-live-source attribute, fully disables the live behavior on a
block. The static render continues to serve correctly.
Optional aria-live opt-in. The host MAY add aria-live="polite" on a
single <p class="casca-analytics-status"> node within the block. The
script's text swap on that node will fire a polite screen-reader
announcement. The script never adds aria-live itself and never sets
aria-live="assertive" or role="alert". Default is no announcement.
JSON sidecar shape (casca-live.v1):
{
"schema_version": "casca-live.v1",
"state": "ok|never-published|zero-traffic|single-day|stale|preliminary|integrity-fail",
"cause": "hash-mismatch|signature-invalid|...|null",
"variant": "dashboard|summary",
"value": "2,467",
"unit": "page views",
"date": "Sat May 30",
"state_note": "Today is still being tallied.",
"status": "Last updated 3 days ago.",
"last_good": "Sat May 27: 2,401 page views.",
"trend_rows": [
{ "day_state": "sealed", "count": "2,467", "bar_size": "84", "note": "Sat May 30", "active": true }
],
"data_summary": "Sat May 30: 2,467 page views. Stable trend.",
"generated_at": 1748563200
}
Integrity-fail field-clearing rule. When state == "integrity-fail",
the server emits empty strings for value, unit, date, last_good,
and every trend_rows[].count / note / bar_size. The script is a
dumb assigner: it writes whatever the sidecar carries. The server is the
single integrity authority. The script has no per-state override for
these fields; this keeps the integrity contract centralized server-side.
What the script does (and does not do). The script writes
textContent on named field spans, setAttribute on the root
data-casca-analytics-state and data-casca-analytics-cause, and
element.style.setProperty('--casca-analytics-bar-size', ...) on bar
elements. The script source contains NO .innerHTML = assignments, NO
Node.replaceWith() / Node.replaceChildren() calls, NO
Element.outerHTML = assignments, NO focus() calls, and NO
tabindex writes. The grep gate is enforced in CI. Element identity for
the <section>, the <table>, every row, and the verify link is
preserved across every state transition; CSS handles the per-state
visibility flip via the [data-casca-analytics-state-block~="X"] gate
added to src/charts/analytics.css.
v1 budget concessions (design §13.4). The shipped script omits trend-
row text/bar updates and the visibilitychange / online pause-resume
handlers so it fits the 2 KB minified / 1 KB gzipped budget. The static
render of the trend table remains complete; only the live-text swap on
trend cells is deferred to a later version. The headline value/unit/date,
the per-state note/status/last-good, the root state attribute, the cause
attribute, and the .casca-data text alternative all do swap live.
Browser floor. fetch, AbortController, Promise, JSON.parse,
querySelectorAll, textContent, setAttribute, and the hidden
attribute. Chrome 66+, Edge 16+, Firefox 57+, Safari 12.1+. Older
browsers see the static render only and never break.
Data Table (visible chart companion)Link to section
NEW in v0.10.0
A styled, accessible native <table> - the visible companion to the charts.
Pure CSS, no JS. Numeric columns align with tabular-nums; supports zebra
striping, an opt-in sticky header, a totals <tfoot>, and trend cells.
Interactivity composes with the Filter primitive (row filtering);
sort and pagination are server-driven.
<div class="casca-table-wrap"> <!-- optional: scroll + sticky header -->
<table class="casca-table">
<caption class="casca-table-caption">Revenue by product</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col" class="casca-table-num">Revenue</th>
<th scope="col" class="casca-table-num">YoY</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Atlas</th>
<td class="casca-table-num">$1.24M</td>
<td class="casca-table-num" data-trend="up">+8.2%</td>
</tr>
</tbody>
<tfoot>
<tr><th scope="row">Total</th><td class="casca-table-num">$4.69M</td><td></td></tr>
</tfoot>
</table>
</div>
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-table | <table> | The styled table (border-collapse, zebra, row hover) |
.casca-table-wrap | wrapper <div> | Optional: horizontal scroll on narrow viewports; a bounded block-size makes <thead> sticky |
.casca-table-caption | <caption> | Styled caption |
.casca-table-num | <th> / <td> | Numeric cell: right-aligned + tabular figures |
data-trend="up" | "down" | "flat" | <td> | Trend cell: ▲/▼/– glyph + color. Keep the +/- sign in the text - the sign carries direction without color |
aria-sort="ascending" | "descending" | <th> | Server-driven sort state; shows ↑/↓/↕ glyph on the header link |
.casca-table-select | <th> / <td> | The narrow, centered row-selection (checkbox) column cell |
.casca-row-select | <input type="checkbox"> | A per-row selection checkbox (branded via accent-color); a checked row highlights via :has() |
.casca-row-select-label | <label> | Optional wrapper around .casca-row-select that makes the whole cell the click target (a native <label> forwards clicks - no JS) |
Row selection (no-JS): put a .casca-row-select checkbox in a
.casca-table-select cell, inside a <form>. Checked rows get a brand-tint
highlight (distinct from the gray hover) and the selection submits as
name=value. Give each checkbox a name via aria-labelledby (→ the row header
id) or aria-label; put a .sr-only "Select" label in the column header.
Wrap the checkbox in a .casca-row-select-label so the entire cell (and the
row's height), not just the ~1em native box, is clickable. The label has no
text - the input keeps its accessible name from aria-labelledby / aria-label.
<td class="casca-table-select">
<label class="casca-row-select-label">
<input class="casca-row-select" type="checkbox" name="rows" value="atlas"
aria-labelledby="row-atlas">
</label>
</td>
<th scope="row" id="row-atlas">Atlas Pro</th>
CSS variables: --casca-table-border / -head-bg / -head-color /
-row-stripe / -hover-bg / -foot-bg / -cell-pad-block /
-cell-pad-inline / -trend-up / -trend-down / -row-selected /
-select-accent (+ -dark variants for the color tokens).
Features:
- ✅ Native
<table>semantics - keyboard + screen-reader accessible, no ARIA scaffolding - ✅ Trend is not color-only (▲/▼ glyph + the signed text); forced-colors keeps both, drops only the color
- ✅ Opt-in sticky header, zebra striping, totals footer, row hover
- ✅ Composes with Filter for no-JS row filtering (
data-filteron<tr>) - ✅ No-JS row selection: checked rows highlight (
:has()) and submit with the form; selected wins over zebra + hover; forced-colors uses a systemHighlight. Wrap the checkbox in.casca-row-select-labelto make the full cell the click target - ✅ Dark-mode aware; works from the core build
No client-side sort/pagination: CSS cannot reorder rows by value without JS. Render sorted/paginated output server-side and style the affordances with
aria-sort+ header links (the sort glyph is keyed offaria-sort, so it can never disagree with the announced state).
Row selection limits: a "select all" toggle and a live "N selected" count both need JS (one checkbox cannot drive others, and CSS cannot display a count). Drive select-all server-side (a submit that re-renders rows pre-checked) or as a JS enhancement. The selection highlight needs
:has(); the tint usescolor-mix(selection is still conveyed by the checked box wherecolor-mixis unsupported - see COMPATIBILITY).
See site/pages/components/data-table.html.
Range Slider (value input, no-JS)Link to section
NEW in v0.11.0
A themed, accessible single-value slider built on the native
<input type="range">. It's a form control: the value submits with the form
and the server re-renders (no JS). Branded with accent-color - the
standardized, cross-browser, no-JS way to color the filled track + thumb.
<div class="casca-range-field">
<label class="casca-range-label" for="threshold">Alert threshold</label>
<input class="casca-range" type="range" id="threshold" name="threshold"
min="0" max="100" value="40" step="5" list="threshold-ticks">
<datalist id="threshold-ticks"> <!-- optional native tick marks -->
<option value="0"></option><option value="50"></option><option value="100"></option>
</datalist>
<div class="casca-range-scale" aria-hidden="true">
<span>0</span><span>50</span><span>100</span>
</div>
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-range-field | wrapper <div> | Column layout: label, input, scale |
.casca-range-label | <label> | Field label (associate with for) |
.casca-range | <input type="range"> | The slider; accent-color brands the filled track + thumb |
.casca-range-scale | <div> | Static min/mid/max text row (aria-hidden; the input already announces its range) |
CSS variables: --casca-range-accent (+ -dark) - filled track + thumb
color (default --casca-color-1); --casca-range-block-size - the input
hit area (default 1.5rem).
Features:
- ✅ Native
<input type="range">- keyboard operable (arrows / Home / End), announces name + range + value, no ARIA needed - ✅ Branded fill + thumb via
accent-color(no pseudo-element track replacement, so the no-JS filled-track coloring survives cross-browser) - ✅ Visible focus ring,
:disabledstate, dark-mode + forced-colors aware - ✅ Optional native tick marks via
<datalist>; works from the core build
No live value readout / no dual-thumb: CSS cannot read an input's value and
<output>needs JS, so there's no drag-time readout - the value submits with the form and the server echoes it back (the scale labels orient the user meanwhile). A connected min–max (dual-thumb) range also needs JS. Both are out of scope. In Safari 15.0–15.3accent-coloris unsupported, so the slider renders with the system accent (functional, not branded) - see COMPATIBILITY.
See site/pages/controls/range-slider.html.
KPI / Stat CardLink to section
NEW in v0.12.0
The dashboard scorecard tile that leads a report - a metric label, a big value,
an optional delta (direction + sign, not color-only), and a caption. Pure
layout, no JS. .casca-stat-grid lays out a responsive row of cards.
<div class="casca-stat-grid">
<article class="casca-stat">
<span class="casca-stat-label">Monthly revenue</span>
<span class="casca-stat-value">$48.2k</span>
<span class="casca-stat-delta" data-trend="up">+12.4%</span>
<span class="casca-stat-caption">vs. last month</span>
</article>
<!-- ... more cards ... -->
</div>
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-stat-grid | wrapper <div> | Responsive grid (auto-fit, ~12rem min track) |
.casca-stat | <article> / <div> | The card: bordered tile, vertical stack |
.casca-stat-label | <span> | Metric name (small, muted) - keep it FIRST so AT reads context before the number |
.casca-stat-value | <span> | The big number (large, bold, tabular) |
.casca-stat-delta | <span> | Change indicator |
data-trend="up" | "down" | "flat" | .casca-stat-delta | ▲/▼/– glyph + color. Keep the +/- sign in the text - the sign carries direction without color |
.casca-stat-caption | <span> | Small context line |
.casca-stat-trend | <div> | Optional sized slot for an inline sparkline / mini chart (drop any casca chart in) |
CSS variables: --casca-stat-bg / -border / -radius / -pad /
-label-color / -value-color / -value-size (default --casca-font-size-5
= 2rem) / -caption-color / -up / -down (+ -dark variants for the colors).
Features:
- ✅ Plain text in reading order - a screen reader announces the card as a sentence; no ARIA needed
- ✅ Delta not color-only (▲/▼ glyph + signed text); forced-colors keeps both, drops only color
- ✅ Responsive grid; collapses to one column on phones
- ✅ Hosts an inline sparkline via
.casca-stat-trend(composition) - ✅ No
:has()/accent-color/color-mix()- renders everywhere; works from the core build
See site/pages/components/stat-card.html.
Keyboard Key (kbd)Link to section
NEW in v1.3.0
Style the native <kbd> element inside .casca so a key reads as a key, no
extra classes at the call site. Inline-flex pill with a slightly raised bottom
border (the universal "physical key" signal), monospace glyph, surface
background.
<p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> to focus search.</p>
<td><kbd>Enter</kbd></td>
Classes: none required. Selector is .casca kbd, so wrapping any subtree
in .casca opts in.
Features:
- ✅ Plain semantic HTML, no extra classes
- ✅ Mono font via
--casca-font-mono, surface + gray tokens, dark and forced-colors aware - ✅ Ships in the core build
See site/pages/components/kbd.html.
Date Field (no-JS)Link to section
NEW in v1.3.0
A themed <input type="date">. CSS themes the field (border, focus ring,
padding, font, branded accent); the popup calendar is the browser's own
non-themeable UI, varying by browser/OS. Same trade-off as Range Slider: stay
native, theme what you can, accept the popup. The value submits with the form,
no JavaScript.
<div class="casca-date-field">
<label class="casca-date-label" for="start">Start date</label>
<input class="casca-date" type="date" id="start" name="start" value="2026-05-29">
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-date-field | wrapper <div> | Column layout: label + input |
.casca-date-label | <label> | Field label (associate with for) |
.casca-date | <input type="date"> | The themed field; calendar popup is the browser's native UI |
Features:
- ✅ Native form control, keyboard-operable, screen-reader-announced
- ✅ Branded via
accent-color; visible focus ring;:disabledstate - ✅ Dark and forced-colors aware; WebKit picker indicator inverts on dark
- ✅ Ships in
casca-extended-controls
The popup calendar UI is browser-native and not styleable. This is the trade-off for staying no-JS; a custom navigable calendar grid would need JavaScript or server round-trips. Firefox exposes fewer pseudo-elements than WebKit and renders its own picker chrome.
See site/pages/controls/date.html.
Color Field (no-JS)Link to section
NEW in v1.3.0
A themed <input type="color">. The wrapper takes Casca's border and radius;
the inner swatch fills with the chosen color. The popup picker is the OS-
native picker. Same pattern as Date Field. The chosen color submits with the
form, no JavaScript.
<div class="casca-color-field">
<label class="casca-color-label" for="brand">Brand color</label>
<input class="casca-color" type="color" id="brand" name="brand" value="#1864ab">
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-color-field | wrapper <div> | Column layout: label + input |
.casca-color-label | <label> | Field label (associate with for) |
.casca-color | <input type="color"> | The themed field; popup picker is the browser's native UI |
Features:
- ✅ Native form control, keyboard-operable, screen-reader-announced
- ✅ Visible focus ring,
:disabledstate, dark and forced-colors aware - ✅ Ships in
casca-extended-controls
Same as Date Field: the popup picker is OS-native and not themeable. A custom HSL-slider color picker would need JavaScript or server round-trips.
See site/pages/controls/color.html.
Interactions (no-JS)Link to section
NEW in v0.8.0
@layer casca.interactions - native HTML + :has() patterns that make charts
and lists interactive with zero JavaScript. They require :has() (Chrome
105+, Safari 15.4+, Firefox 121+); without it, content degrades to "everything
visible" and the native controls still work. See the live catalog at
site/pages/index.html, plus the per-interaction pages
(site/pages/controls/series-toggle.html, site/pages/controls/switch.html,
site/pages/controls/disclosure.html, site/pages/controls/filter.html).
Scoping convention: patterns are markup-coupled (the consumer emits exact
markup), keyed on value + data-* attributes, with a scope ancestor for
:has(). Give each radio group (switch) a UNIQUE name so instances on one
page don't cross-wire.
Series ToggleLink to section
Check/uncheck a chip to show/hide a chart series. Hidden series use
visibility: hidden (NOT display), so the chart keeps its layout, axes, and
SVG :nth-child coloring/stagger - toggling one never moves or recolors the
others. The toggle controls live inside the same .casca as the series, and
OUTSIDE the chart element. Series carry numeric data-series="1".."8".
<div class="casca casca-figure">
<fieldset class="casca-toggles">
<label class="casca-toggle">
<span class="casca-toggle-box">
<input type="checkbox" class="casca-toggle-input" value="1" checked>
</span>
<span class="casca-toggle-label">
<span class="casca-toggle-swatch" style="--_color: var(--casca-color-1)"></span>
Revenue
</span>
</label>
<!-- ... -->
</fieldset>
<svg ...><polyline data-series="1" .../></svg>
</div>
| Class | Description |
|---|---|
.casca-toggles | <fieldset> chip row |
.casca-toggle | a <label>: a checkbox cell + a decorator cell, joined as a two-segment control (the box does NOT wrap the checkbox) |
.casca-toggle-box | bordered, padded cell that frames the native checkbox |
.casca-toggle-input | the native checkbox (value="N"); keeps its default appearance |
.casca-toggle-label | the decorator cell (swatch + text); shares a seam with the box |
.casca-toggle-swatch | color dot inside the decorator (--_color) |
Switch (segmented view swap)Link to section
A radio group swaps which .casca-switch-view shows (Day/Week/Month, chart
types). Active segment via input:checked + label (no :has() needed); the
view swap uses :has() with a first-view fallback.
Two control skins via data-variant, sharing the same radio + view-swap: the
default segmented pill and "tabs" (an underlined tab strip). For a binary
A/B toggle (with a sliding knob) that composes safely inside other switches, use
the dedicated Segment primitive instead.
<div class="casca-switch">
<div class="casca-switch-controls">
<input class="casca-switch-input" type="radio" name="range" id="r1" value="1" checked>
<label class="casca-switch-btn" for="r1">Day</label>
<!-- ... unique `name` per instance ... -->
</div>
<div class="casca-switch-views">
<div class="casca-switch-view" data-view="1">...</div>
</div>
</div>
Disclosure (drill-down)Link to section
Styled native <details>/<summary> - open/close is built into HTML, so this
needs no :has() and has no fallback (universal support).
<details class="casca-disclosure">
<summary class="casca-disclosure-summary">Regional breakdown</summary>
<div class="casca-disclosure-body"> ... </div>
</details>
Filter (chips filter a list)Link to section
Check categories to show, uncheck to hide matching items. Items collapse
(display: none, so the list reflows). Uses a distinct .casca-filter-input
so it never cross-fires with a series toggle on the same page; items carry
data-filter="1".."8".
<div class="casca-filter">
<fieldset class="casca-toggles">
<label class="casca-toggle">
<span class="casca-toggle-box">
<input type="checkbox" class="casca-filter-input" value="1" checked>
</span>
<span class="casca-toggle-label">
<span class="casca-toggle-swatch" style="--_color: var(--casca-color-1)"></span>
Marketing
</span>
</label>
</fieldset>
<ul><li data-filter="1">...</li></ul>
</div>
Result count + empty-state (no-JS). A display: none item generates no box,
so it does not increment a CSS counter - .casca-filter-count shows the live
count of visible items via counter(). Place it after the items in source
order (counters read document order). .casca-filter-empty shows a message when
no category is checked (requires :has(); without it it stays hidden, so the list
never looks falsely empty). The empty-state is scoped to the filter's own
toggle group, so .casca-toggles and .casca-filter-empty must be direct
children of .casca-filter (as below).
<div class="casca-filter">
<fieldset class="casca-toggles"> ... </fieldset>
<ul><li data-filter="1">...</li></ul>
<p>Showing <span class="casca-filter-count"></span> of 6.</p>
<p class="casca-filter-empty">No items match.</p>
</div>
Nesting. A .casca-filter can contain another .casca-filter (e.g. a page
that filters cards, one of which is itself a live filter). The hide rules, the
count, and the empty-state each compose per filter when every nested filter
uses a value / data-filter namespace disjoint from its ancestors (the
1..8 range; allocate sub-ranges - e.g. outer 1,2,3, inner 4,5). This is
required because :has() cannot be confined to a sub-scope, so an outer filter's
hide rule would otherwise also collapse a nested filter's items that share the
same number. The count stays correct automatically (each .casca-filter resets
its own counter()), and the empty-state stays correct via the direct-child
scoping above.
Segment (binary slide toggle)Link to section
A two-state slide toggle for progressive disclosure: a labelled knob that
reveals or hides the optional .casca-segment-extra items in its scope. Anything
NOT marked extra stays visible in both states, so the base set is always
available and the toggle just filters the extras in or out (checked = show all).
Built on the hidden-checkbox + label[for] + subsequent-sibling pattern (the
checkbox comes first; the control and content are its later siblings). No :has(),
so it composes anywhere and re-invalidates reliably on toggle.
<div class="casca-segment">
<input type="checkbox" class="casca-segment-input" id="seg1" checked>
<label class="casca-segment-control" for="seg1">
<span class="casca-segment-label">Core</span>
<span class="casca-segment-track" aria-hidden="true"></span>
<span class="casca-segment-label">Extended</span>
</label>
<div class="casca-card-grid">
<div class="casca-card">always shown</div>
<div class="casca-card casca-segment-extra">hidden when toggled off</div>
</div>
</div>
All interactions are keyboard-accessible (native checkbox/radio/
<details>), reduced-motion safe, and forced-colors aware. The accessible.casca-datatable always lists every series regardless of what's toggled off.
Modal (overlay dialog, no-JS)Link to section
NEW in v1.3.0
A no-JS modal built on the hidden-checkbox + label[for] pattern (same family
as casca-segment, casca-toggle, casca-disclosure). A trigger label opens
it, the close button or a backdrop click close it. Optional slide-in / slide-
out animation configurable via data-slide on the box.
<div class="casca-modal">
<input type="checkbox" id="m1" class="casca-modal-input">
<label class="casca-modal-trigger" for="m1">Open</label>
<label class="casca-modal-backdrop" for="m1" aria-hidden="true"></label>
<div class="casca-modal-box" data-slide role="dialog" aria-labelledby="m1-t" aria-modal="true">
<h2 id="m1-t" class="casca-modal-title">Title</h2>
<div class="casca-modal-body">Body</div>
<label class="casca-modal-close" for="m1" aria-label="Close">x</label>
</div>
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-modal | wrapper <div> | Grouping container (no positioning of its own) |
.casca-modal-input | <input type="checkbox"> | The hidden state checkbox; drives open/closed |
.casca-modal-trigger | <label for> | The "open" button (styled label) |
.casca-modal-backdrop | <label for> | Fixed dimmed overlay; clicking it closes |
.casca-modal-box | <div role="dialog"> | The centered card with title + body + close |
.casca-modal-title | <h2> (or similar) | Title slot |
.casca-modal-body | <div> (or similar) | Body slot; can host any content including forms |
.casca-modal-close | <label for> | The "x" close button |
Attributes:
| Attribute | On | Values | Description |
|---|---|---|---|
data-slide | .casca-modal-box | (bare) / top / bottom / left / right | Slide in from / out to the given direction. Bare attribute defaults to top. Without data-slide, the modal just fades. |
Features:
- ✅ Pure form controls, no JavaScript; degrades to a plain checkbox in ancient engines
- ✅ Backdrop click dismisses (it is a
<label for>covering the viewport) - ✅ Reduced-motion drops the open/close animation; forced-colors keeps the box bounded
- ✅ Ships in
casca-extended-controls
Placement contract: The .casca-modal wrapper must sit outside ancestors
that create stacking contexts. Do not nest it inside isolated cards,
transformed panels, opacity effects, z-indexed positioned wrappers, or other
stacking ancestors. casca check-modal <FILE> enforces the statically
detectable cases for authored HTML and narrow modal-related CSS source.
Honest trade-offs. Focus is NOT trapped inside the modal (no JS); the box is rendered late in the document order, so tabbing from the trigger reaches it naturally and tabbing past the close exits to whatever follows the modal wrapper. Escape does NOT close it (also needs JS).
aria-modal="true"is asserted in the markup but cannot truly inert the rest of the page without JavaScript, screen readers may still reach prior content.
See site/pages/controls/modal.html.
Layout Shell (extended-layout)Link to section
The no-JS page/dashboard shell primitives - what you build a dashboard or
gallery FROM, as opposed to the widgets that fill it. Ships in the opt-in
casca-extended-layout.css bundle (or the all-in-one casca.css); load it
alongside casca-core.css.
CardLink to section
A bordered surface with optional .casca-card-title / .casca-card-body /
.casca-card-footer slots. The card owns the frame; you compose a stat, chart,
table, or controls inside. Use a real heading for the title so the document
outline stays correct. All slots are optional - a bare .casca-card is just a
padded surface.
<article class="casca-card">
<h3 class="casca-card-title">Monthly revenue</h3>
<div class="casca-card-body">
<!-- a .casca-stat, a chart, a .casca-table, ... -->
</div>
<footer class="casca-card-footer">Updated 5 minutes ago</footer>
</article>
Card gridLink to section
A responsive grid for cards (or any tiles): columns auto-fit at
--casca-card-grid-min and collapse to one column on narrow - intrinsically
responsive, no media/container-query setup required.
<div class="casca-card-grid">
<article class="casca-card"> ... </article>
<!-- ... -->
</div>
Grid items receive a defensive min-inline-size: 0 so a child with inline
width: 100% or oversized intrinsic content shrinks to its grid cell instead
of forcing the cell wider.
ThumbLink to section
A clipped, centered tile content area. Use inside a .casca-card to host a
single visual (a chart, an icon, a small composition) without letting a
child's inline width: 100% or oversized intrinsic dimensions burst out of
the tile.
<a class="casca-card" href="...">
<div class="casca-thumb" aria-hidden="true">
<!-- a casca chart, an icon, a small composition -->
</div>
<span class="casca-card-title">Bar</span>
</a>
Variant .casca-thumb-disclosure opens at the top of the tile and caps its
block-size so a <details> expansion does not stretch the surrounding row.
Live-control z-stack: interactive primitives inside a .casca-thumb (a
.casca-toggle, .casca-segment-control, .casca-range, .casca-modal-trigger,
etc.) automatically rise above a stretched-link pseudo-element so they remain
operable when the surrounding tile is a click target.
casca-thumb CSS VariablesLink to section
| Variable | Default | Purpose |
|---|---|---|
--casca-thumb-block-size | 4rem | Fixed tile content height (min and max) |
ToolbarLink to section
A horizontal control bar for filter chips, a segmented switch, or actions. Wraps
by default; data-align distributes groups (between / end),
data-variant="scroll" keeps one row and scrolls on narrow. .casca-toolbar-group
keeps related controls together as one unit.
<div class="casca-toolbar" data-align="between">
<fieldset class="casca-toggles"> ... </fieldset>
<div class="casca-toolbar-group"> ...actions... </div>
</div>
TabsLink to section
Tabs are the existing switch with data-variant="tabs" -
the same radio-driven, no-JS view swap, restyled from a filled pill into an
underlined tab strip. There is no separate .casca-tabs primitive (a tab bar
is a segmented switch; shipping a second one would be redundant).
<div class="casca-switch" data-variant="tabs">
<div class="casca-switch-controls"> ...radios + labels... </div>
<div class="casca-switch-views"> ...panels... </div>
</div>
Anchor navLink to section
An in-page #anchor link list (a TOC / section nav), vertical by default or
horizontal with data-orientation="horizontal" (a top-bar TOC; the active marker
becomes an underline instead of a leading rail). Optionally sticky (data-sticky).
The active item is marked by the author/server with aria-current
(true / page / location) - pure CSS cannot derive the
scrolled-to section as the link's own state, so active state rides on
aria-current (which a screen reader also announces) and degrades to a plain
link list.
<nav class="casca-anchor-nav" aria-label="On this page" data-sticky>
<a class="casca-anchor-nav-link" href="#charts" aria-current="true">Charts</a>
<a class="casca-anchor-nav-link" href="#tables">Tables</a>
</nav>
Layout primitives are
.casca-*-scoped, dark-mode + forced-colors aware, and add no:rootglobals. They carry no base/theme of their own - load overcasca-core.css.
CSS Custom PropertiesLink to section
Global Design TokensLink to section
ColorsLink to section
/* Grayscale */
--casca-gray-0: #ffffff;
--casca-gray-1: #f8f9fa;
--casca-gray-2: #e9ecef;
--casca-gray-3: #dee2e6;
--casca-gray-4: #ced4da;
--casca-gray-5: #adb5bd;
--casca-gray-6: #6c757d;
--casca-gray-7: #495057;
--casca-gray-8: #343a40;
--casca-gray-9: #212529;
/* Chart colors */
--casca-blue-5: #4dabf7;
--casca-blue-6: #228be6;
--casca-teal-5: #20c997;
--casca-teal-6: #12b886;
--casca-orange-5: #ff922b;
--casca-orange-6: #fd7e14;
--casca-pink-5: #f06595;
--casca-pink-6: #e64980;
--casca-green-5: #51cf66;
--casca-green-6: #40c057;
--casca-purple-5: #9775fa;
--casca-purple-6: #845ef7;
--casca-yellow-5: #ffd43b;
--casca-yellow-6: #fab005;
--casca-red-5: #ff6b6b;
--casca-red-6: #fa5252;
SpacingLink to section
--casca-size-1: 0.25rem; /* 4px */
--casca-size-2: 0.5rem; /* 8px */
--casca-size-3: 0.75rem; /* 12px */
--casca-size-4: 1rem; /* 16px */
--casca-size-5: 1.5rem; /* 24px */
--casca-size-6: 2rem; /* 32px */
TypographyLink to section
--casca-font-size-00: 0.75rem; /* 12px */
--casca-font-size-0: 0.875rem; /* 14px */
--casca-font-size-1: 1rem; /* 16px */
--casca-font-size-3: 1.25rem; /* 20px */
--casca-font-size-4: 1.5rem; /* 24px */
--casca-font-size-5: 2rem; /* 32px */
--casca-font-weight-4: 400; /* normal */
--casca-font-weight-6: 600; /* semibold */
--casca-font-weight-7: 700; /* bold */
--casca-font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
Z-Ladder TokensLink to section
Use these tokens for every z-index declaration. auto is also accepted when
the element should not participate in a named layer.
--casca-z-base
--casca-z-local-1
--casca-z-local-2
--casca-z-local-3
--casca-z-local-4
--casca-z-sticky
--casca-z-overlay
--casca-z-popover
--casca-z-modal
casca check-z-ladder <FILE> rejects bare numeric z-index values, unknown
ladder tokens, CSS-wide keywords, var() fallbacks, and calc() expressions.
Chart-Specific TokensLink to section
/* General */
--casca-height: 300px;
--casca-width: 100%;
--casca-gap: 0.5rem;
--casca-padding: 1rem;
/* Bars */
--casca-bar-width: 2rem;
--casca-bar-radius: 0.25rem;
--casca-bar-min-height: 2px;
/* Pies */
--casca-pie-size: 250px;
--casca-donut-hole-size: 40%;
/* Lines */
--casca-line-stroke-width: 2px;
--casca-line-point-size: 6px;
--casca-line-area-opacity: 0.1;
/* Progress & Gauges */
--casca-progress-height: 0.75rem;
--casca-progress-radius: 0.25rem;
--casca-gauge-size: 200px;
--casca-gauge-thickness: 20px;
/* Animation */
--casca-duration: 0.4s;
--casca-easing: cubic-bezier(0.2, 0, 0, 1);
/* Colors */
--casca-label-color: #6c757d;
--casca-axis-color: #ced4da;
--casca-grid-color: #e9ecef;
--casca-axis-width: 1px;
/* Axis-label primitive (.casca-axis, .casca-plot) */
--casca-axis-y-width: 2.25rem; /* left tick gutter in .casca-plot */
--casca-axis-x-height: 1.25rem; /* bottom tick gutter in .casca-plot */
--casca-axis-count: 6; /* radial spoke count (set per radar) */
--casca-axis-radius: 56%; /* radial label distance from center */
/* Chart palette */
--casca-color-1: var(--casca-blue-6);
--casca-color-2: var(--casca-teal-6);
--casca-color-3: var(--casca-orange-6);
--casca-color-4: var(--casca-pink-6);
--casca-color-5: var(--casca-green-6);
--casca-color-6: var(--casca-purple-6);
--casca-color-7: var(--casca-yellow-6);
--casca-color-8: var(--casca-red-6);
/* Legible foreground for text/icons sitting ON the color-1 accent fill
(e.g. the active switch segment, a selected slot). Defaults to white; the
themed bundles flip it to a dark value in dark mode, where color-1 remaps to
a lighter tint. If you override color-1, set on-accent to a color that meets
WCAG AA against your accent in both light and dark. */
--casca-on-accent: var(--casca-gray-0);
/* Layout shell (extended-layout) */
--casca-card-bg: var(--casca-surface-1);
--casca-card-border: var(--casca-gray-3); /* -dark: --casca-gray-7 */
--casca-card-radius: var(--casca-radius-2);
--casca-card-pad: var(--casca-size-5);
--casca-card-gap: var(--casca-size-3);
--casca-card-title-color: var(--casca-gray-9); /* -dark: --casca-gray-0 */
--casca-card-title-size: var(--casca-font-size-1);
--casca-card-footer-color: var(--casca-gray-6); /* -dark: --casca-gray-5 */
--casca-card-grid-min: 16rem; /* card-grid min column width */
--casca-card-grid-gap: var(--casca-size-4);
--casca-toolbar-gap: var(--casca-size-3);
--casca-anchor-nav-gap: var(--casca-size-1);
--casca-anchor-nav-color: var(--casca-gray-7); /* -dark: --casca-gray-4 */
--casca-anchor-nav-active: var(--casca-color-1);
--casca-anchor-nav-pad: var(--casca-size-2);
Customization ExampleLink to section
/* Override default colors */
:root {
--casca-color-1: #0066cc;
--casca-color-2: #00aa66;
--casca-bar-radius: 0.5rem;
--casca-duration: 0.6s;
}
/* Chart-specific customization */
.casca.custom-chart {
--casca-height: 400px;
--casca-bar-width: 3rem;
}
Accessibility ClassesLink to section
/* Screen reader only content */
.casca-data { /* Visually hidden data table */ }
.sr-only { /* Screen reader only */ }
/* Live regions for updates */
.casca-live-region { /* Announces changes */ }
Responsive BreakpointsLink to section
Casca uses container queries for responsive behavior:
/* Small charts (< 400px) */
@container casca (max-width: 400px) {
.casca-bar-value {
--_bar-width: 1rem; /* Narrower bars */
}
.casca-bar-label {
font-size: 0.75rem;
}
}
/* Tiny charts (< 300px) */
@container casca (max-width: 300px) {
.casca-pie {
--_pie-size: 150px;
}
}
Browser SupportLink to section
Minimum Requirements:
- Chrome 105+ (Container Queries, :has())
- Firefox 110+ (Container Queries)
- Safari 16+ (Container Queries, :has())
- Edge 105+
Progressive Enhancement:
- Falls back gracefully in older browsers
- Core data remains accessible via tables
- No JavaScript required for any functionality
CLI Theme ToolsLink to section
casca buildLink to section
casca build composes a custom Casca CSS bundle from embedded source CSS.
With no flags, it emits the same all-in-one bundle as dist/casca.css.
casca build \
[--include core[,extended-charts][,extended-controls][,extended-layout]] \
[--themes solar[,cyber][,lunar][,arcade]] \
[--theme-manifest <path>] \
[--picker on|off] \
[--picker-root <selector>] \
[--output <path>] \
[--minify on|off] \
[--targets <browserslist-string>] \
[--source-dir <path>]
--include defaults to all structural pieces: core,
extended-charts, extended-controls, and extended-layout.
Extensions are always ordered after core; requesting an extension
without core adds core and emits a warning.
--themes defaults to all four moods: solar, cyber, lunar, and
arcade. With picker mode on, solar is the unscoped base by default,
while cyber, lunar, and arcade are scoped through
body[data-casca-theme="<mood>"] and
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="<mood>"]:checked).
A single theme with --picker on is allowed and warns because there is no
overlay to switch to.
--theme-manifest <path> reads the same TOML manifest accepted by
casca scope-theme --theme-manifest. In manifest mode, --themes accepts
manifest theme ids instead of the four built-in mood ids. If --themes is
omitted, all manifest themes are emitted in manifest order with the manifest
base first. Picker scoping uses the manifest picker_root.
--picker off appends each requested theme in order without picker
scoping. Multiple themes are allowed in that mode and warn because later
themes override earlier themes through normal cascade order.
--output - writes CSS to stdout. Any warnings are still written to
stderr. --source-dir <path> reads from a Casca-shaped source tree
instead of embedded sources; pass either the src/ directory or a
repository root containing src/.
casca scope-themeLink to section
casca scope-theme aggregates compiled Casca CSS fragments with source
theme overlays and writes the result to a required output file. CSS is
not written to stdout.
casca scope-theme \
--core <path> \
--extension <path> [--extension <path> ...] \
--theme-manifest <path> \
--output <path>
--core and --extension accept compiled CSS fragments. Manifest mode reads
[theme_state] and [[themes]] from TOML, resolves theme source paths
relative to the manifest, emits the base theme first, and scopes each non-base
theme through body[data-casca-theme="<mood>"] plus the :has() preview root.
The manifest schema is:
[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" # or "tokens"
source = "../src/themes/default.css"
base = true
swatch_light = ["#f2ebda", "#c8633a"]
swatch_dark = ["#251e16", "#e68a60"]
Theme ids must match [a-z0-9][a-z0-9-]{0,47}. kind = "css" runs through
the scoped-selector validator. kind = "tokens" compiles a TOML token file
with [light], [dark], [roles.light], and [roles.dark] tables before
validation.
The compatibility mode remains supported:
casca scope-theme \
--core <path> \
--extension <path> [--extension <path> ...] \
--base <theme-source> \
--overlay <theme-source>:<mood-name> [--overlay ...] \
[--picker-root <selector>] \
--output <path>
In compatibility mode --base and --overlay accept source theme CSS rooted
at .casca. --picker-root defaults to
.casca-theme-picker[data-casca-theme-picker].
The Makefile uses casca scope-theme to write canonical
dist/casca.css, then copies dist/casca-portal.css as the deprecated
one-release alias.
casca dev and casca preview theme servingLink to section
When a project config contains a theme-picker widget, casca dev and
casca preview load the referenced manifest once at startup and serve
GET /_casca/theme from that in-memory context. casca dev also routes
changes to the manifest file through its existing config rebuild path, so a
successful rebuild swaps the output state and theme serving context together.
For portal HTML responses, both commands resolve the active mood from query,
cookie, then manifest default. They set body[data-casca-theme="<id>"], update
the checked picker radio, refresh the hidden return_to, and emit
Vary: Cookie. Assets, redirects, feeds, /_casca/ws, /_casca/dev.js, the
setter path, and other reserved /_casca/ paths bypass the transform.
If no manifest is configured, the endpoint is not active and the commands keep their static serving behavior.
casca lint --theme-overlayLink to section
casca lint --theme-overlay validates a theme source file before it is
compiled. It checks the Casca theme contract: allowed at-rules, no nested
style rules, custom properties in the --casca-* or --_ namespace, and
the selected base or overlay role.
casca lint --theme-overlay <FILE> --as base|overlay [--base <FILE>]
--as base validates the file as a base theme.
--as overlay validates the file as an overlay. --base <FILE> may be
supplied for base-relative checks, including public variable names and
matching light or dark contexts.
Exit code 0 means validation is clean. Exit code 1 means validation or
parsing failed. Exit code 2 means the command line did not match the clap
schema.
In theme-overlay mode, positional FILE arguments are rejected. Pass the
theme source path through --theme-overlay. This preserves the legacy
casca lint <file> dispatch for compiled CSS bundle linting.
CLI Lint ChecksLink to section
casca check-z-ladderLink to section
casca check-z-ladder <FILE> scans one CSS file and reports every z-index
declaration that does not use auto or a declared Casca z-ladder token.
casca check-z-ladder <FILE>
Accepted values are auto and var(--casca-z-base),
var(--casca-z-local-1), var(--casca-z-local-2),
var(--casca-z-local-3), var(--casca-z-local-4),
var(--casca-z-sticky), var(--casca-z-overlay),
var(--casca-z-popover), and var(--casca-z-modal).
Diagnostics use <file>:<line>:<col> and point at the z-index
declaration. Exit code 0 means clean, 1 means violations or a read error,
and 2 is reserved for command-line usage errors.
casca check-modalLink to section
casca check-modal <FILE> checks modal DOM placement and narrow modal-related
CSS regressions.
casca check-modal <FILE>
.html and .htm inputs are tokenized and every .casca-modal ancestor is
checked for known stacking-context triggers. .css inputs check direct
.casca-modal wrapper stacking rules, mapped blocker-class drift, and bounded
selector-ancestor risks using descendant and child combinators. Other
extensions return exit code 2.
Diagnostics use <file>:<line>:<col> and identify the offending ancestor or
selector. Exit code 0 means clean, 1 means violations or a read error, and
2 means the input extension or command line is unsupported.
Version HistoryLink to section
The release history lives in CHANGELOG.md - the single source of truth. (This section used to duplicate it and drifted out of date, so it now just points there.)