@ids-group-ltd/ids-design-system 0.1.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,96 @@
1
+ # Themes — palette + role assignments
2
+
3
+ Colour lives in two files here: a **palette** (defines the concrete colours) and a **theme** (assigns them to semantic roles). The geometry/component layer is separate (`../styles/_tokens.scss`).
4
+
5
+ ## Architecture: three tiers
6
+
7
+ | Tier | Lives in | Examples | Consumers |
8
+ |---|---|---|---|
9
+ | **Tier 1 — Palette** (defines colours) | `themes/<name>/_palette.scss` | `--blue-600`, `--cool-gray-0`, `--brand-gray-500`, `--red-500`, `--blue-h/s/l`, `--shadow-tint-h/s/l` | Only the theme file |
10
+ | **Tier 2 — Theme** (sets roles) | `themes/<name>/_theme.scss` | `--primary: var(--blue-600)`, `--text-primary`, `--surface-default`, `--success`, `--focus-ring`, `--primary-h: var(--blue-h)`, font families; the `--neutral-*` family slot (cool-gray by default, brand-gray via `[data-neutrals="brand"]`) | Components |
11
+ | **Tier 3 — Component / scale** | `styles/_tokens.scss` and inside `*.component.scss` | `--space-*`, `--radius-*`, `--ds-button-bg`, `--duration-fast`, `--layer-modal` | Components (their own) |
12
+
13
+ **Hard rule:** components NEVER consume Tier 1 palette directly. If you write `var(--blue-600)` inside `button.component.scss`, that's a bug — introduce or reuse a Tier 2 role.
14
+
15
+ **Why split palette from theme:** the palette says *what colours exist*; the theme says *what each role is*. A dark theme ships a different palette (different ramp values) + a theme map that re-points roles. Swapping either re-skins the system without touching components.
16
+
17
+ ## Available palette + theme
18
+
19
+ - **`default/_palette.scss`** — Blue + slate ramps (the concrete colours).
20
+ - **`default/_theme.scss`** — Light role map onto the default palette. Both are bundled into `styles/ds.scss` (palette → theme → tokens) so `@use 'styles/ds';` works out of the box.
21
+
22
+ Future (each a `themes/<name>/` folder with `_palette.scss` + `_theme.scss`):
23
+ - `themes/dark/` — Dark-mode flip (`--shadow-tint-l` → ~95%, neutrals inverted, etc.).
24
+ - `themes/high-contrast/` — A11y boost.
25
+
26
+ ## How a consumer picks a theme
27
+
28
+ ### Option A — out-of-the-box default
29
+
30
+ ```scss
31
+ // Consumer styles.scss
32
+ @use 'styles/ds'; // bundles default palette + theme + tokens + reset + ...
33
+ ```
34
+
35
+ ### Option B — explicit theme choice
36
+
37
+ ```scss
38
+ // Consumer styles.scss
39
+ @use 'themes/dark/palette'; // pick a non-default theme: palette …
40
+ @use 'themes/dark/theme'; // … then its role map
41
+ @use 'styles/tokens';
42
+ @use 'styles/reset';
43
+ @use 'styles/typography';
44
+ @use 'styles/scrollbar';
45
+ @use 'styles/icon-base';
46
+ @use 'styles/dropdown-overlay';
47
+ ```
48
+
49
+ Palette + theme must come BEFORE `tokens` so role aliases resolve.
50
+
51
+ ### Option C — brand layer on top of default theme
52
+
53
+ ```scss
54
+ // Consumer styles.scss
55
+ @use 'styles/ds'; // default theme + DS
56
+ @use './app/styles/brand-manage-my'; // overrides palette/roles via [data-brand]
57
+ ```
58
+
59
+ This is the mm-broker pattern: load DS (default theme included), then overlay a brand at a more-specific selector (`[data-brand="manage-my"]`).
60
+
61
+ ## Brand vs theme — what's the difference?
62
+
63
+ - **Theme** swaps the entire Tier 1 palette. Affects EVERY semantic role downstream.
64
+ - **Brand** is a thin overlay scoped via attribute selector (`[data-brand="X"]`). Usually overrides a smaller subset (key palette stops + maybe a few roles).
65
+
66
+ Same mechanism (CSS custom properties + cascade), different scope.
67
+
68
+ ## Adding a new theme
69
+
70
+ 1. Create a `themes/<name>/` folder with `_palette.scss` (ramps + HSL channels) and `_theme.scss` (role map), mirroring `themes/default/`.
71
+ 2. Optionally add it to `styles/ds.scss` if it should be a default-bundled option.
72
+ 3. Or let consumers `@use 'themes/<name>/palette'; @use 'themes/<name>/theme';` directly.
73
+
74
+ Components don't need any change — they consume semantic roles which automatically pick up the new palette.
75
+
76
+ ## Adding a new brand override
77
+
78
+ 1. Create `<consumer-app>/src/app/styles/_brand-X.scss`.
79
+ 2. Open with `[data-brand="X"] { ... }`.
80
+ 3. Override:
81
+ - **Palette HSL channels** (`--blue-h/s/l`) — for focus rings and halos that compose hsla() from channels (the theme bridges `--primary-h: var(--blue-h)`).
82
+ - **Blue ramp** (`--blue-50..900`) — single source of truth. All brand-derived roles (`--primary`, `--primary-strong`, `--primary-subtle`, `--primary-muted-strong`, `--text-link`, `--surface-tint`) automatically retint. **Do NOT also override the `--primary-*` roles directly — that's redundant.**
83
+ - Other roles you want to retint independently (surfaces, borders, text, success/danger ramps, secondary, etc.).
84
+ 4. Apply via `<html data-brand="X">` on the host app.
85
+
86
+ ## Rules summary
87
+
88
+ - Components NEVER reference `var(--blue-600)` / `var(--neutral-200)` / `var(--red-500)` directly. Use a Tier 2 role (`--primary`, `--text-secondary`) or a Tier 3 component token.
89
+ - Themes own palette. Brands overlay on top via `[data-brand]`.
90
+ - Theme switching = swap one `@use 'themes/X'`. No component changes.
91
+ - `--primary-hover`, `--primary-pressed` are auto-derived via `color-mix()`. Don't override unless brand needs non-derived hover.
92
+ - Audit guard (suggested pre-commit hook):
93
+ ```bash
94
+ grep -nE 'var\(--(violet|neutral|red|green|yellow|blue|orange|cyan|magenta)-[0-9]+\)' \
95
+ prototype/ds/src/lib/components/**/*.scss && exit 1
96
+ ```
@@ -0,0 +1,159 @@
1
+ // =========================================================================
2
+ // Default palette — Tier 1 concrete colour primitives.
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.
10
+ //
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".
17
+ // =========================================================================
18
+
19
+ :root {
20
+ // -----------------------------------------------------------------------
21
+ // Cool gray ramp (13 stops) — hand-drawn slate-family gray. The hue drifts
22
+ // 210 → 227 toward the brand hue (229) as it darkens; chroma follows a
23
+ // designed U-curve (S 40% → 12% → 37%). Appearance name, Carbon-style.
24
+ // -----------------------------------------------------------------------
25
+ --cool-gray-0: #FFFFFF;
26
+ --cool-gray-50: #F8FAFC;
27
+ --cool-gray-100: #F1F4F9;
28
+ --cool-gray-150: #E8ECF2;
29
+ --cool-gray-200: #DCE1EA;
30
+ --cool-gray-300: #C2C9D6;
31
+ --cool-gray-400: #9BA3B4;
32
+ --cool-gray-500: #6B7388;
33
+ --cool-gray-600: #4D5468;
34
+ --cool-gray-700: #353B4D;
35
+ --cool-gray-800: #232838;
36
+ --cool-gray-900: #161A28;
37
+ --cool-gray-950: #0B0E18;
38
+
39
+ // -----------------------------------------------------------------------
40
+ // Brand gray ramp (13 stops) — derived, M3-style: the cool-gray S/L curve
41
+ // and hue drift are kept, but the hue source is --neutral-h (defaults to
42
+ // the brand hue). Re-skinning --primary-h re-tints these grays to match
43
+ // the brand automatically. With the default blue brand this ramp matches
44
+ // cool-gray to within rounding.
45
+ // -----------------------------------------------------------------------
46
+ --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%);
60
+
61
+ // -----------------------------------------------------------------------
62
+ // Blue — defined as HSL channels so we can compose hsla() halos and tints
63
+ // from the same source. #2F54FF ≈ hsl(229, 100%, 59%). This is the ramp the
64
+ // default theme maps --primary onto (see styles/_tokens.scss). Naming is
65
+ // concrete (the colour) — the semantic --primary mapping lives in the lib.
66
+ // -----------------------------------------------------------------------
67
+ --blue-h: 229;
68
+ --blue-s: 100%;
69
+ --blue-l: 59%;
70
+
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%);
81
+
82
+ // -----------------------------------------------------------------------
83
+ // Orange — focus / secondary signal
84
+ // -----------------------------------------------------------------------
85
+ --orange-50: #FFF7F0;
86
+ --orange-100: #FFEEDF;
87
+ --orange-200: #FFD9B8;
88
+ --orange-300: #FFC59C;
89
+ --orange-400: #FF9755;
90
+ --orange-500: #FF7A2E;
91
+ --orange-600: #E8631A;
92
+ --orange-700: #B84812;
93
+ --orange-800: #8A340A;
94
+
95
+ // -----------------------------------------------------------------------
96
+ // Green — success
97
+ // -----------------------------------------------------------------------
98
+ --green-50: #ECFAEE;
99
+ --green-100: #D6F3DB;
100
+ --green-200: #BBE8C2;
101
+ --green-300: #8EDA9A;
102
+ --green-400: #4FC368;
103
+ --green-500: #22A33A;
104
+ --green-600: #1B8A2E;
105
+ --green-700: #156822;
106
+
107
+ // -----------------------------------------------------------------------
108
+ // Yellow — attention / warning
109
+ // -----------------------------------------------------------------------
110
+ --yellow-50: #FFFBEA;
111
+ --yellow-100: #FEF8DC;
112
+ --yellow-200: #FCEFA1;
113
+ --yellow-300: #F9E25F;
114
+ --yellow-400: #F4D43A;
115
+ --yellow-500: #E0BE00;
116
+ --yellow-600: #B89B00;
117
+ --yellow-700: #8A7400;
118
+
119
+ // -----------------------------------------------------------------------
120
+ // Red — HSL-channel-split for halo derivation. #D81F1F ≈ hsl(0, 75%, 49%).
121
+ // The default theme maps --error onto this ramp (see styles/_tokens.scss).
122
+ // -----------------------------------------------------------------------
123
+ --red-h: 0;
124
+ --red-s: 75%;
125
+ --red-l: 49%;
126
+
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%);
135
+
136
+ // -----------------------------------------------------------------------
137
+ // Sky — info role ramp. A brighter, cooler blue (#3B82F6) kept distinct
138
+ // from the primary --blue ramp (#2F54FF) so "informational" reads apart
139
+ // from "primary action".
140
+ // -----------------------------------------------------------------------
141
+ --sky-50: #EFF6FF;
142
+ --sky-100: #DBEAFE;
143
+ --sky-200: #BFDBFE;
144
+ --sky-300: #93C5FD;
145
+ --sky-400: #60A5FA;
146
+ --sky-500: #3B82F6;
147
+ --sky-600: #2563EB;
148
+ --sky-700: #1D4ED8;
149
+
150
+ // -----------------------------------------------------------------------
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.
155
+ // -----------------------------------------------------------------------
156
+ --shadow-tint-h: 225;
157
+ --shadow-tint-s: 39%;
158
+ --shadow-tint-l: 7%;
159
+ }
@@ -0,0 +1,274 @@
1
+ // =========================================================================
2
+ // Default theme — Tier 2 semantic role ASSIGNMENTS (light).
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.
10
+ //
11
+ // Tier 3 (component-scoped tokens, geometry, typography, motion) lives in
12
+ // styles/_tokens.scss and is theme-independent.
13
+ // =========================================================================
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
+
35
+ /* Surfaces — six-stop scale.
36
+ · canvas : page background
37
+ · default : cards, sheets, toolbars (the “paper”)
38
+ · secondary : hover lanes, table heads, addons, code blocks
39
+ · overlay : modals, drawers, popovers (paired with scrim)
40
+ · tint : brand-tinted (active rows, sidenav active item)
41
+ · inverted : dark-on-light callouts, toasts
42
+ Conceptual elevation is communicated by SHADOW tokens,
43
+ not by surface tokens. Hover/pressed states use
44
+ --opacity-state-* overlays on top of the underlying surface. */
45
+ --surface-canvas: var(--neutral-50);
46
+ --surface-default: var(--neutral-0);
47
+ --surface-secondary: var(--neutral-100);
48
+ --surface-overlay: var(--neutral-0);
49
+ --surface-inverted: var(--neutral-900);
50
+ --surface-inverted-hover: var(--neutral-800);
51
+ --surface-tint: var(--blue-50);
52
+ --surface-disabled: var(--neutral-100);
53
+ --surface-muted: var(--neutral-150);
54
+
55
+ /* Scrim — modal/drawer backdrop. Derived from inverted surface
56
+ so dark-mode override flips automatically. */
57
+ --surface-scrim: hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .55);
58
+
59
+ /* Visible-on-coloured-fill divider. The split-button right-hand chevron
60
+ sits ON the brand fill, so a transparent white hairline reads correctly
61
+ across any brand hue. Override per theme if the brand is too light. */
62
+ --btn-split-divider: hsla(0, 0%, 100%, .25);
63
+
64
+ /* Visited link — derives from --primary-muted by default so it sympathetically
65
+ shifts with the brand hue. Override (e.g. for financial dashboards that
66
+ want neutral-grey visited links) by setting this token directly. */
67
+ --link-visited: hsl(var(--primary-h) calc(var(--primary-s) * 0.6) 45%);
68
+
69
+ /* Borders */
70
+ --border-default: var(--neutral-200);
71
+ --border-strong: var(--neutral-300);
72
+ --border-subtle: var(--neutral-150);
73
+ --border-divider: var(--neutral-150);
74
+ --border-inverted: var(--neutral-700);
75
+ --border-disabled: var(--neutral-150);
76
+
77
+ /* Text */
78
+ --text-primary: var(--neutral-900);
79
+ --text-secondary: var(--neutral-700);
80
+ --text-tertiary: var(--neutral-500);
81
+ --text-muted: var(--neutral-400);
82
+ --text-disabled: var(--neutral-400);
83
+ --text-inverted: var(--neutral-0);
84
+ /* Text overlaid on arbitrary imagery (camera feeds, photos) — pairs a
85
+ 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);
88
+ --text-link: var(--blue-700);
89
+ --text-on-primary: var(--neutral-0);
90
+ --text-on-success: var(--neutral-0);
91
+ /* Warning fill is yellow (--yellow-400 #F4D43A) — white text on it is only
92
+ ~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);
95
+
96
+ /* Icons */
97
+ --icon-default: var(--neutral-500);
98
+ --icon-strong: var(--neutral-700);
99
+ --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);
110
+
111
+ /* Semantic → palette bridge. The lib knows colours by PURPOSE; the theme
112
+ (themes/default/_theme.scss) ships the concrete ramps + HSL channels. This
113
+ is the one place that maps semantic roles onto a concrete palette. A brand
114
+ re-skins by overriding either the palette (--blue-*, --red-*) or these
115
+ role tokens directly.
116
+ Channel mappings let hsla() halos/tints compose from the same hue source. */
117
+ --primary-h: var(--blue-h);
118
+ --primary-s: var(--blue-s);
119
+ --primary-l: var(--blue-l);
120
+ --error-h: var(--red-h);
121
+ --error-s: var(--red-s);
122
+ --error-l: var(--red-l);
123
+
124
+ /* Primary roles.
125
+ Hover / pressed / selected-hover are DERIVED from --primary at runtime via
126
+ color-mix() so a brand swap (overriding only --primary) propagates to every
127
+ interaction state automatically. A brand that wants a non-derived hover
128
+ hue can still override these tokens directly.
129
+ Step sizes (12% / 24% darker) mirror the blue 600→700→800 ramp. */
130
+ --primary: var(--blue-600);
131
+ --primary-hover: color-mix(in srgb, var(--primary) 88%, black);
132
+ --primary-pressed: color-mix(in srgb, var(--primary) 76%, black);
133
+ --primary-selected-hover: color-mix(in srgb, var(--primary) 88%, black);
134
+ --primary-subtle: var(--blue-50);
135
+ --primary-muted: var(--blue-100);
136
+ --primary-muted-strong: var(--blue-200);
137
+ --primary-strong: var(--blue-700);
138
+ --primary-on: var(--neutral-0);
139
+
140
+ /* Accent / secondary role — composed from --secondary-h/s/l channels.
141
+ Channels default to brand values so unbranded apps render identical to
142
+ --primary. Override --secondary-h/s/l per-brand (or via the tweaks panel)
143
+ to introduce a true secondary hue — --secondary recomposes automatically.
144
+ Hover/pressed derive from --secondary so any hue change propagates. */
145
+ --secondary-h: var(--primary-h);
146
+ --secondary-s: var(--primary-s);
147
+ --secondary-l: var(--primary-l);
148
+
149
+ --secondary: hsl(var(--secondary-h), var(--secondary-s), var(--secondary-l));
150
+ --secondary-hover: color-mix(in srgb, var(--secondary) 88%, black);
151
+ --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-subtle: hsl(var(--secondary-h), 100%, 97%);
154
+ --secondary-muted: hsl(var(--secondary-h), 95%, 94%);
155
+ --secondary-on: var(--primary-on);
156
+
157
+ /* Tertiary — third brand hue. Used by the tertiary button variant and
158
+ tonal accents (decorative chips, hierarchy tones, indicators). Mirrors
159
+ the --primary / --secondary token-family shape: HSL channels for halo
160
+ composition, hover/pressed via color-mix, subtle/muted ramps for tonal
161
+ surfaces. Defaults to --primary channels so single-hue themes render
162
+ identical to primary. Multi-hue brands override --tertiary-h/s/l (or
163
+ --tertiary directly for non-HSL values). */
164
+ --tertiary-h: var(--primary-h);
165
+ --tertiary-s: var(--primary-s);
166
+ --tertiary-l: var(--primary-l);
167
+
168
+ --tertiary: hsl(var(--tertiary-h), var(--tertiary-s), var(--tertiary-l));
169
+ --tertiary-hover: color-mix(in srgb, var(--tertiary) 88%, black);
170
+ --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-subtle: hsl(var(--tertiary-h), 100%, 97%);
173
+ --tertiary-muted: hsl(var(--tertiary-h), 95%, 94%);
174
+ --tertiary-on: var(--primary-on);
175
+
176
+ /* Status roles.
177
+ -hover / -pressed are derived from the base role via the same color-mix()
178
+ rule as --primary-hover. Override per role if a brand needs a non-derived
179
+ hue. -text / -border / -subtle / -muted stay as ramp picks because their
180
+ contrast targets are tuned, not procedural. */
181
+ --success: var(--green-500);
182
+ --success-hover: color-mix(in srgb, var(--success) 88%, black);
183
+ --success-subtle: var(--green-50);
184
+ --success-muted: var(--green-100);
185
+ --success-border: var(--green-200);
186
+ --success-text: var(--green-700);
187
+
188
+ --warning: var(--yellow-400);
189
+ --warning-hover: color-mix(in srgb, var(--warning) 88%, black);
190
+ --warning-subtle: var(--yellow-50);
191
+ --warning-muted: var(--yellow-100);
192
+ --warning-border: var(--yellow-200);
193
+ --warning-text: var(--yellow-700);
194
+
195
+ --error: var(--red-500);
196
+ --error-hover: color-mix(in srgb, var(--error) 88%, black);
197
+ --error-pressed: color-mix(in srgb, var(--error) 76%, black);
198
+ --error-subtle: var(--red-50);
199
+ --error-muted: var(--red-100);
200
+ --error-border: var(--red-200);
201
+ --error-text: var(--red-600);
202
+
203
+ --info: var(--sky-500);
204
+ --info-hover: color-mix(in srgb, var(--info) 88%, black);
205
+ --info-subtle: var(--sky-50);
206
+ --info-muted: var(--sky-100);
207
+ --info-border: var(--sky-200);
208
+ --info-text: var(--sky-700);
209
+
210
+ /* Status pills (event lifecycle).
211
+ DECISION: --status-confirmed-* chains through --primary-* so a
212
+ brand re-skin moves Confirmed with it. The other lifecycle
213
+ stages are deliberately neutral/green/dark to keep semantic
214
+ distance from brand. */
215
+ /* Status pills — canonical (single system).
216
+ Legacy classes `.badge--active` (single) and ad-hoc semantic
217
+ 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-subtle);
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);
226
+
227
+ /* Focus rings.
228
+ Outer halos use hsla() composed from brand/danger HSL channels
229
+ so re-skinning --primary-h propagates into focus state.
230
+ The inner halo reads --surface-canvas so the ring stays visible
231
+ in both light and dark themes (canvas flips, ring adapts). */
232
+ --focus-ring-inner: var(--surface-canvas);
233
+ --focus-ring-outer-h: var(--primary-h);
234
+ --focus-ring-outer-s: var(--primary-s);
235
+ --focus-ring-outer-l: var(--primary-l);
236
+ --focus-ring-outer-alpha: 1;
237
+
238
+ --focus-ring:
239
+ 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));
241
+ --focus-ring-inverted:
242
+ 0 0 0 2px var(--neutral-900),
243
+ 0 0 0 4px var(--blue-300);
244
+ --focus-ring-error:
245
+ 0 0 0 2px var(--focus-ring-inner),
246
+ 0 0 0 4px hsla(var(--error-h), var(--error-s), var(--error-l), 1);
247
+
248
+ /* Field focus — coloured halo only (no double-ring). Derived from
249
+ brand HSL so it tracks rebrand. Alpha is a token so dark themes
250
+ can boost it for legibility. */
251
+ --focus-field-alpha: .22;
252
+ --focus-field:
253
+ 0 0 0 3px hsla(var(--primary-h), var(--primary-s), var(--primary-l), var(--focus-field-alpha));
254
+ --focus-field-error:
255
+ 0 0 0 3px hsla(var(--error-h), var(--error-s), var(--error-l), var(--focus-field-alpha));
256
+ }
257
+
258
+ /* Brand-derived neutrals — opt-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);
274
+ }