Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the self-hosted JetBrains Mono fonts, and interactive mockups of every screen across three iterations. Iteration 3 is final: phone, tablet and desktop tiers, device preview, and "empty spool weight" terminology. The build scripts regenerate the screens byte-identically from source. Implementation is tracked in Maestro feature UI Refresh - V3.0 (224b72a1-8d84-4970-97ea-5dafa4d8ea3b). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
17 KiB
CubeCraftLabs Design System v3 (proposal): Soft UI
Status: proposal, not adopted. Nothing in the Extrudex repository has changed.
Base: Design System v2.0 as served at http://pages.ccl/design-v2.0/, verified
on 2026-09-18 to match web/src/theme.css on dev (321ce0d) token for token.
Tokens: design-system-v3.css · Fonts: fonts/
(JetBrains Mono 400/500/700/800 woff2 from @fontsource/jetbrains-mono 5.3.0,
the same package the app imports; OFL license included).
1. Principles
- The v2.0 palette stays. Charcoal, violet accent, green and red are unchanged. Every v2.0 token name still exists and still resolves. New colors are tints of v2.0 hues, except for two new status hues (warning, info) that v2.0 lacked.
- Shadow is decoration, never structure. Every button, input, select, toggle and checkbox has a border at 3:1 or better against its surface (WCAG 1.4.11). Turn the shadows off and every control is still findable.
- No state relies on shadow direction alone. See the state matrix in §4.
- Offline-identical. One self-hosted font. No CDN, no remote asset, no
@import. The mockups embed the font as base64. - Numbers stay square. Tables, code and numeric columns keep
--radius-none. Soft corners belong to containers and controls, not data.
2. Two v2.0 rules this proposal overturns (deliberately)
| v2.0 rule | v3 | Why |
|---|---|---|
| "Radius: the brand is square. 0 is the rule." | A 0–26 px radius scale | Neumorphism reads as extruded material. A soft raised shape on a square corner looks like a rendering error, not a style. --radius-none stays for tables, code and grid lines. |
| "Shadows: flat brand. Only a faint lift where truly needed." | A four-level raised/inset shadow system | Soft UI is the shadow system. The accessibility cost of that is paid by §1.2 and §4. |
DECISIONS.MD says "do not edit a token to suit a component". v3 doesn't edit v2.0 tokens: it adds new ones and remaps the alias layer. The two changes above are brand-level decisions for you to accept or reject, and they're listed again in SUMMARY.md.
3. Every token that changed or was added compared to v2.0
Unchanged (kept verbatim): the whole --ccl-* base scale and its aliases,
--font-*, --fw-*, --fs-*, --lh-*, --tracking-*, --space-*,
--ease-out, --dur-fast, --dur-base, --scrollbar-width, --radius-none,
--shadow-none, --border-width.
3a. Base scale: added
| Token | Value | Reason |
|---|---|---|
--ccl-gold-light |
#a99af0 |
v2.0's accent #8f7ae0 is 3.99:1 as text on charcoal (fails 4.5:1), and v2.0's own ink-on-accent button text is 4.32:1 (fails). A lighter step of the same hue passes both: 5.67:1 as text and 6.14:1 under ink. |
--ccl-violet-deep |
#5b43b8 |
The accent for the light theme. #8f7ae0 on a light surface is about 2.6:1. |
--ccl-charcoal-inset |
#232327 |
Neumorphic wells (inputs, tracks) need a step between page #2c2c30 and code #1b1b1e. Code black inside a well looked like a hole. |
--ccl-amber |
#e8c170 |
v2.0 has no warning color. "Running low", "tare estimated" and "duration is an upper bound" all need a caution that isn't an error. |
--ccl-blue |
#8fc3f0 |
An informational/provenance hue for "sliced", "recalled" and "on feature branch" labels, so provenance doesn't borrow success or error meanings. |
--ccl-mist / -2 / -3 |
#e4e2ea / #f1f0f5 / #d9d6e1 |
The light theme's page, high and inset surfaces. They carry charcoal's slight violet cast, inverted, so both themes are one family. Soft UI can't use pure white: the highlight half of the shadow needs a lighter color than the surface. |
3b. Semantic tokens: changed
| Token | v2.0 | v3 dark | v3 light | Reason |
|---|---|---|---|---|
--surface-raised |
#36363a |
#2c2c30 (= page) |
#e4e2ea |
In soft UI a raised surface is the same material as the page. The lift comes from the shadow pair. A lighter fill plus a shadow looks like a card lying on top. |
--shadow-raised |
0 1px 0 0 #3d3d44 |
var(--nm-raised-2) |
same | The v2.0 name now points to the level-2 soft shadow, so existing rules pick it up. |
--accent-fill (new) |
(none) | #a99af0 |
#5b43b8 |
Primary button fill. The text on it passes AA, which v2.0's did not (4.32:1). |
--text-on-accent |
#2a2431 |
#2a2431 |
#ffffff |
Unchanged in dark. White in light, because the light accent fill is dark. |
--focus-ring |
#8f7ae0 |
#a99af0 |
#5b43b8 |
v2.0's ring was 3.45:1 on a raised surface. v3's is at least 5.6:1 everywhere. |
--radius-sm / --radius-md |
0 | 8 px / 12 px | same | See §2. |
--border-hairline (light) |
(none) | #3d3d44 |
#cfccd8 |
The light theme needs its own value. Hairlines are decorative and not relied on. |
--scrollbar-thumb |
#4e4e57 |
var(--border-strong) |
#b9b5c6 |
Now follows the theme. |
3c. Semantic tokens: added
| Token | Dark | Light | Reason |
|---|---|---|---|
--surface-high |
#36363a |
#f1f0f5 |
Dialogs and menus. Reuses v2.0's charcoal-2 in dark. |
--surface-inset |
#232327 |
#d9d6e1 |
Inputs, wells and progress tracks. |
--text-accent |
#a99af0 |
#5b43b8 |
Links and accent text that pass 4.5:1. |
--accent-fill-press |
#9785e6 |
#4d37a3 |
The pressed primary button is a different fill, not just an inset shadow. |
--accent-wash |
10% of #a99af0 |
10% of #5b43b8 |
The pressed/selected background for secondary controls. |
--status-warning / --status-info |
amber / blue | #7c5100 / #1d5e94 |
Explained in §3a. |
--status-success / --status-error (light) |
(v2.0 hues) | #2c6619 / #a8202f |
The v2.0 hues are too light for a light surface. |
--wash-success/error/warning/info |
10% alpha | 10% alpha | Chip backgrounds. 16% dropped chip text below 4.5:1, so it's 10%. |
--border-control |
#85858f |
#736e86 |
The 1.4.11 token. v2.0's strongest border #4e4e57 is 1.69:1 on charcoal, so it can't identify a control. v3's is 3.29–4.30:1 on every surface. |
--border-control-width / --border-error-width |
1 px / 2 px | same | An error changes the border's width as well as its color. |
--focus-width / --focus-offset |
2 px / 3 px | same | An offset outline, visually separate from the shadow. |
--dur-press |
60 ms | same | The press responds faster than hover, so the push feels physical. |
3d. Neumorphism tokens: added
| Token | Value | Purpose |
|---|---|---|
--radius-xs … --radius-pill |
4 / 8 / 12 / 18 / 26 / 999 px | Chips / inputs / buttons / cards / dialogs / tracks and badges |
--nm-d-1…4, --nm-b-1…4 |
2/5, 4/10, 8/18, 14/30 px | Shadow distance and blur per intensity level |
--nm-shadow-dark |
dark rgba(0,0,0,.62) · light rgba(88,80,120,.30) |
Bottom-right half of the raised pair |
--nm-shadow-light |
dark rgba(255,255,255,.055) · light rgba(255,255,255,.95) |
Top-left half of the raised pair |
--nm-inset-dark / --nm-inset-light |
dark .55 / .045 · light .28 / .9 |
Inset (pressed/well) pair |
--nm-rim |
dark inset 0 1px 0 rgba(255,255,255,.06) |
Top-edge rim light (the dark-mode fix, §5) |
--nm-raised-gradient |
dark #313136 → #29292d · light #eceaf1 → #dcd9e3 |
A 145° surface gradient that sells the convex shape |
--nm-raised-1…4 |
composed | Level 1 chips · 2 buttons/rows · 3 cards · 4 dialogs |
--nm-inset-1…2 |
composed | Level 1 inputs/pressed buttons · 2 wells/panels |
--nm-flat |
none |
Disabled controls. They lose their shadow entirely. |
4. State matrix
What changes in each state. The "besides shadow" column is what makes each state identifiable without the shadow.
| State | Shadow | Besides shadow (the part that must be visible) |
|---|---|---|
| Rest | raised-2 | 1 px solid --border-control (3:1+) |
| Hover | raised-2 | Border becomes --text-accent |
| Pressed / selected | inset-1 | Background --accent-wash, accent border, ✓ glyph on toggles; primary uses the --accent-fill-press fill |
| Focused | unchanged | 2 px solid outline, 3 px offset, --focus-ring. Combines with any other state. |
| Disabled | none | Dashed border, transparent fill, secondary text, not-allowed cursor |
| Error | inset-1 (inputs) | 2 px border in --status-error, ⚠ icon, and a message under the field |
Because each state uses a different channel (outline, fill and glyph, dash, width and icon), any two states stay distinguishable in grayscale.
5. Dark-mode neumorphism: the problem and the fix
The problem. Soft UI draws the same surface with a lighter shadow top-left
and a darker one bottom-right. On #2c2c30, the conventional recipe (darker and
lighter charcoal, for example #1b1b1e and #3d3d44) gives about 1.3:1 each
way. The shape vanishes, or turns into a muddy halo.
The fix, as four rules in the tokens:
- The dark half is true black at high alpha (
rgba(0,0,0,.62)), not a darker gray. Alpha black darkens whatever it lands on, so it stays visible even beside the v2.0 raised gray. - The light half is white at about 5% alpha. At that level it reads as a sheen. Any brighter and dark soft UI starts to glow.
- A 1 px rim light (
inset 0 1px 0 rgba(255,255,255,.06)) on every raised surface. The rim is sharp where the shadow is soft, so it keeps the top edge visible even when the drop shadow merges into the page. - The page stays at v2.0's
#2c2c30, not near-black. Soft UI needs room below the surface for the dark half, and#1b1b1ewould leave none. That is also why wells use the new in-between#232327.
The fifth rule is the accessibility floor, and it's why none of this is fragile: every control has a 3:1 border anyway. If a monitor crushes the blacks, or someone forces high contrast, the UI loses depth but keeps its structure.
6. Alternative palettes (not the main proposal)
Both would reuse every structural and neumorphism token. Only the base scale would change.
| A: Warm graphite | B: Slate and teal | |
|---|---|---|
| Page (dark / light) | #2e2b29 / #e9e5e0 |
#27303a / #e1e7ec |
| Accent (dark text / light text) | #e0a36b / #8a4b12 |
#5fd0c0 / #0f6b61 |
| Mood | Workshop, wood and brass. It suits a print farm but moves away from the CCL violet. | Cooler and more technical. It reads as "instrument panel" and gives the swatches more room. |
| Risk | Amber becomes the accent, so warning needs a new hue | Teal is close to success green, so success would need re-tuning |
The main proposal keeps v2.0 violet. Both alternatives change the brand color.
7. Contrast checks
The script used the WCAG 2.1 relative luminance formula. The v2.0 rows show what v3 fixes.
7a. v2.0 as it stands today (dark only)
| Pair | Ratio | Needs | Result |
|---|---|---|---|
Text #f4f4f6 on page #2c2c30 |
12.66:1 | 4.5 | Pass |
Muted #b2b2ba on page |
6.60:1 | 4.5 | Pass |
Ink #2a2431 on accent #8f7ae0 (primary button) |
4.32:1 | 4.5 | FAIL |
Accent #8f7ae0 as text on page |
3.99:1 | 4.5 | FAIL |
Strongest border #4e4e57 vs page (control edge) |
1.69:1 | 3 | FAIL |
Focus ring #8f7ae0 on raised #36363a |
3.45:1 | 3 | Pass (narrowly) |
7b. v3, both themes
All 54 checks pass.
| Theme | Pair | Foreground | Background | Ratio | Needs | Result |
|---|---|---|---|---|---|---|
| Dark | Body text | #f4f4f6 |
#2c2c30 |
12.66:1 | 4.5:1 | Pass |
| Dark | Body text on inset (inputs) | #f4f4f6 |
#232327 |
14.25:1 | 4.5:1 | Pass |
| Dark | Body text on high surface (dialog) | #f4f4f6 |
#36363a |
10.95:1 | 4.5:1 | Pass |
| Dark | Secondary text | #b2b2ba |
#2c2c30 |
6.60:1 | 4.5:1 | Pass |
| Dark | Secondary text on inset | #b2b2ba |
#232327 |
7.43:1 | 4.5:1 | Pass |
| Dark | Secondary text on high | #b2b2ba |
#36363a |
5.71:1 | 4.5:1 | Pass |
| Dark | Accent text / links | #a99af0 |
#2c2c30 |
5.67:1 | 4.5:1 | Pass |
| Dark | Accent text on high | #a99af0 |
#36363a |
4.91:1 | 4.5:1 | Pass |
| Dark | Text on primary button | #2a2431 |
#a99af0 |
6.14:1 | 4.5:1 | Pass |
| Dark | Success text | #a9d88b |
#2c2c30 |
8.53:1 | 4.5:1 | Pass |
| Dark | Error text | #f08b93 |
#2c2c30 |
5.84:1 | 4.5:1 | Pass |
| Dark | Error text on high | #f08b93 |
#36363a |
5.05:1 | 4.5:1 | Pass |
| Dark | Warning text | #e8c170 |
#2c2c30 |
8.14:1 | 4.5:1 | Pass |
| Dark | Info text | #8fc3f0 |
#2c2c30 |
7.44:1 | 4.5:1 | Pass |
| Dark | Control border vs page (1.4.11) | #85858f |
#2c2c30 |
3.81:1 | 3:1 | Pass |
| Dark | Control border vs inset | #85858f |
#232327 |
4.29:1 | 3:1 | Pass |
| Dark | Control border vs high | #85858f |
#36363a |
3.29:1 | 3:1 | Pass |
| Dark | Focus ring vs page | #a99af0 |
#2c2c30 |
5.67:1 | 3:1 | Pass |
| Dark | Focus ring vs high | #a99af0 |
#36363a |
4.91:1 | 3:1 | Pass |
| Dark | Primary button fill vs page | #a99af0 |
#2c2c30 |
5.67:1 | 3:1 | Pass |
| Dark | Error border vs inset | #f08b93 |
#232327 |
6.58:1 | 3:1 | Pass |
| Dark | Warning border vs page | #e8c170 |
#2c2c30 |
8.14:1 | 3:1 | Pass |
| Dark | ok chip text on 10% wash | #a9d88b |
#383d39 |
6.80:1 | 4.5:1 | Pass |
| Dark | err chip text on 10% wash | #f08b93 |
#40363a |
4.88:1 | 4.5:1 | Pass |
| Dark | warn chip text on 10% wash | #e8c170 |
#3f3b36 |
6.50:1 | 4.5:1 | Pass |
| Dark | info chip text on 10% wash | #8fc3f0 |
#363b43 |
6.03:1 | 4.5:1 | Pass |
| Dark | acc chip text on 10% wash | #a99af0 |
#383743 |
4.77:1 | 4.5:1 | Pass |
| Light | Body text | #2a2431 |
#e4e2ea |
11.73:1 | 4.5:1 | Pass |
| Light | Body text on inset (inputs) | #2a2431 |
#d9d6e1 |
10.51:1 | 4.5:1 | Pass |
| Light | Body text on high surface (dialog) | #2a2431 |
#f1f0f5 |
13.28:1 | 4.5:1 | Pass |
| Light | Secondary text | #55505f |
#e4e2ea |
6.06:1 | 4.5:1 | Pass |
| Light | Secondary text on inset | #55505f |
#d9d6e1 |
5.43:1 | 4.5:1 | Pass |
| Light | Secondary text on high | #55505f |
#f1f0f5 |
6.86:1 | 4.5:1 | Pass |
| Light | Accent text / links | #5b43b8 |
#e4e2ea |
5.57:1 | 4.5:1 | Pass |
| Light | Accent text on high | #5b43b8 |
#f1f0f5 |
6.30:1 | 4.5:1 | Pass |
| Light | Text on primary button | #ffffff |
#5b43b8 |
7.14:1 | 4.5:1 | Pass |
| Light | Success text | #2c6619 |
#e4e2ea |
5.42:1 | 4.5:1 | Pass |
| Light | Error text | #a8202f |
#e4e2ea |
5.62:1 | 4.5:1 | Pass |
| Light | Error text on high | #a8202f |
#f1f0f5 |
6.36:1 | 4.5:1 | Pass |
| Light | Warning text | #7c5100 |
#e4e2ea |
5.39:1 | 4.5:1 | Pass |
| Light | Info text | #1d5e94 |
#e4e2ea |
5.31:1 | 4.5:1 | Pass |
| Light | Control border vs page (1.4.11) | #736e86 |
#e4e2ea |
3.80:1 | 3:1 | Pass |
| Light | Control border vs inset | #736e86 |
#d9d6e1 |
3.41:1 | 3:1 | Pass |
| Light | Control border vs high | #736e86 |
#f1f0f5 |
4.30:1 | 3:1 | Pass |
| Light | Focus ring vs page | #5b43b8 |
#e4e2ea |
5.57:1 | 3:1 | Pass |
| Light | Focus ring vs high | #5b43b8 |
#f1f0f5 |
6.30:1 | 3:1 | Pass |
| Light | Primary button fill vs page | #5b43b8 |
#e4e2ea |
5.57:1 | 3:1 | Pass |
| Light | Error border vs inset | #a8202f |
#d9d6e1 |
5.03:1 | 3:1 | Pass |
| Light | Warning border vs page | #7c5100 |
#e4e2ea |
5.39:1 | 3:1 | Pass |
| Light | ok chip text on 10% wash | #2c6619 |
#d2d6d5 |
4.74:1 | 4.5:1 | Pass |
| Light | err chip text on 10% wash | #a8202f |
#decfd7 |
4.81:1 | 4.5:1 | Pass |
| Light | warn chip text on 10% wash | #7c5100 |
#dad4d3 |
4.73:1 | 4.5:1 | Pass |
| Light | info chip text on 10% wash | #1d5e94 |
#d0d5e1 |
4.64:1 | 4.5:1 | Pass |
| Light | acc chip text on 10% wash | #5b43b8 |
#d6d2e5 |
4.83:1 | 4.5:1 | Pass |
Additional pairs checked:
| Theme | Pair | Ratio | Needs | Result |
|---|---|---|---|---|
| Dark | Ink on primary fill #a99af0 |
6.14:1 | 4.5 | Pass |
| Dark | Ink on pressed primary #9785e6 |
4.88:1 | 4.5 | Pass |
| Light | White on primary fill #5b43b8 |
7.14:1 | 4.5 | Pass |
| Light | White on pressed primary #4d37a3 |
8.78:1 | 4.5 | Pass |
| Dark | Progress fill #a99af0 vs track #232327 |
6.39:1 | 3 | Pass |
| Light | Progress fill #5b43b8 vs track #d9d6e1 |
4.99:1 | 3 | Pass |
| Dark | Low-stock fill #f08b93 vs track |
6.58:1 | 3 | Pass |
| Light | Low-stock fill #a8202f vs track |
5.03:1 | 3 | Pass |
| Dark | Accent text on pressed wash (≈#3a3848) |
4.66:1 | 4.5 | Pass |
Not covered by the table: filament swatch colors are user data, not tokens.
Each swatch gets a --border-control ring, so a charcoal-black or mist-white
filament still shows an edge on either theme, and the color name is always
printed beside it.