@sema-agent/server 1.196.0 → 1.197.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ # Choosing a form
2
+
3
+ Decide this **before** color. The data's job picks the form — and sometimes the
4
+ right form is not a chart.
5
+
6
+ ## Is it even a chart?
7
+
8
+ | The data is… | Use | Not |
9
+ |---|---|---|
10
+ | A single current value (+ maybe a trend) | **Stat tile** (value + delta + sparkline) | A one-bar bar chart |
11
+ | A handful of headline numbers | **KPI row** of stat tiles | A grouped bar chart |
12
+ | The one number a dashboard leads with | **Hero figure** (≥48px, sans) | — |
13
+ | A single ratio against a limit | **Meter** (same-ramp track) | A pie of 2 slices |
14
+ | More than ~7 classes that all carry meaning | A **table** (or table + chart) | More colors |
15
+
16
+ If a chart *is* right, pick the type by the job:
17
+
18
+ ## The job → the type
19
+
20
+ | Job (what the reader must do) | Default form | Color job |
21
+ |---|---|---|
22
+ | Compare magnitude, low → high | bar / column; **heatmap** for a grid | sequential (one hue) |
23
+ | Trend over time | line; area for a single series | sequential or 1 categorical |
24
+ | Tell distinct series apart | grouped/stacked bar, multi-line | **categorical** |
25
+ | One series is the point, rest are context | **emphasis** (highlight one, gray the rest) | 1 hue + gray |
26
+ | Above/below a baseline; Δ to target | diverging bar, or line vs baseline | diverging |
27
+ | Part-to-whole | **stacked bar** (go horizontal for many / long-named categories) | categorical |
28
+ | Ordered-scale share (Likert, sentiment, agree↔disagree) | **diverging stacked bar**, centered on neutral | diverging |
29
+ | Before → after per item | dumbbell | 1 hue, 2 shades |
30
+
31
+ ## The rules behind the table
32
+
33
+ - **Sequential is the safe default.** One hue, more-is-darker. It stays legible and
34
+ consistent and is hard to misread. Reach for it unless the data's job is
35
+ specifically *identity* or *polarity*.
36
+ - **Categorical is for when the series ARE the subject** — and it has a real cost:
37
+ it can bury the one data point that actually matters. If the story is "this one
38
+ went up," that's **emphasis**, not categorical.
39
+ - **Emphasis** = the most underused form. One series in the accent hue, the rest in
40
+ the de-emphasis gray. Often the honest answer to "make this chart clearer."
41
+ - **Texture is an opt-in expression, not a default form.** It earns its place only
42
+ for accessibility (full CVD), print/export, and `forced-colors`. Never decorative.
43
+ → see `marks-and-anatomy.md`.
44
+
45
+ ## Series-count ladder (categorical)
46
+
47
+ | Series | Treatment |
48
+ |---|---|
49
+ | 1–3 | color alone is comfortable for everyone; direct-label |
50
+ | 4 | the CVD floor enters — direct labels become mandatory, not a courtesy |
51
+ | 5–6 | soft cap; legend or small multiples |
52
+ | 7–8 | token ceiling; past it, fold the tail into "Other," facet into small multiples, or use composite encoding (hue × shape) |
53
+
54
+ Never solve "too many series" by generating more hues. A generated 9th hue is
55
+ indistinguishable from an existing one under CVD and breaks every check.
56
+
57
+ ---
@@ -0,0 +1,113 @@
1
+ # Color formula
2
+
3
+ Color is **not hand-picked**. Every chart color does exactly one of four jobs, and a
4
+ palette is legal only if it passes six checks. The checks are the product — they are
5
+ what makes a palette safe to change and what lets the same method run on any design
6
+ system's ramps.
7
+
8
+ ## The four jobs
9
+
10
+ | Job | What it encodes | Structure |
11
+ |---|---|---|
12
+ | **Categorical** | identity (which series) | 8 hues, fixed order, assigned in sequence, never cycled |
13
+ | **Ordinal** | position in a sequence (funnel stage, tier, bucket) | one hue, monotone lightness steps; light end still ≥ 2:1 on surface |
14
+ | **Sequential** | magnitude (how much) | one hue, steps 100→700, light→dark; flips anchor in dark |
15
+ | **Diverging** | polarity (which side of a baseline) | two hues + a neutral gray midpoint; equal steps per arm |
16
+ | **Status** | state (good→critical) | a small fixed scale, reserved meaning, always icon+label |
17
+
18
+ **Categorical or ordinal?** If swapping the category order would change the
19
+ meaning — funnel stages, size tiers (S/M/L), age bands, cohort buckets — it is
20
+ **ordinal** and takes a one-hue ramp so the reader sees the order in the color.
21
+ If swapping would not — product names, teams, regions, endpoints — it is
22
+ **nominal categorical** and each bar takes the *same* slot-1 hue (one series,
23
+ so no legend box — the title names it), or slots 1..N when there are N separate
24
+ series. Never color nominal bars by their value: that spends the identity channel
25
+ re-encoding what bar length already shows.
26
+
27
+ ## The six checks
28
+
29
+ Every categorical color — current or proposed — must pass all six.
30
+
31
+ 1. **Fixed hue anchors.** Eight families in a fixed order. The order is the
32
+ CVD-safety mechanism; it never changes. *(structural — enforced, not measured)*
33
+ 2. **Lightness band per mode.** OKLCH L ≈ 0.43–0.77 light; ≈ 0.48–0.67 dark. *(validator)*
34
+ 3. **Chroma floor.** OKLCH C ≥ ~0.10 — below it a hue reads as gray and stops doing
35
+ identity work. *(validator)*
36
+ 4. **CVD separation.** Machado-2009 ΔE ≥ 12 target / ≥ 8 floor (floor legal only with
37
+ secondary encoding), under protanopia & deuteranopia. *Adjacent* pairs for
38
+ stacks/bars/lines (only neighbors touch — assignment never skips); **all pairs for
39
+ scatter, bubble, choropleth, and small-multiples**, where any two marks can sit side
40
+ by side — pass `--pairs all` there or a real collapse stays hidden. *(validator)*
41
+ 5. **Contrast vs surface.** ≥ 3:1 for marks; conditionally relaxed where values are
42
+ readable another way (visible labels or the table view). *(validator)*
43
+ 6. **Documented palette only.** Every slot is a hex from the instance file
44
+ (`palette.md` or its equivalent) — no eyeballed values. *(structural; for a
45
+ customer's ramps, snap to nearest — below)*
46
+
47
+ ## Run the checks — never eyeball them
48
+
49
+ ```
50
+ node scripts/validate_palette.js \
51
+ "#2a78d6,#1baf7a,#eda100,#008300,#4a3aa7,#e34948,#e87ba4,#eb6834" --mode light
52
+ ```
53
+
54
+ (`scripts/` is relative to this skill's base directory, shown at the top of the prompt.)
55
+
56
+ (or load it as `<script type="module">` in the chart's own page — it reads
57
+ `data-palette` off `<body>` and logs a `console.table` report)
58
+
59
+ Reports each computable check (2–5) with PASS / WARN / FAIL plus the worst CVD pair.
60
+ Exit 0 = no hard FAIL (WARN bands — floor-band CVD 8–12 and sub-3:1 contrast relief —
61
+ still exit 0 and require secondary encoding); exit 1 on any FAIL. Run once per mode
62
+ (`--mode dark --surface "#1a1a19"`), and add
63
+ `--pairs all` for scatter / bubble / map / small-multiples charts (where any two marks
64
+ can be neighbors — the default adjacent check would hide a collapse). For an
65
+ **ordinal** ramp pass `--ordinal` — it switches to the ramp checks (monotone L,
66
+ adjacent ΔL ≥ 0.06, light-end contrast ≥ 2.0:1, single hue) instead of the
67
+ categorical six.
68
+ A WARN on CVD (8–12 floor) is legal **only** if you also ship secondary encoding
69
+ (direct labels, gaps, or texture). A WARN on contrast is **not dismissable** — it
70
+ obligates a relief channel (visible direct labels or the table view); shipping the
71
+ sub-3:1 fill with neither is a fail.
72
+
73
+ **Scope — what the validator does and doesn't cover.** These six checks validate a
74
+ *categorical* palette (series identity). They do **not** judge a lone status/text
75
+ color or a sequential ramp. For a single status or text color, run a WCAG *text*-
76
+ contrast check (4.5:1 normal, 3:1 large) — `validate_palette.js` exports
77
+ `contrast(a, b)` for exactly this. For sequential/diverging, the check is lightness
78
+ monotonicity across the ramp, not adjacency CVD — running the categorical validator on
79
+ a sequential ramp **will FAIL by design** (it spans the band; steps sit close), which
80
+ is expected, not a real failure; don't "fix" a good ramp to satisfy it.
81
+
82
+ ## Snap-to-passing (any design system)
83
+
84
+ Given a customer's ramps and a desired order:
85
+ 1. For each slot, pick the step whose OKLCH L sits in the mode's band and C ≥ floor.
86
+ 2. Run the validator. For any adjacent pair below ΔE 12, nudge one slot ± a step
87
+ (hold its hue, move its lightness) and re-run.
88
+ 3. Repeat until the worst adjacent pair clears the floor. Function preserved, the
89
+ customer's hues kept.
90
+
91
+ ## Themes
92
+
93
+ The slot **order** is a separable, named choice — a *theme* — on the same hues and
94
+ the same six checks. Each design system names a default order and any alternates;
95
+ swapping themes tunes the mood without touching the method. A surface adopts one
96
+ theme and freezes it; never mix themes within a dashboard. (See `palette.md`.)
97
+
98
+ **Deriving an order when a system has no theme yet:** don't guess. Enumerate candidate
99
+ orderings of the system's hues, run the validator on each, and pick the one that
100
+ maximizes the *minimum adjacent* CVD ΔE. (Seeding from a known-good order by hue-family
101
+ analogy, then optimizing, is fine — this is exactly how the default in
102
+ `palette.md` was derived.)
103
+
104
+ ## Status is fixed
105
+
106
+ Status never follows the theme — it is a small fixed scale (good → warning → serious
107
+ → critical) with reserved meaning, on steps deliberately distinct from the categorical
108
+ slots so a status color never impersonates a series, and always paired with an
109
+ icon + label (on a light surface warning and serious sit below 3:1 by design —
110
+ the pairing is the mitigation). (Exact steps in `palette.md`.) The collision rule: when a series *means* good/bad (error rate, pass/fail) it wears
111
+ status tokens; when it's just "series 4" it wears categorical — never both in one chart.
112
+
113
+ ---
@@ -0,0 +1,39 @@
1
+ # Components — the pieces a chart is made of
2
+
3
+ A chart is built from these parts, assembled in plain HTML/SVG. Tier 0 is the
4
+ foundation everything mounts on; the System tier is what makes the method
5
+ portable (and is, itself, this skill).
6
+
7
+ ## Tier 0 — Foundations
8
+ - **Color roles** — categorical (8 × light/dark), sequential ramps, diverging pairs,
9
+ status (4), de-emphasis / "Other", grayscale chart furniture (axis/grid/label/surface).
10
+ Defined as CSS custom properties at the top of the HTML — see `palette.md`.
11
+ - **Texture fill** — the directional fill + 45°/135° rotations.
12
+ - **Chart container** — a `<figure>` (or card `<div>`) that owns responsive
13
+ sizing, title/caption, and the **table-view toggle** (the accessibility twin
14
+ of every chart). **Any fixed height includes the x-axis band** (plot height
15
+ + axis labels) so the card never gets a nested vertical scroll; prefer
16
+ letting the container grow with its content.
17
+ - **Legend** (toggle-to-isolate, texture-aware swatches) · **Tooltip** · **Axis** · **Data label**.
18
+
19
+ ## Tier 1 — The charts people ask for
20
+ - **Bar chart** — grouped + stacked, thin-bar default, horizontal + vertical.
21
+ - **Line chart** — multi-series, soft-fill area variant, accessibility markers.
22
+ - **Stat tile** — value + delta + optional sparkline (the figure contract).
23
+ - **Meter / progress track** — same-ramp tracks.
24
+
25
+ ## Tier 2 — Rounding out the kit
26
+ - **Area chart** (stacked, band-edge = line) · **Sparkline** · **Heatmap**
27
+ - **Scale legend** (sequential / diverging) · **Chart filters / time range** · **Empty state**
28
+
29
+ ## System tier — becomes the skill
30
+ - **Six-checks validator** — `scripts/validate_palette.js` (palette validation).
31
+ - **Theming engine** — snap a customer's ramps to passing values (color-formula.md).
32
+ - **Chart-type heuristic** — pick the form (choosing-a-form.md).
33
+ - **Table-view generator** — the WCAG-clean equivalent of any chart.
34
+
35
+ Notes: part-to-whole rides on the stacked bar chart; donut stays deprioritized.
36
+ Small multiples is a layout pattern over these, not a separate piece. Scatter
37
+ joins Tier 2 if scatter-heavy surfaces land.
38
+
39
+ ---
@@ -0,0 +1,60 @@
1
+ # Interaction — tooltips & filters
2
+
3
+ An HTML chart is interactive by default — the hover layer is part of the deliverable,
4
+ not an upgrade. Omitting it is the exception (a bare stat tile), never the default.
5
+ Design it with the same care as the static render.
6
+
7
+ ## Tooltips & hover
8
+
9
+ Tooltips **enhance, they never gate**: every value a tooltip shows is also reachable
10
+ without it, through direct labels or the table view. Same details on keyboard focus
11
+ as on hover.
12
+
13
+ - **The crosshair finds the X.** A vertical hairline tracks the pointer and snaps to
14
+ the nearest data position. Readers aim at a date, never at a 2px line.
15
+ - **On bars and cells, the mark is the hit target.** No crosshair — each bar, segment,
16
+ dot, or heat-cell carries its own `pointermove`/`focus` tooltip showing category and
17
+ value, and the hovered mark lifts (slight lighten or outline) so the reader sees it respond.
18
+ - **One tooltip, every series.** The readout lists every series at that X — the
19
+ pointer never has to land on a line or a fill to get a value.
20
+ - **Labels are untrusted data — use `textContent`.** Series and category names
21
+ often come from CSV headers, tool output, or API responses. Insert them into
22
+ tooltip/legend/table DOM with `textContent` or `createTextNode`, never via
23
+ `innerHTML` string concatenation.
24
+ - **Values lead, labels follow.** In the tooltip the value is the Strong,
25
+ high-contrast element and the series name is secondary — the legend's hierarchy
26
+ inverted, because here the reader has the series and wants the number.
27
+ - **Line keys, not boxes.** Tooltip rows key their series with a short stroke of the
28
+ series color; at tooltip density a filled box is data-weight ink doing a label's
29
+ job. (Legends still mirror the mark: rect for bars/areas, line for lines.)
30
+ - **The hit target is bigger than the mark.** A mark's hover/focus area includes its
31
+ 2px surface gap and then some — never only the painted pixels. An 8px scatter dot is a
32
+ pinpoint nobody hits reliably; give each point a transparent hit area of at least
33
+ **24px**, or — for dense scatter — a nearest-point / Voronoi layer so the pointer only
34
+ has to be *closest*, not dead-center. (The crosshair already does this for the X on
35
+ line and bar charts; scatter and bubble need the per-point version.)
36
+ - **A value pushed off its mark lives in the tooltip.** When a label won't fit inside a
37
+ small bar (see `marks-and-anatomy.md`), that bar's hit area carries the value on hover
38
+ and focus — the tooltip is its overflow home, and the table view keeps it reachable
39
+ without hovering at all.
40
+
41
+ ## Filters & time ranges
42
+
43
+ Every monitoring dashboard needs the same controls. These are **standard UI, not
44
+ chart marks** — build them with ordinary HTML form controls styled to match the
45
+ chart chrome. Dataviz only adds composition rules:
46
+
47
+ - **One row, above the charts.** Filters sit in a single left-aligned row above the
48
+ content they scope — never inside a chart card, never per-chart. If one chart needs
49
+ its own range, it's a different dashboard.
50
+ - **Date range first.** It's the filter every reader reaches for; presets (today,
51
+ last 7 / 30 / 90 days) before a custom range.
52
+ - **Filters scope everything below them.** Every chart, stat, and table re-renders
53
+ against the same slice, so the numbers always agree.
54
+ - **Refetch keeps the frame.** While data reloads, charts hold their previous render
55
+ at reduced opacity — no skeleton, no layout jump, no flash.
56
+
57
+ A good date picker lists presets as rows (nobody fights a calendar grid for "last 30
58
+ days"), marks selection with a 16px bold check, keeps hover a ghost wash so it never
59
+ competes with selection, and tucks the custom range behind a hairline in the footer.
60
+ (See `palette.md` for the reference spec.)
@@ -0,0 +1,97 @@
1
+ # Marks & anatomy
2
+
3
+ The quiet, considered look is a few fixed specs plus two pieces of negative space.
4
+ The data is the only thing allowed to be loud.
5
+
6
+ ## Mark specs (fixed across every chart)
7
+
8
+ | Mark | Spec |
9
+ |---|---|
10
+ | Bar / column | **≤ 24px thick** (cap it — never fill the slot; let the band's leftover be air); **4px rounded data-end, square at the baseline**; grows from a single baseline |
11
+ | Line | **2px**, round join/cap |
12
+ | Marker / end-dot | **≥ 8px** (r ≥ 4), filled with the series color |
13
+ | Area fill | the series hue at **~10% opacity** (a wash, never a saturated block) |
14
+ | Gridlines / axes | one-step-off-surface gray, **hairline (1px), solid** (never dashed), recessive |
15
+
16
+ ## The two spacers (white doing the separating)
17
+
18
+ - **Surface gap.** A **2px gap** in the surface color separates touching marks — every
19
+ segment of a stacked bar, and every adjacent (touching) bar, the same width. Keep it
20
+ one consistent width across a stack; neighbors one step apart read distinct because of
21
+ the gap, not a stroke drawn around them.
22
+ - **Surface ring.** Dots and end-markers carry a **2px ring in the surface color**,
23
+ so they stay legible where they cross a line or overlap each other. The ring is part
24
+ of the mark's hover/hit target, not just spacing — see `interaction.md` (small dots
25
+ are easy to under-size for hover).
26
+
27
+ Never draw a border around a mark to separate it. The gap and the ring are the
28
+ mechanism; a stroke adds data-weight ink that isn't data.
29
+
30
+ ## Labels & legend
31
+
32
+ A **legend is always present for two or more series** — the dependable identity
33
+ channel; never make the reader rely on color-matching alone. Direct labels then ride
34
+ the marks to *supplement* it. **A single series needs no legend box**: there is only
35
+ one color, so the chart's title or subtitle already says what is plotted. A box with
36
+ one swatch restates the title and costs space.
37
+
38
+ - **Label selectively — never a number on every point.** A value beside every dot or
39
+ segment is chaos and goes unread. Label the endpoint, the extreme, or the one series
40
+ the story is about; let the axis, the legend, and the tooltip/table carry the rest.
41
+ Direct labels work *because* they are sparing — flood the chart and they stop working.
42
+ - **Direct labels before gridlines; gridlines before a second axis.**
43
+ - **A label that won't fit doesn't get clipped — measure first.** Only place a label
44
+ *inside* a bar or stacked segment when the rendered text fits with comfortable
45
+ padding on both sides. If it doesn't fit: for a whole bar/column, move the label
46
+ outside the bar end (or to the tooltip if there's no room outside either); for an
47
+ *interior* stacked segment (which has no free end),
48
+ skip the inline label and let the legend + tooltip carry it. Either way the value
49
+ stays in the table view, so nothing is gated. Never use `overflow: hidden` on the
50
+ segment to "solve" it — that crops the first/last characters and is worse than no
51
+ label. Text never overflows or is clipped by its own mark.
52
+ - Bars → value at the tip. Columns → value on the cap. Lines → value at the end.
53
+ - Y-axis ticks: round to clean numbers (0 / 1,000 / 2,000), thousands-comma'd; they
54
+ carry the values you didn't directly label, so keep them unless every value is labeled.
55
+ - **Text never wears the data color.** Marks — bars, lines, dots, area fills — carry
56
+ the series color; labels, values, legends, and axis text use **text tokens**
57
+ (primary / secondary / muted). A light categorical hue (yellow, aqua) is illegible
58
+ as text on the surface. Identity comes from the colored mark *beside* the text — a
59
+ dot, a short line-key, a swatch — never from coloring the text itself. A label set
60
+ *inside* a colored fill (a stacked segment, a map tile) is the one exception: pick
61
+ white or ink by the fill's luminance so it always clears contrast.
62
+ - **When end-labels collide, don't stack them.** Direct end-labels work when series
63
+ separate at the right edge. When lines converge, nudging labels apart vertically
64
+ detaches them from their lines and reads as noise — instead use **leader lines**
65
+ (a thin connector from label to line-end), facet into **small multiples**, or fall
66
+ back to the legend + tooltip. Past ~4 converging series, small multiples is usually right.
67
+
68
+ ## Figures — when the form is a number
69
+
70
+ - **Stat tile** contract: `label` (sentence case, no trailing colon) · `value` (Sans
71
+ semibold, auto-compact: 1,284 / 12.9K / $4.2M) · `delta` (optional; signed,
72
+ vs a named period; color = direction × whether up is good) · `trend` (optional;
73
+ 12-point sparkline in the de-emphasis hue, current period in the accent).
74
+ - **Meter:** the fill carries severity (accent → warning → danger); the unfilled
75
+ track is a **lighter step of the same ramp** (blue-on-blue, etc.) so state reads
76
+ across the whole bar.
77
+ - **Hero figure.** The single number a dashboard leads with, ≥48px, in the same
78
+ sans as everything else (never a display or serif face — it reads as off-brand
79
+ decoration). Exactly one per view.
80
+ - **Proportional figures for big numbers; tabular only in columns.** A large
81
+ standalone value (hero figure, stat-tile value) uses the font's default
82
+ proportional figures — `tabular-nums` gives every digit the width of a `0`, so a
83
+ number like `121` looks loose at display sizes. Reserve
84
+ `font-variant-numeric: tabular-nums` for columns of numbers that must align
85
+ vertically (table rows, axis ticks).
86
+
87
+ ## Texture — the backup channel (opt-in)
88
+
89
+ Where hue fails — full-severity CVD, grayscale print, `forced-colors` — texture
90
+ carries identity. One directional hand-drawn fill, used at **45° and its 135° mirror
91
+ only** (never horizontal/vertical — those read as gridlines/bars). Inked tone-on-tone
92
+ (a step from the fill's own ramp), equal loudness across slots. On value scales the
93
+ texture is *ordered* (rotation steps with magnitude; arm angle carries the diverging
94
+ sign) so it never misstates the value. Triggered by an accessibility setting, print,
95
+ or `forced-colors` — never on by default. (See `palette.md`.)
96
+
97
+ ---
@@ -0,0 +1,149 @@
1
+ # Reference palette
2
+
3
+ This is the **reference instance** of the data-viz method: every parameter the
4
+ method needs, filled in with a validated default palette. The rest of the skill
5
+ is system-agnostic — **to target your brand, substitute this file's values** and
6
+ re-run the validator. Nothing else changes.
7
+
8
+ ## How to use these values
9
+
10
+ Everything below is plain hex. In an HTML chart, **define the slots you use as
11
+ CSS custom properties in a local `<style>` block** at the top of the file, then
12
+ reference them by role throughout — so the light/dark values swap in one place,
13
+ and the chart body is written against roles rather than raw hex:
14
+
15
+ ```css
16
+ .viz-root {
17
+ --surface-1: #fcfcfb; /* chart surface */
18
+ --text-primary: #0b0b0b;
19
+ --text-secondary: #52514e;
20
+ --series-1: #2a78d6; /* categorical slot 1 */
21
+ /* …only the roles this chart uses */
22
+ }
23
+ @media (prefers-color-scheme: dark) {
24
+ .viz-root {
25
+ --surface-1: #1a1a19;
26
+ --text-primary: #ffffff;
27
+ --text-secondary: #c3c2b7;
28
+ --series-1: #3987e5;
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Categorical palette
34
+
35
+ Both modes are selected. The dark column is the same eight hues stepped for the
36
+ dark surface, not a separate palette:
37
+
38
+ | Slot | Hue | Light | Dark |
39
+ |------|-----|-------|------|
40
+ | 1 | blue | `#2a78d6` | `#3987e5` |
41
+ | 2 | aqua | `#1baf7a` | `#199e70` |
42
+ | 3 | yellow | `#eda100` | `#c98500` |
43
+ | 4 | green | `#008300` | `#008300` |
44
+ | 5 | violet | `#4a3aa7` | `#9085e9` |
45
+ | 6 | red | `#e34948` | `#e66767` |
46
+ | 7 | magenta | `#e87ba4` | `#d55181` |
47
+ | 8 | orange | `#eb6834` | `#d95926` |
48
+
49
+ Light-mode worst adjacent CVD ΔE is 24.2 — well clear of the ≥12 target. Three
50
+ light-mode slots (aqua, yellow, magenta) sit below 3:1 contrast on the light
51
+ surface: the **relief rule** applies (ship visible direct labels or the table
52
+ view). The dark steps were chosen for the dark band (OKLCH L ≈ 0.48–0.67, ≥ 3:1
53
+ on the dark surface) and validated as a set — worst adjacent ΔE 10.3, the floor
54
+ band, so four-plus series lean on direct labels or texture in dark mode too.
55
+
56
+ The slot **ordering** is the CVD-safety mechanism, not cosmetic — it was derived
57
+ by enumerating orderings and picking the one that maximizes the minimum adjacent
58
+ ΔE (see `color-formula.md` § Themes). When you swap in your brand's hues, do the
59
+ same: run the validator on candidate orderings and keep the best.
60
+
61
+ ## Sequential hue
62
+
63
+ Default single hue: **blue**, light→dark. When two sequential contexts appear at
64
+ once, the second takes the next categorical slot's hue (aqua), each as its own
65
+ one-hue ramp.
66
+
67
+ | step | hex | step | hex | step | hex | step | hex |
68
+ |---|---|---|---|---|---|---|---|
69
+ | 100 | `#cde2fb` | 250 | `#86b6ef` | 400 | `#3987e5` | 550 | `#1c5cab` |
70
+ | 150 | `#b7d3f6` | 300 | `#6da7ec` | 450 | `#2a78d6` | 600 | `#184f95` |
71
+ | 200 | `#9ec5f4` | 350 | `#5598e7` | 500 | `#256abf` | 650 | `#104281` |
72
+ | | | | | | | 700 | `#0d366b` |
73
+
74
+ The full 100→700 range is for **sequential** encoding (continuous magnitude —
75
+ heatmaps, choropleths) where the lightest step means "near zero" and is allowed
76
+ to recede toward the surface. For an **ordinal** ramp (discrete ordered marks —
77
+ funnel stages, tiers — validated with `--ordinal`), the step nearest the surface
78
+ must still clear 2:1: on light, start no lighter than **step 250** (`#86b6ef`,
79
+ 2.06:1); on dark, go no darker than **step 600** (`#184f95`, 2.15:1).
80
+
81
+ ## Diverging pair
82
+
83
+ **blue ↔ red** — warm/cool poles that read as opposite. Neutral midpoint is gray
84
+ (light `#f0efec`, dark `#383835`). Equal step count per arm. (blue↔aqua was
85
+ rejected — both cool, the midpoint doesn't read as "nothing".)
86
+
87
+ ## Status palette (fixed — never themed)
88
+
89
+ | role | hex | light-surface contrast | dark-surface contrast |
90
+ |---|---|---|---|
91
+ | good | `#0ca30c` | 3.27 | 5.19 |
92
+ | warning | `#fab219` | 1.79 | 9.49 |
93
+ | serious | `#ec835a` | 2.57 | 6.60 |
94
+ | critical | `#d03b3b` | 4.68 | 3.62 |
95
+
96
+ Dark: same four steps — all clear 3:1 on the dark surface (`#1a1a19`) and remain
97
+ distinct from the dark categorical slots. On the light surface, warning and
98
+ serious are sub-3:1 by design; the **icon + label** pairing is the mitigation, so
99
+ a status color never carries meaning alone. These steps are deliberately distinct
100
+ from the categorical slots so a status color never impersonates a series.
101
+
102
+ ## Texture fill (the accessibility channel)
103
+
104
+ One hand-drawn **"Lines"** fill, used at **45° and its 135° mirror only**. Inked
105
+ tone-on-tone (a darker step of the fill's own ramp). On value scales it is
106
+ *ordered* (rotation steps with magnitude; arm angle carries the diverging sign).
107
+ Triggered by the accessibility setting, print, or `forced-colors` — never
108
+ decorative, never on by default.
109
+
110
+ ## Surfaces (for the validator)
111
+
112
+ - Light chart surface: `#fcfcfb`
113
+ - Dark chart surface: `#1a1a19`
114
+
115
+ These are the validator's built-in defaults. **When you swap in your own
116
+ palette, re-run against your own surfaces:**
117
+ `--surface <your-light> --mode light` and `--surface <your-dark> --mode dark` —
118
+ contrast and band results are only meaningful against the surface the chart
119
+ actually renders on.
120
+
121
+ ## Chart chrome & ink
122
+
123
+ | Role | Light | Dark |
124
+ |---|---|---|
125
+ | Chart surface | `#fcfcfb` | `#1a1a19` |
126
+ | Page plane | `#f9f9f7` | `#0d0d0d` |
127
+ | Primary ink | `#0b0b0b` | `#ffffff` |
128
+ | Secondary ink | `#52514e` | `#c3c2b7` |
129
+ | Muted (axis/labels) | `#898781` | `#898781` |
130
+ | Gridline (hairline) | `#e1e0d9` | `#2c2c2a` |
131
+ | Baseline / axis | `#c3c2b7` | `#383835` |
132
+ | Delta ↑ good (success text) | `#006300` | `#0ca30c` |
133
+ | Border (hairline ring) | `rgba(11,11,11,0.10)` | `rgba(255,255,255,0.10)` |
134
+
135
+ ## Filter controls
136
+
137
+ Filters are standard UI, not chart components — the chart layer only adds the
138
+ composition rules in `interaction.md`. A date-range control is a list of preset
139
+ rows (today, last 7/30/90 days, month-to-date) with selection marked by a 16px
140
+ bold check, hover as a ghost wash, and custom range behind a hairline in the
141
+ footer. Dimension filters are a standard combobox.
142
+
143
+ ## Typeface & figures
144
+
145
+ Everything — including the hero figure — stays in the system sans: `system-ui,
146
+ -apple-system, "Segoe UI", sans-serif`. No display or serif face anywhere. Large
147
+ standalone numbers (hero figure, stat-tile values) use the default proportional
148
+ figures; reserve `font-variant-numeric: tabular-nums` for columns that must align
149
+ vertically (table rows, axis ticks). Substitute your brand's UI sans here.