@ids-group-ltd/ids-design-system 0.2.2 → 0.4.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.
@@ -1,19 +1,14 @@
1
1
  // =========================================================================
2
- // Default palette — Tier 1 concrete colour primitives.
2
+ // Base palette — Tier 1 theme-independent primitives.
3
3
  //
4
- // This file DEFINES the colours: raw ramps (--cool-gray-*, --brand-gray-*,
5
- // --blue-*, --red-*, --green-*, --yellow-*, --sky-*, --orange-*) + the HSL
6
- // channels (--blue-h/s/l, --red-h/s/l, --shadow-tint-h/s/l) they compose from.
7
- // Tokens are named by the COLOUR itself — the palette does not know what a
8
- // colour is "for". The --neutral-* FAMILY SLOT (which gray ramp is active)
9
- // is semantic, so it lives in the theme (_theme.scss), not here.
4
+ // Emits at :root. Colour ramps and channels that are identical in every
5
+ // theme: hue ramps (--cool-gray-*, --brand-gray-*, --blue-*, --red-*, ),
6
+ // HSL channels (--blue-h/s/l, --red-h/s/l), shadow substrate h/s (NOT -l
7
+ // that is per-theme), static anchors, and font families.
10
8
  //
11
- // The companion theme (themes/default/_theme.scss) SETS these onto semantic
12
- // roles (--primary --blue-600, --error → --red-500, …). Components consume
13
- // only those roles, never this palette directly.
14
- //
15
- // A sibling palette (e.g. _palette-dark.scss) ships different ramp values; pair
16
- // it with a matching theme file. Default = "Blue + Slate baseline".
9
+ // The --neutral-* FAMILY SLOT (which gray ramp is active) is per-theme and
10
+ // lives in each theme's _palette.scss mixin. Components consume only semantic
11
+ // roles; they never reference this file directly.
17
12
  // =========================================================================
18
13
 
