Skip to main content
Casca
Theme

Casca API ReferenceLink to section

Complete reference for all Casca components, CSS custom properties, and HTML patterns.

Table of ContentsLink to section


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:

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

AttributeValuesDescription
data-grid-Show background grid lines
data-diverging-Enable diverging baseline mode
data-orientationhorizontalHorizontal bar layout
data-directionpositive, negativeBar direction (diverging only)
data-labelStringValue label text (on the bar)
data-labelsalwaysShow data-label values without hover (touch / print / static no-JS); default is hover-only

Bar Chart CSS VariablesLink to section

VariableDefaultDescription
--value0%Bar height/length (0-100%)
--colorvar(--casca-color-1)Bar fill color
--casca-bar-width2remBar width
--casca-bar-radius0.25remBar border radius
--casca-bar-min-height2pxMinimum bar height
--casca-bar-rank-label-width7remLeading 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

AttributeValuesDescription
data-segments0-8Number of pie segments. 1 renders a solid single-category disc (--color-1); 0 (or omitted) renders a neutral "no data" disc
data-variantdonutDonut 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-data table. Very small slices are handled natively by conic-gradient (a 1% slice = 3.6deg); set the matching --angle-* values.

Pie Chart CSS VariablesLink to section

VariableDefaultDescription
--angle-N0degCumulative angle for segment N (1-8)
--color-Nvar(--casca-color-N)Color for segment N
--casca-pie-size250pxPie diameter
--casca-donut-hole-size40%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:

ClassDescription
.casca-heatmapThe grid container (--casca-heatmap-cols equal columns)
.casca-heatmap-colheadOptional column header (sits in the grid's first row)
.casca-heatmap-cellA cell; --value (0–1) drives fill intensity
.casca-heatmap-scaleOptional legend row (Busy → Free)
.casca-heatmap-scale-barThe gradient bar inside the legend

Heatmap CSS Variables:

VariableDefaultDescription
--casca-heatmap-cols7Number of columns (set inline per grid)
--value0Per-cell intensity, 01 (on .casca-heatmap-cell)
--casca-heatmap-colorvar(--casca-color-1)Full-intensity (value 1) color
--casca-heatmap-emptyvar(--casca-gray-2)Empty (value 0) color
--casca-heatmap-empty-darkvar(--casca-gray-8)Empty color in dark mode
--casca-heatmap-gapvar(--casca-size-1)Gap between cells
--casca-heatmap-radiusvar(--casca-radius-1)Cell corner radius

Features:

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 0100, 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 / attrDescription
.casca-scatterThe plot box (position: relative, aspect-ratio, left+bottom axis frame)
data-gridOpt-in graph-paper grid background
.casca-scatter-pointA point; positioned by --x / --y (0–100)
.casca-scatter-trendOpt-in trend line; full-width band from --y1 (left edge) to --y2 (right edge)
.casca-scatter-quadrantsOpt-in dividers; vertical rule at --qx, horizontal at --qy
.casca-scatter-quadrant-labelA quadrant corner label; positioned by --x / --y

Scatter CSS Variables:

VariableDefaultDescription
--x / --y0Point / label position, 0100 (percent of plot)
--size--casca-scatter-point-sizePoint diameter (bubble variant)
--colorvar(--casca-color-1)Point color (category)
--y1 / --y250Trend line y at the left / right edge, 0100 (server-fit)
--qx / --qy50Quadrant divider positions, 0100
--casca-scatter-aspect4 / 3Plot aspect ratio
--casca-scatter-point-size0.75remDefault point diameter
--casca-scatter-grid-colorvar(--casca-grid-color)Grid line color
--casca-scatter-grid-step25%Grid spacing
--casca-scatter-trend-colorvar(--casca-label-color)Trend line color
--casca-scatter-trend-width1.5Trend band thickness (% of plot height)
--casca-scatter-quadrant-colorvar(--casca-axis-color)Quadrant divider color

Features:

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-data table 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 / attrDescription
.casca-waterfallThe chart container (flex row of bars, baseline at 0)
data-gridOpt-in horizontal gridlines
.casca-waterfall-barOne step; --start / --end (0–100) set the floating segment
data-directionincrease | decrease | total - picks the segment color
.casca-waterfall-capOptional value label floated above the segment
.casca-waterfall-labels / -labelX-axis category label row

Waterfall CSS Variables:

VariableDefaultDescription
--start / --end0Cumulative level before / after the step (0–100; on the bar)
--casca-waterfall-height240pxPlot height
--casca-waterfall-increasevar(--casca-color-5)Increase color
--casca-waterfall-decreasevar(--casca-color-8)Decrease color
--casca-waterfall-totalvar(--casca-color-1)Total-bar color
--casca-waterfall-gapvar(--casca-size-3)Gap between bars
--casca-waterfall-connector-colorvar(--casca-axis-color)Connector line color

Features:

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 / varDescription
.casca-radarThe square plot with circular grid (set --casca-radar-axes = spoke count)
.casca-radar-seriesOne series; --points (server-computed vertices) + --color
--casca-radar-axes6
--casca-radar-grid-colorvar(--casca-grid-color)
--casca-radar-fill-opacity0.35

Features:

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-data table 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 / varDescription
.casca-candlestickThe chart container (flex row of candles)
.casca-candleOne candle; --open / --high / --low / --close (0–100)
data-directionup (default) | down - picks the candle color
--casca-candle-upvar(--casca-color-5)
--casca-candle-downvar(--casca-color-8)
--casca-candlestick-height240px
--casca-candle-body-inset22%

Features:

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

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 / attrDescription
.casca-plotOptional 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-tickOne label; --pos (linear) or --index (radial)
.casca-axis-title + data-axis="x|y"Axis caption; y reads bottom-to-top

CSS Variables:

VariableDefaultDescription
--pos0Linear tick position, 0100 (percent of the axis; set on the tick)
--index0Radial tick index, 0-based (set on the tick)
--casca-axis-count6Radial spoke count (set on the band to match the radar)
--casca-axis-radius56%Radial label distance from center (just outside the ring)
--casca-axis-y-width2.25remLeft tick gutter in .casca-plot
--casca-axis-x-height1.25remBottom tick gutter in .casca-plot

Features:

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.

VariableDefaultPurpose
--casca-prose-max-width45rem (720px)Measured column width
--casca-prose-font-family'Newsreader', 'Iowan Old Style', Georgia, serifBody font stack
--casca-prose-font-size1.0625rem (17px)Body font size
--casca-prose-line-height1.6Body leading
--casca-prose-colorvar(--casca-gray-9)Body color (light)
--casca-prose-color-darkvar(--casca-gray-1)Body color (dark)
--casca-prose-heading-colorvar(--casca-gray-9)Heading color (light)
--casca-prose-heading-color-darkvar(--casca-gray-0)Heading color (dark)
--casca-prose-heading-fontvar(--casca-font-sans)Heading font stack
--casca-prose-link-colorvar(--casca-color-1)Link color
--casca-prose-link-color-hovervar(--casca-color-2)Link hover color
--casca-prose-code-colorvar(--casca-gray-9)Inline code color
--casca-prose-code-bgvar(--casca-gray-2)Inline code background
--casca-prose-code-fontvar(--casca-font-mono)Code font stack
--casca-prose-pre-colorvar(--casca-gray-0)Block code color
--casca-prose-pre-bgvar(--casca-gray-9)Block code background
--casca-prose-blockquote-colorvar(--casca-gray-7)Blockquote text
--casca-prose-blockquote-bordervar(--casca-color-1)Blockquote accent stripe
--casca-prose-rule-colorvar(--casca-gray-3)<hr> and table rules
--casca-prose-marker-colorvar(--casca-color-1)List marker accent
--casca-prose-block-spacingvar(--casca-size-4)Vertical rhythm

Contract notesLink to section

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 / attributePurpose
.casca-prose .casca-prose-calloutExplicit 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 summaryNative prose disclosure styling
.casca-prose dl, .casca-prose dt, .casca-prose ddDefinition-list typography
.casca-prose .casca-prose-anchorSSG-emitted heading permalink
.casca-prose .casca-prose-anchor-labelVisually-hidden heading-anchor accessible name

Prose Extension CSS VariablesLink to section

Override at :root or on .casca-prose.

VariableDefaultPurpose
--casca-prose-callout-padding-blockvar(--casca-size-3)Callout block padding
--casca-prose-callout-padding-inlinevar(--casca-size-4)Callout inline padding
--casca-prose-callout-radiusvar(--casca-radius-2)Callout corner radius
--casca-prose-callout-border-width1pxCallout top, end, and bottom border width
--casca-prose-callout-accent-width3pxCallout leading accent stripe
--casca-prose-callout-colorper variantOptional override for the active callout role color
--casca-prose-callout-bgpage background chain, tinted with color-mix() when supportedCallout background
--casca-prose-callout-bordervar(--casca-rule, var(--casca-gray-3)), tinted with color-mix() when supportedCallout border color
--casca-prose-callout-note / --casca-prose-callout-note-darkvar(--casca-gray-7) / var(--casca-gray-3)Note role color
--casca-prose-callout-tip / --casca-prose-callout-tip-darkvar(--casca-color-2)Tip role color
--casca-prose-callout-warning / --casca-prose-callout-warning-darkvar(--casca-color-3)Warning role color
--casca-prose-callout-danger / --casca-prose-callout-danger-darkvar(--casca-color-8)Danger role color
--casca-prose-callout-important / --casca-prose-callout-important-darkvar(--casca-color-1)Important role color
--casca-prose-details-bg / --casca-prose-details-bg-darktransparentDisclosure background
--casca-prose-details-border / --casca-prose-details-border-darkvar(--casca-gray-3) / var(--casca-gray-7)Disclosure border
--casca-prose-details-border-width1pxDisclosure border width
--casca-prose-details-padding-blockvar(--casca-size-3)Disclosure block padding
--casca-prose-details-padding-inlinevar(--casca-size-4)Disclosure inline padding
--casca-prose-details-radiusvar(--casca-radius-2)Disclosure corner radius
--casca-prose-details-marker-color / --casca-prose-details-marker-color-darkvar(--casca-color-1)Disclosure marker color
--casca-prose-details-marker-size0.75emDisclosure marker size
--casca-prose-dl-term-colorvar(--casca-prose-heading-color)Definition term color
--casca-prose-dl-term-fontvar(--casca-prose-heading-font)Definition term font
--casca-prose-dl-term-weightvar(--casca-font-weight-7)Definition term weight
--casca-prose-dl-term-spacing-abovevar(--casca-size-4)Space before the next term
--casca-prose-dl-definition-colorvar(--casca-prose-color)Definition text color
--casca-prose-dl-definition-indentvar(--casca-size-4)Definition inline indent
--casca-prose-dl-definition-gapvar(--casca-size-2)Gap between sibling definitions
--casca-prose-anchor-colorvar(--casca-prose-link-color)Heading anchor color
--casca-prose-anchor-color-hovervar(--casca-prose-link-color-hover)Revealed heading anchor color
--casca-prose-anchor-opacity0Resting heading anchor opacity
--casca-prose-anchor-opacity-revealed1Hover, focus, and touch heading anchor opacity
--casca-prose-anchor-gapvar(--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

VariableDefaultPurpose
--casca-divider-pixel-colorvar(--casca-color-1)Pixel-block fill
--casca-divider-pixel-label-colorvar(--casca-gray-6)Label color (light)
--casca-divider-pixel-label-color-darkvar(--casca-gray-5)Label color (dark)
--casca-divider-pixel-fontvar(--casca-font-mono)Font for blocks + label
--casca-divider-pixel-block-sizevar(--casca-font-size-3)Pixel-block size
--casca-divider-pixel-label-sizevar(--casca-font-size-0)Label size
--casca-divider-pixel-gapvar(--casca-size-3)Gap between blocks and label
--casca-divider-pixel-margin-blockvar(--casca-size-6)Vertical room around divider
--casca-divider-pixel-letter-spacing0.18emLetter-spacing for the label

Contract notesLink to section


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:

SlotPurpose
brandHeader brand slot. The template provides fallback text.
navHeader navigation links.
theme-pickerHeader mood picker form. Keep it in light DOM so :has() preview works.
heroLanding-only hero area. Non-landing variants suppress the shadow slot.
footerFooter content. The template provides fallback text.
unnamed defaultMain content rendered inside the shadow <main>.

Variant contract:

data-variantMain widthTypographyNotes
omitted or doc50remProse-likeDefault.
article38remProse-likeNarrow editorial column.
demo64remCasca sansWider component demo surface.
landing80remCasca sansRenders 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

VariableDefaultPurpose
--casca-layout-doc-max-inline50remDefault and doc main width
--casca-layout-article-max-inline38remarticle main width
--casca-layout-demo-max-inline64remdemo main width
--casca-layout-landing-max-inline80remlanding main width
--casca-layout-padding-block-startvar(--casca-size-5)Main block-start padding
--casca-layout-padding-block-endvar(--casca-size-6)Main block-end padding
--casca-layout-padding-inlinevar(--casca-size-3)Main inline padding
--casca-layout-gapvar(--casca-size-4)Landing hero to main gap
--casca-layout-header-bgvar(--casca-surface-1)Header background
--casca-layout-header-bordervar(--casca-rule)Header block-end rule
--casca-layout-header-block-paddingvar(--casca-size-2)Header vertical padding
--casca-layout-footer-bgvar(--casca-surface-1)Footer background
--casca-layout-footer-bordervar(--casca-rule)Footer block-start rule
--casca-layout-footer-block-paddingvar(--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

VariableDefaultPurpose
--casca-wordmark-size3.5remPre-scroll height (56px)
--casca-wordmark-size-scrolled2remScrolled height (32px)
--casca-wordmark-transition-duration0.18sSize transition timing

Contract notesLink to section

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

VariableDefaultPurpose
--casca-site-header-bgvar(--casca-surface-1)Background
--casca-site-header-colorvar(--casca-gray-9)Text color (light)
--casca-site-header-color-darkvar(--casca-gray-0)Text color (dark)
--casca-site-header-bordervar(--casca-gray-3)Block-end rule (light)
--casca-site-header-border-darkvar(--casca-gray-7)Block-end rule (dark)
--casca-site-header-pad-blockvar(--casca-size-2)Vertical padding
--casca-site-header-pad-inlinevar(--casca-size-4)Horizontal padding
--casca-site-header-gapvar(--casca-size-4)Gap between slots
--casca-site-header-min-block3.5remMinimum block-size
--casca-site-header-fontvar(--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:

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

VariableDefaultPurpose
--casca-theme-picker-size1.5rem (24px)Button edge length (desktop)
--casca-theme-picker-size-touch2.75rem (44px)Button edge length on touch (DR6)
--casca-theme-picker-gapvar(--casca-size-1)Gap between buttons
--casca-theme-picker-radiusvar(--casca-radius-1)Button border-radius
--casca-theme-picker-bordervar(--casca-gray-4)Unchecked border (light)
--casca-theme-picker-border-darkvar(--casca-gray-6)Unchecked border (dark)
--casca-theme-picker-border-checkedvar(--casca-gray-9)Checked border (light)
--casca-theme-picker-border-checked-darkvar(--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

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

VariableDefaultPurpose
--casca-hero-max-inline64remMaximum column width
--casca-hero-pad-blockvar(--casca-size-6)Top/bottom padding
--casca-hero-pad-inlinevar(--casca-size-4)Left/right padding
--casca-hero-gapvar(--casca-size-4)Gap between slots
--casca-hero-figure-block13.75rem (220px)Figure slot height (DESIGN.md)
--casca-hero-caption-fontvar(--casca-font-display)Caption row font (Departure)
--casca-hero-caption-size0.75rem (12px)Caption font-size
--casca-hero-caption-colorvar(--casca-gray-7)Caption color (light)
--casca-hero-caption-color-darkvar(--casca-gray-3)Caption color (dark)
--casca-hero-caption-letter0.08emCaption letter-spacing
--casca-hero-manifesto-fontvar(--casca-font-serif)Manifesto line font (Newsreader)
--casca-hero-manifesto-size1.0625remManifesto font-size
--casca-hero-manifesto-colorvar(--casca-gray-7)Manifesto color (light)
--casca-hero-manifesto-color-darkvar(--casca-gray-3)Manifesto color (dark)
--casca-hero-brand-gapvar(--casca-size-2)Gap between wordmark + manifesto

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

VariableDefaultPurpose
--casca-site-footer-bgvar(--casca-surface-1)Background
--casca-site-footer-colorvar(--casca-gray-7)Default text color (light)
--casca-site-footer-color-darkvar(--casca-gray-3)Default text color (dark)
--casca-site-footer-bordervar(--casca-gray-3)Block-start rule (light)
--casca-site-footer-border-darkvar(--casca-gray-7)Block-start rule (dark)
--casca-site-footer-pad-blockvar(--casca-size-5)Top/bottom padding
--casca-site-footer-pad-inlinevar(--casca-size-4)Left/right padding
--casca-site-footer-gapvar(--casca-size-2)Gap between row + line
--casca-site-footer-row-fontvar(--casca-font-display)Row font (Departure Mono)
--casca-site-footer-row-size0.75rem (12px)Row font-size
--casca-site-footer-row-colorvar(--casca-gray-9)Row color (light)
--casca-site-footer-row-color-darkvar(--casca-gray-0)Row color (dark)
--casca-site-footer-row-letter0.12emRow letter-spacing
--casca-site-footer-row-gapvar(--casca-size-2)Gap between phrases
--casca-site-footer-row-separator"\B7" (U+00B7)Pip character between phrases
--casca-site-footer-line-fontvar(--casca-font-serif)Italic line font (Newsreader)
--casca-site-footer-line-size1remItalic line font-size
--casca-site-footer-line-colorvar(--casca-gray-7)Italic line color (light)
--casca-site-footer-line-color-darkvar(--casca-gray-3)Italic line color (dark)
--casca-site-footer-line-max36remItalic line max-inline-size

Contract notesLink to section


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:

AttributeValuesDescription
data-variantsuccess, warning, error, info, outline, neutralChip color scheme (neutral = filled, low-emphasis category/count tag; meaning rides on the text, not color)
data-positionabsoluteEnable absolute positioning

Chip CSS Variables:

VariableDefaultDescription
--chip-bgvar(--casca-gray-9)Background color
--chip-colorvar(--casca-gray-0)Text color
--chip-bordertransparentBorder color
--chip-topautoTop position (absolute)
--chip-rightautoRight position (absolute)
--chip-bottomautoBottom position (absolute)
--chip-leftautoLeft 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:

ID Pattern Flexibility:

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:

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:

ClassDescription
.casca-pagerContainer for pager buttons and label
.casca-pager-btnBase button/link class (required)
.casca-pager-btn-prevLeft half (rounded left corners)
.casca-pager-btn-nextRight half (rounded right corners)
.casca-pager-labelFloating center label (absolute positioned, z-index: 10)

Features:

States:

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:

ClassDescription
.casca-legend-rowContainer (CSS Grid: auto 1fr auto)
.casca-legend-row-labelMiddle column, truncates with ellipsis (min-width: 0)
.casca-legend-row-railRight column, flex container for chips (flex-shrink: 0)
.casca-legend-swatchColor indicator (left column)

Attributes:

AttributeValuesDescription
hrefURLMakes row interactive (link)
typebutton, submitMakes 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:

States:

Layout Details:

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:

ClassDescription
.casca-slot-gridThe <fieldset> container (CSS Grid; max columns = --casca-slot-cols)
.casca-slot-grid-titleThe <legend>; spans the full grid width
.casca-slot-colA single day / resource column (vertical stack of slots)
.casca-slot-colheadColumn header text (e.g. "Mon 27")
.casca-slotA selectable slot; the <label> wrapping the radio
.casca-slot-inputThe native <input type="radio"> (visually hidden, still focusable)
.casca-slot-timeThe visible time label inside a slot

Attributes:

AttributeOnValuesDescription
--casca-slot-cols.casca-slot-grid (inline custom property)integerMaximum 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-inputstringStandard radio name + submitted value

States (all pure CSS):

Features:

Slot Grid CSS Variables:

VariableDefaultDescription
--casca-slot-cols5Maximum column count; columns collapse below this as the grid narrows
--casca-slot-col-min4.5remMinimum column width before columns collapse (responsive threshold)
--casca-slot-gap--casca-size-2Gap between columns and slots
--casca-slot-radius--casca-radius-2Slot corner radius
--casca-slot-min-size2.75remMinimum slot size (44px touch target)
--casca-slot-bg / -color / -bordergraysAvailable slot colors (light)
--casca-slot-hover-bg / -bordergraysHover colors (light)
--casca-slot-selected-bg / -border--casca-color-1Selected fill / border (accent); auto-lightens in dark
--casca-slot-selected-color--casca-on-accentSelected label color; tracks the accent foreground (white in light, dark in dark-mode themes)
--casca-slot-taken-bg / -colorgraysTaken / disabled colors (light)
--casca-slot-pending-bg / -color / -borderyellow familyPending / hold colors (light); defaults to the casca yellow ramp, map to your scheduling palette
--casca-slot-focus-ring--casca-color-1Focus outline color
--casca-slot-*-darkgrays / yellowDark-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-state picks the look; disabled is the single signal for "not selectable" (it drives the not-allowed cursor and suppresses the hover affordance). A data-state="taken"/"pending" slot without a disabled input still looks taken/pending but remains clickable - set disabled.
  • selected reflects an enabled checked input. A disabled checked slot (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 is disabled). Omit the attribute rather than emitting it empty.
  • Adding a new non-interactive state needs only one [data-state="x"] appearance rule plus a disabled input; 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 / attributeOnDescription
.casca-analyticsroot sectionAnalytics component root, used with .casca
.casca-analytics-heroheaderPublic lead section
.casca-analytics-kickertextSmall register label
.casca-analytics-headlineheadingState heading
.casca-analytics-valuespanNumeric value with tabular figures
.casca-analytics-unitspanUnit text, usually page views
.casca-analytics-datespanPublic date label
.casca-analytics-ledeparagraphPublic ethics line
.casca-analytics-checklistlistNon-extractive checklist
.casca-analytics-state-noteparagraphVisible state note
.casca-analytics-trenddivTrend table wrapper
.casca-analytics-trend-tabletableVisible semantic trend table
.casca-analytics-counttable cellNumeric count cell
.casca-analytics-barspanPresentational bar, aria-hidden="true"
.casca-analytics-day-notespanVisible day status
.casca-analytics-registerssectionPublic and ops register wrapper
.casca-analytics-registerdivOne register panel
.casca-analytics-register-titleheadingRegister heading
.casca-analytics-statusparagraphCompact summary status
.casca-analytics-noticesectionIntegrity-fail notice body
.casca-analytics-notice-titleheadingIntegrity-fail title
.casca-analytics-notice-bodyparagraphIntegrity-fail explanation
.casca-analytics-last-goodparagraphLast verified timestamp line
.casca-analytics-verifyfooterProvenance footer
.casca-analytics-verify-linklinkVerification link
.casca-analytics-verify-recipeblockOptional plain verification steps
data-casca-analytics-variant="dashboard | summary"rootLayout variant
data-casca-analytics-state="ok | never-published | zero-traffic | single-day | stale | preliminary | integrity-fail"rootRendered state
data-casca-analytics-cause="missing-feed | ttl-exceeded | publisher-timestamp-behind | hash-mismatch | signature-invalid | signature-absent | schema-mismatch | json-parse | field-invalid"rootOptional state cause
data-casca-analytics-register="public | ops"registerRegister vocabulary
data-casca-analytics-day-state="sealed | preliminary"trend rowDay finality
--casca-analytics-bar-size: NN%bar inline styleServer-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:

  1. Per-site (asset shipping). Set [build] live_layer = true in casca.toml. When true, the symmetrics preprocessor emits a per-block JSON sidecar at _casca/preprocessors/symmetrics/<cache_key>.json. When false (the default), no sidecar is emitted and the analytics block remains purely static.
  2. 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 / attributeOnDescription
.casca-table<table>The styled table (border-collapse, zebra, row hover)
.casca-table-wrapwrapper <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:

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 off aria-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 uses color-mix (selection is still conveyed by the checked box where color-mix is 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:

ClassOnDescription
.casca-range-fieldwrapper <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:

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.3 accent-color is 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 / attributeOnDescription
.casca-stat-gridwrapper <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:

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:

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:

ClassOnDescription
.casca-date-fieldwrapper <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:

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:

ClassOnDescription
.casca-color-fieldwrapper <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:

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>
ClassDescription
.casca-toggles<fieldset> chip row
.casca-togglea <label>: a checkbox cell + a decorator cell, joined as a two-segment control (the box does NOT wrap the checkbox)
.casca-toggle-boxbordered, padded cell that frames the native checkbox
.casca-toggle-inputthe native checkbox (value="N"); keeps its default appearance
.casca-toggle-labelthe decorator cell (swatch + text); shares a seam with the box
.casca-toggle-swatchcolor 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-data table always lists every series regardless of what's toggled off.


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:

ClassOnDescription
.casca-modalwrapper <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:

AttributeOnValuesDescription
data-slide.casca-modal-box(bare) / top / bottom / left / rightSlide in from / out to the given direction. Bare attribute defaults to top. Without data-slide, the modal just fades.

Features:

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

VariableDefaultPurpose
--casca-thumb-block-size4remFixed 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 :root globals. They carry no base/theme of their own - load over casca-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:

Progressive Enhancement:


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