# st-core.fscss v2: Full Release Notes & Technical Reference

**Pure CSS statistical dashboard components. No JavaScript dependencies. No SVG. No canvas.**

st-core.fscss is a CSS visualization system built on the FSCSS ecosystem. It renders area charts, multi-line charts, stat cards, and dashboard chrome using nothing but `clip-path: polygon()`, `repeating-linear-gradient()`, and CSS custom properties. This post covers the full v2 release: what changed, every mixin the library ships, and how the underlying array-to-polygon math actually works.

MIT licensed. Repo: [github.com/fscss-ttr/st-core.fscss](https://github.com/fscss-ttr/st-core.fscss). Requires FSCSS v1.2.3+.

---

## What's New in v2

v1 charts were locked to exactly 8 fixed data points, mapped onto hardcoded variables `--st-p1` through `--st-p8`. v2 removes that ceiling:

- **Dynamic datasets (`@arr`)** — pass arrays of any length: 5 points, 12, 50+.
- **Automatic X-spacing** — points distribute evenly across `0%` to `100%` based on `array.length`, no manual positioning.
- **Inline normalization** — natural 0–100 values get normalized to CSS percentages directly inside the style calculations.
- **Dual-edge polygon polyline** — line strokes are drawn as a closed top-to-bottom band rather than a hack around `border`, giving precise stroke rendering.

The v1 API (`@st-chart`, 8 named params) is still available for anyone with a genuinely fixed 8-point series who doesn't want to declare an array.

---

## Installation

**Runtime / CDN mode** — for prototyping, drop this in your `<head>`:

```html
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.3/runtime.min.js" async></script>
```

Then import the module in your `<style>` block:

```css
@import((*) from st-core@v2)
```

**CLI / compiled mode** — for production, compile `.fscss` straight to `.css`:

```bash
npm install -g fscss@latest
fscss input.fscss output.css
```

VS Code syntax highlighting and auto-compile: [FSCSS Support extension](https://marketplace.visualstudio.com/items?itemName=Figsh.fscss).

---

## How It Works, Conceptually

Every chart is a polygon. Define an array, apply a renderer mixin against it, done:

```css
@arr myData[50, 10, 97, 35, 66, 50, 80, 54, 70, 60]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)
```

The mixin reads the array's length to compute horizontal steps, reads normalized Y values to compute vertical position, and emits a `clip-path: polygon()`. Two responsibilities are deliberately kept separate:

- **Renderers** (`@st-chart-fill`, `@st-chart-line`, `@st-chart-dots`) declare *shape*. They take an array only to know how many points to loop over.
- **`@st-chart-points(array)`** declares *values*. It's the only mixin that writes `--st-p1`…`--st-p{n}` onto an element. Every renderer only *reads* those variables.

Because these are ordinary inherited custom properties, any child element that skips `@st-chart-points` inherits values from the nearest ancestor that called it. That's convenient for a single-series chart (call it once, on the container) and mandatory to override for multi-series charts (each series needs its own call, or it silently renders its neighbor's data).

---

## Full Mixin Reference

### `@st-root()` — Design Tokens

```css
@st-root()                 /* targets :root */
@st-root(root.class...)    /* targets a custom scope */
```

Initializes every color, radius, and spacing token the rest of the library reads via `var(--st-*)`. Nothing downstream hardcodes a color. It also seeds `--st-p1`…`--st-p8` with placeholder defaults — a safety net so a component reading `--st-p$i` before `@st-chart-points` has run gets a plausible shape instead of a broken layout, rather than `undefined`.

### `@st-container(selector)` — Viewport Wrapper

```css
@st-container(body)
```

Centers content in the viewport with the design-token background and text color. Defaults to `body`.

### `@st-phone(selector)` — Device Frame

```css
@st-phone(.wrapper)
```

A 360px-wide rounded card frame with layered shadow, meant to mimic a phone-sized dashboard surface. `overflow: hidden` keeps chart fills and lines from bleeding past the rounded corners.

### `@st-chart-points(array)` — Value Normalizer

```css
.chart {
  @st-chart-points(myData)
}
```

Builds a throwaway index array the same length as your data, loops over it, and for each index writes:

```
--st-p{i}: (100 - value_at_i)%
```

The inversion matters because CSS boxes render top-down (`0%` is the top) while chart data conceptually grows bottom-up. This is the *only* mixin in the library that writes `--st-p*` from an arbitrary-length array — call it once per dataset, on whichever element needs those values (container, or a specific child for a second series).

### `@st-chart-fill(selector, array)` — Area Fill

```css
@st-chart-fill(.chart-fill, myData)
```

Builds the same index-array loop as `@st-chart-points`, but uses it to emit `clip-path: polygon()` stops instead of variables. X-position per point is computed inline as `(i - 1) * 100 / (length - 1)`, spreading points evenly regardless of array length. Y-position reads `var(--st-p{i})` — it never touches raw array values directly. The loop generates only the top edge of the shape; `100% 100%, 0% 100%` closes it down to the bottom corners. The fill itself is a top-to-bottom gradient from translucent `--st-accent` to transparent, layered on top of the polygon shape.

### `@st-chart-line(selector, array)` — Polyline Stroke

```css
@st-chart-line(.chart-line, myData)
```

`clip-path` has no native stroke concept, so this mixin fakes one by drawing a thin *closed band*: the top edge follows the data forward (left to right), the bottom edge follows the same data in reverse (right to left), offset downward by `--st-chart-line-width`. Walking the second pass in reverse is what closes the polygon without the shape crossing itself. The band is filled solid with `var(--st-accent)`, which reads visually as a line with real thickness.

### `@st-chart-line-width(value)` — Stroke Width Override

```css
.chart-line {
  @st-chart-line-width(2.5px);
}
```

Overrides the `1.5px` default set in `@st-root`. Since `@st-chart-line` reads `var(--st-chart-line-width)` at draw time, this is also changeable live via `element.style.setProperty('--st-chart-line-width', '3px')` without recompiling anything.

### `@st-chart-dot(selector, x%, y%, size)` — Single Manual Marker

```css
@st-chart-dot(.chart-dot, 70, 60, 12px)
```

Unlike the renderer mixins above, this takes explicit coordinates rather than an array — for annotating one specific point (a peak, a tooltip anchor) independent of any dataset. Applies the same top-down inversion (`100 - y`) inline rather than through the variable system, and centers the marker on the coordinate via a `-6px` offset on both axes.

### `@st-chart-dots(prefix, array, size)` — Auto-Generated Markers

```css
@st-chart-dots(.dot-, myData, 8px)
```

Generates `.dot-1`, `.dot-2`, ... `.dot-N` — one rule per array index. Internally it writes `top` twice: once computed directly from the raw array value (a self-contained fallback with no dependency on `--st-p*`), then again from `var(--st-p{i})`. CSS resolves duplicate declarations in source order, so the second write wins at render time — meaning `@st-chart-dots`, despite having a self-sufficient fallback, still effectively requires `@st-chart-points(array)` to have run somewhere in scope for correct vertical positioning. Horizontal positioning is fully self-sufficient either way, since it's computed from array length alone.

### `@st-chart-grid(selector, rows, cols)` — Background Grid

```css
@st-chart-grid(.chart-grid, 10, 7)
```

No polygons involved — two stacked `repeating-linear-gradient()`s do the work. One draws horizontal lines every `100% / rows`, the other vertical lines every `100% / cols`, each a hard 1px stripe followed by transparent space. The two axes are tinted differently (`--st-muted` vs `--st-accent`) as a subtle visual cue, and the whole thing sits at `opacity: .2` so it reads as reference lines rather than competing with the actual data.

### `@st-chart-axis-x(selector)` / `@st-chart-axis-y(selector)` — Axis Wrappers

```css
@st-chart-axis-x(.x-axis)
@st-chart-axis-y(.y-axis)
```

Pure layout, no data dependency — they just space out hand-written `<span>` labels. The X axis is a horizontal flex row with `justify-content: space-between`. The Y axis uses `flex-direction: column-reverse` so a `0, 20, 40...100` label list reads correctly bottom-to-top against a chart where `0%` is the top of the box, and is positioned `absolute` to overlay the chart rather than push content beside it.

### `@st-stat-card(selector)` — Stat Card Component

```css
@st-stat-card(.stat-card)
```

One call generates five rules at once: the card container plus three fixed inner-class conventions — `.st-stat-label`, `.st-stat-value`, `.st-stat-delta` — and two delta-direction modifiers, `.up` (green) and `.down` (red). The mixin owns that internal naming contract, so markup only needs to use the fixed class names inside a `.stat-card`.

### `@st-cat-bar-fill(selector, range)` — Progress Bar Fill

```css
@st-cat-bar-fill(.bar-fill, 75)
```

Unrelated to the line/area system — for horizontal progress or category bars. `range` is a plain percentage, written to `--st-cat-bar-fill-range` and consumed as `width`. `transform-origin: left` is preset in case you want to animate the fill in with a `scaleX` transform from zero.

---

## Updating Charts from JavaScript

Pure CSS rendering doesn't mean the data has to be static. Updating a chart at runtime is just rewriting the `--st-p{n}` variables directly:

```js
const chartLine = document.querySelector(".chart-line");
const normalize = (n) => (100 - n) + '%';

function updatePoints(pointsArray) {
  const cssVars = pointsArray
    .map((v, i) => `--st-p${i + 1}: ${normalize(v)};`)
    .join(' ');

  chartLine.style.cssText = cssVars;
}

updatePoints([50, 20, 85, 40, 95]);
```

Add `transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);` to `.chart-fill` / `.chart-line`, and dataset changes animate through the compositor thread, untouched by layout or paint.

---

## Worked Examples

### Basic area + line chart

```html
<style>
@import((*) from st-core@v2)
@st-root()

@arr myData[20, 45, 28, 80, 65, 90, 40]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)

.chart {
  @st-chart-points(myData)
  position: relative;
  height: 200px;
  width: 100%;
  max-width: 400px;
  background: var(--st-surface);
  border-radius: 16px;
}
</style>

<div class="chart">
  <div class="chart-fill"></div>
  <div class="chart-line"></div>
</div>
```

### Multi-line, multi-area with independent series

```css
@arr seriesA[30, 50, 75, 40, 85, 60]
@arr seriesB[10, 25, 45, 20, 55, 30]

@st-chart-fill(.fill-a, seriesA)
@st-chart-line(.line-a, seriesA)
@st-chart-fill(.fill-b, seriesB)
@st-chart-line(.line-b, seriesB)

.chart {
  @st-chart-points(seriesA)
  position: relative;
  height: 220px;
  width: 100%;
  background: var(--st-bg);
}

.fill-a { @st-chart-points(seriesA) opacity: 0.6; --st-accent: #9d7eff; }
.line-a { @st-chart-points(seriesA) --st-accent: #9d7eff; }

/* .fill-b / .line-b MUST set their own points — otherwise they
   inherit .chart's seriesA values and render the wrong data */
.fill-b { @st-chart-points(seriesB) opacity: 0.3; --st-accent: #4fffb0; }
.line-b { @st-chart-points(seriesB) --st-accent: #4fffb0; }
```

```html
<div class="chart">
  <div class="chart-fill fill-a"></div>
  <div class="chart-line line-a"></div>
  <div class="chart-fill fill-b"></div>
  <div class="chart-line line-b"></div>
</div>
```

### Full mobile dashboard frame

```html
<style>
@import((*) from st-core@v2)
@st-root()
@st-container(body)
@st-phone(.wrapper)

.wrapper { display: flex; flex-direction: column; gap: 16px; padding: 24px; }

@arr myData[56, 67, 70, 43, 67, 80]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)
@st-chart-dot(.chart-dot, 70, 60)
@st-stat-card(.stat-card)
@st-chart-axis-x(.x-axis)
@st-chart-axis-y(.y-axis)
@st-chart-grid(.chart-grid, 10, 7)

.chart {
  width: 100%; height: 200px; border-radius: 20px;
  position: relative; overflow: hidden;
  background: var(--st-surface);
  @st-chart-points(myData)
}
.chart-fill, .chart-line { transition: clip-path 0.8s ease-in-out; }
</style>

<div class="wrapper">
  <div class="stat-card">
    <div class="st-stat-label">TOTAL EXPENSES</div>
    <div class="st-stat-value">$1,326.03</div>
    <div class="st-stat-delta up">+5.1% vs last week</div>
  </div>

  <div class="chart">
    <div class="chart-fill"></div>
    <div class="chart-line"></div>
    <div class="chart-dot"></div>
    <div class="chart-grid"></div>
    <div class="y-axis">
      <span>0</span><span>20</span><span>40</span><span>60</span><span>80</span><span>100</span>
    </div>
  </div>

  <div class="x-axis">
    <span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span>
  </div>
</div>
```

---

## The One Rule That Ties It Together

| Concern | Who owns it | Depends on `--st-p*`? |
|---|---|---|
| X-position (horizontal spacing) | Computed inline per mixin, from array **length** | No |
| Y-position (vertical value) | `var(--st-p{i})`, written only by `@st-chart-points` | Yes |
| Shape (fill / line / dot) | `@st-chart-fill` / `@st-chart-line` / `@st-chart-dots` | Reads, never writes |
| Values | `@st-chart-points(array)` | Writes, on whichever element calls it |

Every renderer only needs the array to know *how many* points to loop over. The actual values reach the page exclusively through `--st-p1`…`--st-p{n}`, inherited down the DOM like any custom property. That's the entire reason a multi-series chart needs `@st-chart-points(seriesN)` called explicitly on each series' own element: skip it, and that element silently inherits whatever its nearest ancestor last set.

---

## Design Token Reference

| Variable | Default | Usage |
|---|---|---|
| `--st-bg` | `#0e0d14` | Page body background |
| `--st-surface` | `#161422` | Surface / container background |
| `--st-card` | `#1c1a2e` | Card component background |
| `--st-accent` | `#9d7eff` | Primary brand accent |
| `--st-accent-2` | `#c4a8ff` | Secondary accent gradient |
| `--st-green` | `#4fffb0` | Positive delta state |
| `--st-red` | `#ff5e7d` | Negative delta state |
| `--st-text` | `#e8e3ff` | Primary text color |
| `--st-muted` | `#6b6488` | Muted labels / grid stroke |
| `--st-radius-xl` | `40px` | Outer device frame radius |
| `--st-radius-lg` | `16px` | Component card radius |
| `--st-chart-line-width` | `1.5px` | Line stroke thickness |

---

## Performance & SEO

- **Zero hydration overhead** — pure CSS charts need no JS initialization before rendering.
- **Tiny compiled size** — lightweight CSS shapes and variables, around 0.8kb minified.
- **GPU-accelerated transitions** — dataset updates through CSS custom properties run on the browser's compositor thread.

---

MIT licensed, built with [FSCSS](https://fscss.devtem.org). Repo and contributions: [github.com/fscss-ttr/st-core.fscss](https://github.com/fscss-ttr/st-core.fscss).