19
14
  :root {
@@ -22,7 +17,7 @@
22
17
  // 210 → 227 toward the brand hue (229) as it darkens; chroma follows a
23
18
  // designed U-curve (S 40% → 12% → 37%). Appearance name, Carbon-style.
24
19
  // -----------------------------------------------------------------------
25
- --cool-gray-0: #FFFFFF;
20
+ --cool-gray-0: #FFF;
26
21
  --cool-gray-50: #F8FAFC;
27
22
  --cool-gray-100: #F1F4F9;
28
23
  --cool-gray-150: #E8ECF2;
@@ -44,19 +39,19 @@
44
39
  // cool-gray to within rounding.
45
40
  // -----------------------------------------------------------------------
46
41
  --neutral-h: var(--primary-h, var(--blue-h));
47
- --brand-gray-0: #FFFFFF;
48
- --brand-gray-50: hsl(calc(var(--neutral-h) - 19), 40.0%, 98.0%);
49
- --brand-gray-100: hsl(calc(var(--neutral-h) - 12), 40.0%, 96.1%);
50
- --brand-gray-150: hsl(calc(var(--neutral-h) - 13), 27.8%, 92.9%);
51
- --brand-gray-200: hsl(calc(var(--neutral-h) - 10), 25.0%, 89.0%);
52
- --brand-gray-300: hsl(calc(var(--neutral-h) - 10), 19.6%, 80.0%);
53
- --brand-gray-400: hsl(calc(var(--neutral-h) - 8), 14.3%, 65.7%);
54
- --brand-gray-500: hsl(calc(var(--neutral-h) - 6), 11.9%, 47.6%);
55
- --brand-gray-600: hsl(calc(var(--neutral-h) - 5), 14.9%, 35.5%);
56
- --brand-gray-700: hsl(calc(var(--neutral-h) - 4), 18.5%, 25.5%);
57
- --brand-gray-800: hsl(calc(var(--neutral-h) - 3), 23.1%, 17.8%);
58
- --brand-gray-900: hsl(calc(var(--neutral-h) - 2), 29.0%, 12.2%);
59
- --brand-gray-950: hsl(calc(var(--neutral-h) - 3), 37.1%, 6.9%);
42
+ --brand-gray-0: #FFF;
43
+ --brand-gray-50: hsl(calc(var(--neutral-h) - 19) 40.0% 98.0%);
44
+ --brand-gray-100: hsl(calc(var(--neutral-h) - 12) 40.0% 96.1%);
45
+ --brand-gray-150: hsl(calc(var(--neutral-h) - 13) 27.8% 92.9%);
46
+ --brand-gray-200: hsl(calc(var(--neutral-h) - 10) 25.0% 89.0%);
47
+ --brand-gray-300: hsl(calc(var(--neutral-h) - 10) 19.6% 80.0%);
48
+ --brand-gray-400: hsl(calc(var(--neutral-h) - 8) 14.3% 65.7%);
49
+ --brand-gray-500: hsl(calc(var(--neutral-h) - 6) 11.9% 47.6%);
50
+ --brand-gray-600: hsl(calc(var(--neutral-h) - 5) 14.9% 35.5%);
51
+ --brand-gray-700: hsl(calc(var(--neutral-h) - 4) 18.5% 25.5%);
52
+ --brand-gray-800: hsl(calc(var(--neutral-h) - 3) 23.1% 17.8%);
53
+ --brand-gray-900: hsl(calc(var(--neutral-h) - 2) 29.0% 12.2%);
54
+ --brand-gray-950: hsl(calc(var(--neutral-h) - 3) 37.1% 6.9%);
60
55
 
61
56
  // -----------------------------------------------------------------------
62
57
  // Blue — defined as HSL channels so we can compose hsla() halos and tints
@@ -68,16 +63,16 @@
68
63
  --blue-s: 100%;
69
64
  --blue-l: 59%;
70
65
 
71
- --blue-50: hsl(var(--blue-h), 100%, 97%);
72
- --blue-100: hsl(var(--blue-h), 95%, 94%);
73
- --blue-200: hsl(var(--blue-h), 95%, 89%);
74
- --blue-300: hsl(var(--blue-h), 98%, 82%);
75
- --blue-400: hsl(var(--blue-h), 100%, 75%);
76
- --blue-500: hsl(var(--blue-h), 100%, 66%);
77
- --blue-600: hsl(var(--blue-h), var(--blue-s), var(--blue-l));
78
- --blue-700: hsl(var(--blue-h), 85%, 52%);
79
- --blue-800: hsl(var(--blue-h), 79%, 40%);
80
- --blue-900: hsl(var(--blue-h), 77%, 30%);
66
+ --blue-50: hsl(var(--blue-h) 100% 97%);
67
+ --blue-100: hsl(var(--blue-h) 95% 94%);
68
+ --blue-200: hsl(var(--blue-h) 95% 89%);
69
+ --blue-300: hsl(var(--blue-h) 98% 82%);
70
+ --blue-400: hsl(var(--blue-h) 100% 75%);
71
+ --blue-500: hsl(var(--blue-h) 100% 66%);
72
+ --blue-600: hsl(var(--blue-h) var(--blue-s) var(--blue-l));
73
+ --blue-700: hsl(var(--blue-h) 85% 52%);
74
+ --blue-800: hsl(var(--blue-h) 79% 40%);
75
+ --blue-900: hsl(var(--blue-h) 77% 30%);
81
76
 
82
77
  // -----------------------------------------------------------------------
83
78
  // Orange — focus / secondary signal
@@ -95,6 +90,9 @@
95
90
  // -----------------------------------------------------------------------
96
91
  // Green — success
97
92
  // -----------------------------------------------------------------------
93
+ // HSL channels (additive — for focus-ring composition + ds-color). The ramp
94
+ // stops below stay hand-picked hex; these reproduce --green-500 ≈ #22A33A.
95
+ --green-h: 132; --green-s: 66%; --green-l: 39%;
98
96
  --green-50: #ECFAEE;
99
97
  --green-100: #D6F3DB;
100
98
  --green-200: #BBE8C2;
@@ -107,6 +105,9 @@
107
105
  // -----------------------------------------------------------------------
108
106
  // Yellow — attention / warning
109
107
  // -----------------------------------------------------------------------
108
+ // HSL channels (additive — for focus-ring composition + ds-color). Reproduce
109
+ // --yellow-400 ≈ #F4D43A; ramp stops below stay hand-picked hex.
110
+ --yellow-h: 47; --yellow-s: 89%; --yellow-l: 59%;
110
111
  --yellow-50: #FFFBEA;
111
112
  --yellow-100: #FEF8DC;
112
113
  --yellow-200: #FCEFA1;
@@ -124,14 +125,14 @@
124
125
  --red-s: 75%;
125
126
  --red-l: 49%;
126
127
 
127
- --red-50: hsl(var(--red-h), 86%, 97%);
128
- --red-100: hsl(var(--red-h), 86%, 95%);
129
- --red-200: hsl(var(--red-h), 86%, 88%);
130
- --red-300: hsl(var(--red-h), 84%, 76%);
131
- --red-400: hsl(var(--red-h), 87%, 57%);
132
- --red-500: hsl(var(--red-h), var(--red-s), var(--red-l));
133
- --red-600: hsl(var(--red-h), 78%, 41%);
134
- --red-700: hsl(var(--red-h), 78%, 31%);
128
+ --red-50: hsl(var(--red-h) 86% 97%);
129
+ --red-100: hsl(var(--red-h) 86% 95%);
130
+ --red-200: hsl(var(--red-h) 86% 88%);
131
+ --red-300: hsl(var(--red-h) 84% 76%);
132
+ --red-400: hsl(var(--red-h) 87% 57%);
133
+ --red-500: hsl(var(--red-h) var(--red-s) var(--red-l));
134
+ --red-600: hsl(var(--red-h) 78% 41%);
135
+ --red-700: hsl(var(--red-h) 78% 31%);
135
136
 
136
137
  // -----------------------------------------------------------------------
137
138
  // Sky — info role ramp. A brighter, cooler blue (#3B82F6) kept distinct
@@ -148,12 +149,28 @@
148
149
  --sky-700: #1D4ED8;
149
150
 
150
151
  // -----------------------------------------------------------------------
151
- // Shadow tint — channel-split substrate. Light theme uses a cool
152
- // near-black slate (225 / 39% / 7%). A dark theme would override
153
- // --shadow-tint-l to ~95% so the same alphas render as soft light
154
- // glows over dark surfaces.
152
+ // Shadow tint — channel-split substrate h/s only. The lightness (-l) is
153
+ // per-theme: light uses 7%, dark uses 95% (so the same alphas read as soft
154
+ // glows over dark surfaces). Each theme's _palette.scss mixin sets -l.
155
155
  // -----------------------------------------------------------------------
156
156
  --shadow-tint-h: 225;
157
157
  --shadow-tint-s: 39%;
158
- --shadow-tint-l: 7%;
158
+
159
+ // -----------------------------------------------------------------------
160
+ // Theme-stable contrast anchors — never flip with the theme. On-fill text/
161
+ // icon roles point here so a dark theme keeps white-on-coloured-fill correct.
162
+ // -----------------------------------------------------------------------
163
+ --static-white: #FFF;
164
+ --static-ink: #161A28;
165
+
166
+ // -----------------------------------------------------------------------
167
+ // Font families — a theme/brand choice (the size/weight/leading SCALE is
168
+ // theme-independent and lives in styles/_tokens.scss). Display + heading
169
+ // default to body sans; a brand can mix a serif/display family here without
170
+ // touching component code.
171
+ // -----------------------------------------------------------------------
172
+ --font-sans: "Mulish", -apple-system, blinkmacsystemfont, "Segoe UI", roboto, sans-serif;
173
+ --font-mono: "JetBrains Mono", ui-monospace, sfmono-regular, menlo, monospace;
174
+ --font-display: var(--font-sans);
175
+ --font-heading: var(--font-sans);
159
176
  }
@@ -1,40 +1,21 @@
1
1
  // =========================================================================
2
- // Default theme — Tier 2 semantic role ASSIGNMENTS (light).
2
+ // Shared role structure — Tier 2 semantic role ASSIGNMENTS.
3
3
  //
4
- // The palette (themes/default/_palette.scss) DEFINES the concrete colours
5
- // (--blue-*, --red-*, --neutral-*, + HSL channels). THIS file SETS them
6
- // onto semantic roles (--primary --blue-600, --surface-canvas → --neutral-50,
7
- // --error --red-500, …). Components consume only these roles, never the
8
- // palette directly so swapping the palette, or re-mapping here, re-skins the
9
- // whole system. A sibling theme (e.g. dark) ships its own palette + this map.
4
+ // Exposes @mixin roles: the role map whose structure is identical across
5
+ // themes and which resolves through the per-theme palette (surfaces
6
+ // --neutral-*, text/icon/border --neutral-*, brand channels, status, focus,
7
+ // semanticpalette bridge). These are the LIGHT-DEFAULT values dark theme
8
+ // overrides only the deltas in its own _theme.scss mixin body.
10
9
  //
11
- // Tier 3 (component-scoped tokens, geometry, typography, motion) lives in
12
- // styles/_tokens.scss and is theme-independent.
10
+ // This mixin is an authoring-DRY device. At runtime, exactly one theme block
11
+ // matches (`:not([data-theme='dark'])` vs `[data-theme='dark']`), and that
12
+ // block declares the complete token set itself (palette + roles + overrides).
13
13
  // =========================================================================
14
14
 
15
- :root {
16
- /* Neutral family slot — semantic pointer at the active gray ramp from the
17
- palette. Default: hand-drawn cool-gray. <html data-neutrals="brand">
18
- (ds-docs tweaks panel) repoints it at the brand-derived brand-gray ramp,
19
- where --primary-h re-tints every gray. Roles below consume --neutral-*
20
- so the family switch never touches them. */
21
- --neutral-0: var(--cool-gray-0);
22
- --neutral-50: var(--cool-gray-50);
23
- --neutral-100: var(--cool-gray-100);
24
- --neutral-150: var(--cool-gray-150);
25
- --neutral-200: var(--cool-gray-200);
26
- --neutral-300: var(--cool-gray-300);
27
- --neutral-400: var(--cool-gray-400);
28
- --neutral-500: var(--cool-gray-500);
29
- --neutral-600: var(--cool-gray-600);
30
- --neutral-700: var(--cool-gray-700);
31
- --neutral-800: var(--cool-gray-800);
32
- --neutral-900: var(--cool-gray-900);
33
- --neutral-950: var(--cool-gray-950);
34
-
15
+ @mixin roles {
35
16
  /* Surfaces — six-stop scale.
36
17
  · canvas : page background
37
- · default : cards, sheets, toolbars (the paper)
18
+ · default : cards, sheets, toolbars (the "paper")
38
19
  · secondary : hover lanes, table heads, addons, code blocks
39
20
  · overlay : modals, drawers, popovers (paired with scrim)
40
21
  · tint : brand-tinted (active rows, sidenav active item)
@@ -54,12 +35,12 @@
54
35
 
55
36
  /* Scrim — modal/drawer backdrop. Derived from inverted surface
56
37
  so dark-mode override flips automatically. */
57
- --surface-scrim: hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .55);
38
+ --surface-scrim: hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 55%);
58
39
 
59
40
  /* Visible-on-coloured-fill divider. The split-button right-hand chevron
60
41
  sits ON the brand fill, so a transparent white hairline reads correctly
61
42
  across any brand hue. Override per theme if the brand is too light. */
62
- --btn-split-divider: hsla(0, 0%, 100%, .25);
43
+ --btn-split-divider: hsl(0deg 0% 100% / 25%);
63
44
 
64
45
  /* Visited link — derives from --primary-muted by default so it sympathetically
65
46
  shifts with the brand hue. Override (e.g. for financial dashboards that
@@ -81,35 +62,28 @@
81
62
  --text-muted: var(--neutral-400);
82
63
  --text-disabled: var(--neutral-400);
83
64
  --text-inverted: var(--neutral-0);
65
+
84
66
  /* Text overlaid on arbitrary imagery (camera feeds, photos) — pairs a
85
67
  guaranteed-light colour with a contrast shadow. */
86
- --text-on-media: var(--neutral-0);
87
- --text-on-media-shadow: 0 1px 3px hsla(0, 0%, 0%, .7);
68
+ --text-on-media: var(--static-white);
69
+ --text-on-media-shadow: 0 1px 3px hsl(0deg 0% 0% / 70%);
88
70
  --text-link: var(--blue-700);
89
- --text-on-primary: var(--neutral-0);
90
- --text-on-success: var(--neutral-0);
71
+ --text-on-primary: var(--static-white);
72
+ --text-on-success: var(--static-white);
73
+
91
74
  /* Warning fill is yellow (--yellow-400 #F4D43A) — white text on it is only
92
75
  ~1.5:1 (fails WCAG). On-warning text is the darkest neutral (~10:1). */
93
- --text-on-warning: var(--neutral-900);
94
- --text-on-error: var(--neutral-0);
76
+ --text-on-warning: var(--static-ink);
77
+ --text-on-error: var(--static-white);
95
78
 
96
79
  /* Icons */
97
80
  --icon-default: var(--neutral-500);
98
81
  --icon-strong: var(--neutral-700);
99
82
  --icon-muted: var(--neutral-400);
100
- --icon-on-primary: var(--neutral-0);
101
-
102
- /* Font families — a theme/brand choice (the size/weight/leading SCALE is
103
- theme-independent and lives in styles/_tokens.scss). Display + heading
104
- default to body sans; a brand can mix a serif/display family here without
105
- touching component code. */
106
- --font-sans: "Mulish", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
107
- --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
108
- --font-display: var(--font-sans);
109
- --font-heading: var(--font-sans);
83
+ --icon-on-primary: var(--static-white);
110
84
 
111
85
  /* Semantic → palette bridge. The lib knows colours by PURPOSE; the theme
112
- (themes/default/_theme.scss) ships the concrete ramps + HSL channels. This
86
+ (themes/light/_theme.scss) ships the concrete ramps + HSL channels. This
113
87
  is the one place that maps semantic roles onto a concrete palette. A brand
114
88
  re-skins by overriding either the palette (--blue-*, --red-*) or these
115
89
  role tokens directly.
@@ -120,6 +94,12 @@
120
94
  --error-h: var(--red-h);
121
95
  --error-s: var(--red-s);
122
96
  --error-l: var(--red-l);
97
+ --success-h: var(--green-h);
98
+ --success-s: var(--green-s);
99
+ --success-l: var(--green-l);
100
+ --warning-h: var(--yellow-h);
101
+ --warning-s: var(--yellow-s);
102
+ --warning-l: var(--yellow-l);
123
103
 
124
104
  /* Primary roles.
125
105
  Hover / pressed / selected-hover are DERIVED from --primary at runtime via
@@ -135,7 +115,7 @@
135
115
  --primary-muted: var(--blue-100);
136
116
  --primary-muted-strong: var(--blue-200);
137
117
  --primary-strong: var(--blue-700);
138
- --primary-on: var(--neutral-0);
118
+ --primary-on: var(--static-white);
139
119
 
140
120
  /* Accent / secondary role — composed from --secondary-h/s/l channels.
141
121
  Channels default to brand values so unbranded apps render identical to
@@ -146,12 +126,13 @@
146
126
  --secondary-s: var(--primary-s);
147
127
  --secondary-l: var(--primary-l);
148
128
 
149
- --secondary: hsl(var(--secondary-h), var(--secondary-s), var(--secondary-l));
129
+ --secondary: hsl(var(--secondary-h) var(--secondary-s) var(--secondary-l));
150
130
  --secondary-hover: color-mix(in srgb, var(--secondary) 88%, black);
151
131
  --secondary-pressed: color-mix(in srgb, var(--secondary) 76%, black);
152
- --secondary-strong: hsl(var(--secondary-h), var(--secondary-s), calc(var(--secondary-l) - 11%));
153
- --secondary-subtitle: hsl(var(--secondary-h), 100%, 97%);
154
- --secondary-muted: hsl(var(--secondary-h), 95%, 94%);
132
+ --secondary-strong: hsl(var(--secondary-h) var(--secondary-s) calc(var(--secondary-l) - 11%));
133
+ --secondary-subtitle: hsl(var(--secondary-h) 100% 97%);
134
+ --secondary-muted: hsl(var(--secondary-h) 95% 94%);
135
+ --secondary-muted-strong: hsl(var(--secondary-h) 95% 89%);
155
136
  --secondary-on: var(--primary-on);
156
137
 
157
138
  /* Tertiary — third brand hue. Used by the tertiary button variant and
@@ -165,12 +146,13 @@
165
146
  --tertiary-s: var(--primary-s);
166
147
  --tertiary-l: var(--primary-l);
167
148
 
168
- --tertiary: hsl(var(--tertiary-h), var(--tertiary-s), var(--tertiary-l));
149
+ --tertiary: hsl(var(--tertiary-h) var(--tertiary-s) var(--tertiary-l));
169
150
  --tertiary-hover: color-mix(in srgb, var(--tertiary) 88%, black);
170
151
  --tertiary-pressed: color-mix(in srgb, var(--tertiary) 76%, black);
171
- --tertiary-strong: hsl(var(--tertiary-h), var(--tertiary-s), calc(var(--tertiary-l) - 11%));
172
- --tertiary-subtitle: hsl(var(--tertiary-h), 100%, 97%);
173
- --tertiary-muted: hsl(var(--tertiary-h), 95%, 94%);
152
+ --tertiary-strong: hsl(var(--tertiary-h) var(--tertiary-s) calc(var(--tertiary-l) - 11%));
153
+ --tertiary-subtitle: hsl(var(--tertiary-h) 100% 97%);
154
+ --tertiary-muted: hsl(var(--tertiary-h) 95% 94%);
155
+ --tertiary-muted-strong: hsl(var(--tertiary-h) 95% 89%);
174
156
  --tertiary-on: var(--primary-on);
175
157
 
176
158
  /* Status roles.
@@ -182,6 +164,7 @@
182
164
  --success-hover: color-mix(in srgb, var(--success) 88%, black);
183
165
  --success-subtitle: var(--green-50);
184
166
  --success-muted: var(--green-100);
167
+ --success-muted-strong: var(--green-200);
185
168
  --success-border: var(--green-200);
186
169
  --success-text: var(--green-700);
187
170
 
@@ -189,6 +172,7 @@
189
172
  --warning-hover: color-mix(in srgb, var(--warning) 88%, black);
190
173
  --warning-subtitle: var(--yellow-50);
191
174
  --warning-muted: var(--yellow-100);
175
+ --warning-muted-strong: var(--yellow-200);
192
176
  --warning-border: var(--yellow-200);
193
177
  --warning-text: var(--yellow-700);
194
178
 
@@ -197,9 +181,18 @@
197
181
  --error-pressed: color-mix(in srgb, var(--error) 76%, black);
198
182
  --error-subtitle: var(--red-50);
199
183
  --error-muted: var(--red-100);
184
+ --error-muted-strong: var(--red-200);
200
185
  --error-border: var(--red-200);
201
186
  --error-text: var(--red-600);
202
187
 
188
+ /* -pressed (success/warning) + -on aliases — added for the --accent-* pointer
189
+ family so every ds-color family resolves --accent-pressed / --accent-on. */
190
+ --success-pressed: color-mix(in srgb, var(--success) 76%, black);
191
+ --warning-pressed: color-mix(in srgb, var(--warning) 76%, black);
192
+ --error-on: var(--text-on-error);
193
+ --success-on: var(--text-on-success);
194
+ --warning-on: var(--text-on-warning);
195
+
203
196
  --info: var(--sky-500);
204
197
  --info-hover: color-mix(in srgb, var(--info) 88%, black);
205
198
  --info-subtitle: var(--sky-50);
@@ -212,17 +205,18 @@
212
205
  brand re-skin moves Confirmed with it. The other lifecycle
213
206
  stages are deliberately neutral/green/dark to keep semantic
214
207
  distance from brand. */
208
+
215
209
  /* Status pills — canonical (single system).
216
210
  Legacy classes `.badge--active` (single) and ad-hoc semantic
217
211
  pills are unified in the Badge component to consume these tokens. */
218
- --status-planning-bg: var(--neutral-150);
219
- --status-planning-fg: var(--neutral-700);
220
- --status-confirmed-bg: var(--primary-subtitle);
221
- --status-confirmed-fg: var(--primary-strong);
222
- --status-active-bg: var(--green-50);
223
- --status-active-fg: var(--green-700);
224
- --status-completed-bg: var(--neutral-900);
225
- --status-completed-fg: var(--neutral-0);
212
+ --status-planning-bg-color: var(--neutral-150);
213
+ --status-planning-text-color: var(--neutral-700);
214
+ --status-confirmed-bg-color: var(--primary-subtitle);
215
+ --status-confirmed-text-color: var(--primary-strong);
216
+ --status-active-bg-color: var(--green-50);
217
+ --status-active-text-color: var(--green-700);
218
+ --status-completed-bg-color: var(--neutral-900);
219
+ --status-completed-text-color: var(--neutral-0);
226
220
 
227
221
  /* Focus rings.
228
222
  Outer halos use hsla() composed from brand/danger HSL channels
@@ -237,38 +231,60 @@
237
231
 
238
232
  --focus-ring:
239
233
  0 0 0 2px var(--focus-ring-inner),
240
- 0 0 0 4px hsla(var(--focus-ring-outer-h), var(--focus-ring-outer-s), var(--focus-ring-outer-l), var(--focus-ring-outer-alpha));
234
+ 0 0 0 4px hsl(var(--focus-ring-outer-h) var(--focus-ring-outer-s) var(--focus-ring-outer-l) / var(--focus-ring-outer-alpha));
241
235
  --focus-ring-inverted:
242
236
  0 0 0 2px var(--neutral-900),
243
237
  0 0 0 4px var(--blue-300);
244
238
  --focus-ring-error:
245
239
  0 0 0 2px var(--focus-ring-inner),
246
- 0 0 0 4px hsla(var(--error-h), var(--error-s), var(--error-l), 1);
240
+ 0 0 0 4px hsl(var(--error-h) var(--error-s) var(--error-l) / 100%);
247
241
 
248
242
  /* Field focus — coloured halo only (no double-ring). Derived from
249
243
  brand HSL so it tracks rebrand. Alpha is a token so dark themes
250
244
  can boost it for legibility. */
251
245
  --focus-field-alpha: .22;
252
246
  --focus-field:
253
- 0 0 0 3px hsla(var(--primary-h), var(--primary-s), var(--primary-l), var(--focus-field-alpha));
247
+ 0 0 0 3px hsl(var(--primary-h) var(--primary-s) var(--primary-l) / var(--focus-field-alpha));
254
248
  --focus-field-error:
255
- 0 0 0 3px hsla(var(--error-h), var(--error-s), var(--error-l), var(--focus-field-alpha));
256
- }
249
+ 0 0 0 3px hsl(var(--error-h) var(--error-s) var(--error-l) / var(--focus-field-alpha));
250
+
251
+ /* Per-family focus rings/fields — pre-composed once from each family's
252
+ channels so ds-color flips a single --accent-focus-* pointer (no :root
253
+ composite re-declaration in the scope). --focus-ring-error exists above. */
254
+ --focus-ring-secondary:
255
+ 0 0 0 2px var(--focus-ring-inner),
256
+ 0 0 0 4px hsl(var(--secondary-h) var(--secondary-s) var(--secondary-l) / 100%);
257
+ --focus-ring-tertiary:
258
+ 0 0 0 2px var(--focus-ring-inner),
259
+ 0 0 0 4px hsl(var(--tertiary-h) var(--tertiary-s) var(--tertiary-l) / 100%);
260
+ --focus-ring-success:
261
+ 0 0 0 2px var(--focus-ring-inner),
262
+ 0 0 0 4px hsl(var(--success-h) var(--success-s) var(--success-l) / 100%);
263
+ --focus-ring-warning:
264
+ 0 0 0 2px var(--focus-ring-inner),
265
+ 0 0 0 4px hsl(var(--warning-h) var(--warning-s) var(--warning-l) / 100%);
266
+ --focus-field-secondary:
267
+ 0 0 0 3px hsl(var(--secondary-h) var(--secondary-s) var(--secondary-l) / var(--focus-field-alpha));
268
+ --focus-field-tertiary:
269
+ 0 0 0 3px hsl(var(--tertiary-h) var(--tertiary-s) var(--tertiary-l) / var(--focus-field-alpha));
270
+ --focus-field-success:
271
+ 0 0 0 3px hsl(var(--success-h) var(--success-s) var(--success-l) / var(--focus-field-alpha));
272
+ --focus-field-warning:
273
+ 0 0 0 3px hsl(var(--warning-h) var(--warning-s) var(--warning-l) / var(--focus-field-alpha));
257
274
 
258
- /* Brand-derived neutralsopt-in family switch (ds-docs tweaks panel sets
259
- the attribute). Repoints the --neutral-* slot at the brand-gray ramp. */
260
- :root[data-neutrals='brand'] {
261
- --neutral-0: var(--brand-gray-0);
262
- --neutral-50: var(--brand-gray-50);
263
- --neutral-100: var(--brand-gray-100);
264
- --neutral-150: var(--brand-gray-150);
265
- --neutral-200: var(--brand-gray-200);
266
- --neutral-300: var(--brand-gray-300);
267
- --neutral-400: var(--brand-gray-400);
268
- --neutral-500: var(--brand-gray-500);
269
- --neutral-600: var(--brand-gray-600);
270
- --neutral-700: var(--brand-gray-700);
271
- --neutral-800: var(--brand-gray-800);
272
- --neutral-900: var(--brand-gray-900);
273
- --neutral-950: var(--brand-gray-950);
275
+ /* Accent pointer layer the single indirection ds-color flips. Defaults to
276
+ the primary family (+ existing focus/tint tokens) so the unbranded theme is
277
+ pixel-identical. Components read these for their accent instead of
278
+ --primary* / --focus-* / --surface-tint. */
279
+ --accent: var(--primary);
280
+ --accent-hover: var(--primary-hover);
281
+ --accent-pressed: var(--primary-pressed);
282
+ --accent-strong: var(--primary-strong);
283
+ --accent-subtitle: var(--primary-subtitle);
284
+ --accent-muted: var(--primary-muted);
285
+ --accent-muted-strong: var(--primary-muted-strong);
286
+ --accent-on: var(--primary-on);
287
+ --accent-focus-ring: var(--focus-ring);
288
+ --accent-focus-field: var(--focus-field);
289
+ --accent-surface-tint: var(--surface-tint);
274
290
  }
@@ -0,0 +1,39 @@
1
+ // =========================================================================
2
+ // Dark palette — Tier 1b per-theme primitives.
3
+ //
4
+ // Exposes @mixin palette with ONLY the dark-specific primitives:
5
+ // · --neutral-* slot: hand-tuned dark neutrals (NOT a linear inverse —
6
+ // note --neutral-0 is an ELEVATED card surface, LIGHTER than the
7
+ // --neutral-50 canvas — the opposite of light, where 0 (#FFF) is the
8
+ // brightest stop). Brand/status ramps are left to the role layer.
9
+ // · --shadow-tint-l: 95% (flipped so same alphas read as soft glows)
10
+ //
11
+ // Theme-independent ramps (--cool-gray-*, --brand-gray-*, --blue-*, etc.)
12
+ // and HSL channels live in themes/_base-palette.scss and emit at :root.
13
+ //
14
+ // KNOWN LIMITATION — data-neutrals="brand" + dark: this ramp overrides
15
+ // --neutral-* directly, so when both `[data-neutrals='brand']` and
16
+ // `[data-theme='dark']` are set (equal specificity) dark wins by source order —
17
+ // the brand-gray hue-tint is dropped and these fixed dark neutrals apply. Benign
18
+ // (renders correctly dark, just not brand-hue-tinted); a dark brand-gray ramp is
19
+ // a future follow-up, not v1.
20
+ // =========================================================================
21
+
22
+ @mixin palette {
23
+ --neutral-0: #171B24; // surface-default / overlay (cards) — elevated
24
+ --neutral-50: #0F131A; // surface-canvas — darkest (page bg)
25
+ --neutral-100: #1E2330; // surface-secondary / disabled
26
+ --neutral-150: #242A38; // surface-muted, border-subtitle/divider/disabled
27
+ --neutral-200: #2A3040; // border-default
28
+ --neutral-300: #3A4252; // border-strong
29
+ --neutral-400: #6B7388; // text-muted/disabled, icon-muted
30
+ --neutral-500: #8A93A6; // text-tertiary, icon-default
31
+ --neutral-600: #9BA3B4;
32
+ --neutral-700: #B7BFD0; // text-secondary, icon-strong, border-inverted
33
+ --neutral-800: #CDD3E0;
34
+ --neutral-900: #E7EAF2; // text-primary, surface-inverted
35
+ --neutral-950: #F4F6FA;
36
+
37
+ // Shadow substrate flips light so the same alphas read as soft glows on dark.
38
+ --shadow-tint-l: 95%;
39
+ }
@@ -0,0 +1,122 @@
1
+ // =========================================================================
2
+ // Dark theme — Tier 2 role overrides.
3
+ //
4
+ // Composes the dark palette (neutral ramp + shadow-l 95%) with the shared
5
+ // role structure, then overrides only the roles whose VALUES differ from
6
+ // the light defaults. The activation selector (:root[data-theme='dark'])
7
+ // lives in styles/_theme-activation.scss. Dark is opt-in — light is the
8
+ // default; there is no automatic prefers-color-scheme switch.
9
+ //
10
+ // Most roles follow the inverted --neutral-* ramp automatically (custom
11
+ // props resolve at use-time). This file overrides ONLY roles that can't
12
+ // follow the flip: tint/subtle surfaces (near-white *-50 picks → dark hue
13
+ // tints), on-tint text (*-text / *-strong → lighter stops), hover/pressed
14
+ // direction, scrim (must stay dark), focus halo alpha, and charts.
15
+ // On-fill text already points at theme-stable --static-white/--static-ink.
16
+ // =========================================================================
17
+
18
+ @use 'palette';
19
+ @use '../semantic';
20
+
21
+ @mixin theme {
22
+ @include palette.palette;
23
+ @include semantic.roles;
24
+
25
+ // Interaction states: on dark, hover/pressed must LIGHTEN. The default theme
26
+ // derives them as color-mix toward black (darkens) — wrong direction here.
27
+ // Mirror the light recipe's 88% / 76% steps, mixed toward white instead.
28
+ --primary-hover: color-mix(in srgb, var(--primary) 88%, white);
29
+ --primary-pressed: color-mix(in srgb, var(--primary) 76%, white);
30
+ --primary-selected-hover: color-mix(in srgb, var(--primary) 88%, white);
31
+ --secondary-hover: color-mix(in srgb, var(--secondary) 88%, white);
32
+ --secondary-pressed: color-mix(in srgb, var(--secondary) 76%, white);
33
+ --tertiary-hover: color-mix(in srgb, var(--tertiary) 88%, white);
34
+ --tertiary-pressed: color-mix(in srgb, var(--tertiary) 76%, white);
35
+ --success-hover: color-mix(in srgb, var(--success) 88%, white);
36
+ --warning-hover: color-mix(in srgb, var(--warning) 88%, white);
37
+ --error-hover: color-mix(in srgb, var(--error) 88%, white);
38
+ --error-pressed: color-mix(in srgb, var(--error) 76%, white);
39
+ --success-pressed: color-mix(in srgb, var(--success) 76%, white);
40
+ --warning-pressed: color-mix(in srgb, var(--warning) 76%, white);
41
+ --info-hover: color-mix(in srgb, var(--info) 88%, white);
42
+
43
+ // Brand tint surfaces + on-tint text (channel-derived → track rebrand).
44
+ --surface-tint: hsl(var(--primary-h) 45% 16%);
45
+ --primary-subtitle: hsl(var(--primary-h) 45% 18%);
46
+ --primary-muted: hsl(var(--primary-h) 40% 26%);
47
+ --primary-muted-strong: hsl(var(--primary-h) 42% 32%);
48
+ --primary-strong: hsl(var(--primary-h) 90% 72%);
49
+
50
+ --secondary-subtitle: hsl(var(--secondary-h) 45% 18%);
51
+ --secondary-muted: hsl(var(--secondary-h) 40% 26%);
52
+ --secondary-muted-strong: hsl(var(--secondary-h) 42% 32%);
53
+ --secondary-strong: hsl(var(--secondary-h) 90% 72%);
54
+
55
+ --tertiary-subtitle: hsl(var(--tertiary-h) 45% 18%);
56
+ --tertiary-muted: hsl(var(--tertiary-h) 40% 26%);
57
+ --tertiary-muted-strong: hsl(var(--tertiary-h) 42% 32%);
58
+ --tertiary-strong: hsl(var(--tertiary-h) 90% 72%);
59
+
60
+ --text-link: hsl(var(--primary-h) 90% 74%);
61
+ --link-visited: hsl(var(--primary-h) 50% 70%);
62
+
63
+ // Status families: dark tint bg + border, lighter text.
64
+ --success-subtitle: hsl(132deg 38% 14%);
65
+ --success-muted: hsl(132deg 34% 20%);
66
+ --success-muted-strong: hsl(132deg 30% 28%);
67
+ --success-border: hsl(132deg 30% 28%);
68
+ --success-text: hsl(132deg 55% 62%);
69
+
70
+ --warning-subtitle: hsl(45deg 42% 14%);
71
+ --warning-muted: hsl(45deg 38% 22%);
72
+ --warning-muted-strong: hsl(45deg 35% 30%);
73
+ --warning-border: hsl(45deg 35% 30%);
74
+ --warning-text: hsl(45deg 90% 62%);
75
+
76
+ --error-subtitle: hsl(0deg 45% 16%);
77
+ --error-muted: hsl(0deg 40% 24%);
78
+ --error-muted-strong: hsl(0deg 38% 32%);
79
+ --error-border: hsl(0deg 38% 32%);
80
+ --error-text: hsl(0deg 85% 74%);
81
+
82
+ --info-subtitle: hsl(217deg 45% 16%);
83
+ --info-muted: hsl(217deg 40% 24%);
84
+ --info-border: hsl(217deg 38% 32%);
85
+ --info-text: hsl(217deg 90% 74%);
86
+
87
+ // Status pills that referenced palette stops directly (green-50/700) — repoint
88
+ // to the dark status roles so the lifecycle pill reads on dark. (planning →
89
+ // neutral, confirmed → primary-*, completed → neutral all follow flips.)
90
+ --status-active-bg-color: var(--success-subtitle);
91
+ --status-active-text-color: var(--success-text);
92
+
93
+ // Scrim stays dark — must NOT follow --shadow-tint-l: 95%.
94
+ --surface-scrim: hsl(225deg 39% 4% / 70%);
95
+
96
+ // Field focus halo — boost alpha for legibility on dark.
97
+ --focus-field-alpha: .35;
98
+
99
+ // Chart / dataviz overrides. Axis/grid/label/title/cursor already flip via the
100
+ // --neutral-* / --text-* roles; only the light-bg-tuned bits move here.
101
+ --chart-hover-bg-color: hsl(0deg 0% 100% / 6%); // light wash (default dark wash is invisible on dark)
102
+ --chart-tooltip-bg-color: var(--surface-overlay); // native dark tooltip, not an inverted light box
103
+ --chart-tooltip-text-color: var(--text-primary);
104
+ --chart-tooltip-border: var(--border-strong);
105
+
106
+ // Sequential — flip so low → high reads dark → bright on a dark canvas.
107
+ --chart-seq-1: var(--blue-800);
108
+ --chart-seq-2: var(--blue-700);
109
+ --chart-seq-3: var(--blue-600);
110
+ --chart-seq-4: var(--blue-500);
111
+ --chart-seq-5: var(--blue-400);
112
+ --chart-seq-6: var(--blue-300);
113
+
114
+ // Divergent — bright red ← dark-neutral zero → bright green (|value| → brightness).
115
+ --chart-div--3: hsl(0deg 70% 62%);
116
+ --chart-div--2: hsl(0deg 55% 46%);
117
+ --chart-div--1: hsl(0deg 45% 32%);
118
+ --chart-div-0: var(--neutral-150);
119
+ --chart-div-1: hsl(140deg 40% 32%);
120
+ --chart-div-2: hsl(140deg 48% 46%);
121
+ --chart-div-3: hsl(140deg 55% 60%);
122
+ }