ptech-tokens 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.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # ptech-tokens
2
+
3
+ Design tokens and foundation utilities for the ptech design system. Ships as a separate workspace package so the token contract is version-controlled and consumable independently of the React component library.
4
+
5
+ Two files form the public contract: `tokens.css` sets primitive values and light-mode overrides as **unlayered** `:root` declarations (and a `[data-theme="light"]` selector block); `foundation.css` contributes a small set of framework-agnostic utility classes in `@layer utilities` and `@layer components`. Consumers import both via CSS `@import`, in the same pattern used by `packages/library/src/index.css`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add ptech-tokens
11
+ ```
12
+
13
+ Inside this monorepo, the library depends on it via `"ptech-tokens": "workspace:*"`. Downstream hosts pin the version through their own `package.json` or use the workspace alias when co-located.
14
+
15
+ ---
16
+
17
+ ## Consumer contract
18
+
19
+ ### 1. Import order and entry points
20
+
21
+ The package exposes three CSS entry points:
22
+
23
+ | Import | Contents |
24
+ |---|---|
25
+ | `ptech-tokens/index.css` | Re-exports `tokens.css` + `foundation.css` in one shot |
26
+ | `ptech-tokens/tokens.css` | `:root` primitives and `[data-theme="light"]` overrides only |
27
+ | `ptech-tokens/foundation.css` | Utility classes only |
28
+
29
+ **Tokens must load before Tailwind** so that `@theme inline` mappings in the library resolve correctly. The library's own `src/index.css` follows this order:
30
+
31
+ ```css
32
+ @import "tailwindcss";
33
+ @import "ptech-tokens/tokens.css";
34
+ @import "ptech-tokens/foundation.css";
35
+ ```
36
+
37
+ **Module Federation hosts** that consume the library via MF do not need to import these files manually — the library's CSS is injected at remote load time via `cssInjection: "wrapper"` (rsbuild config). The tokens arrive as part of that injection.
38
+
39
+ **Hosts that want to re-theme** without replacing the library should apply CSS custom property overrides on `:root` (or a more-specific selector) in a stylesheet that loads after the library injects:
40
+
41
+ ```css
42
+ /* host-overrides.css — loaded after ptechlibraryv2 remote */
43
+ :root {
44
+ --color-ps-aqua: #00b8c7;
45
+ }
46
+
47
+ [data-theme="light"] {
48
+ --color-ps-aqua: #0090a0;
49
+ }
50
+ ```
51
+
52
+ Because `ptech-tokens` declarations are **unlayered**, they already outrank Tailwind's `@layer theme` defaults and the library's `@layer` recipes. Any unlayered override placed later in source order wins by cascade precedence.
53
+
54
+ > **⚠️ Source order is not guaranteed under Module Federation.** With `cssInjection: "wrapper"` the library injects its CSS (including these `:root` token declarations) at *remote-load time* — i.e. **after** a host's static `<link rel="stylesheet">` has already been parsed. Two unlayered `:root` rules at equal specificity resolve by source order, so a plain `:root { … }` override in a static host stylesheet can **lose** to the later-injected library copy.
55
+ >
56
+ > **Robust override strategies (pick one):**
57
+ > 1. **Higher-specificity selector (recommended, no JS).** Specificity beats source order, so a selector more specific than `:root` always wins regardless of injection timing:
58
+ > ```css
59
+ > html[data-theme="light"] { --color-ps-aqua: #0090a0; }
60
+ > html.my-app { --color-ps-aqua: #00b8c7; } /* add a class/attr to <html> */
61
+ > ```
62
+ > 2. **Programmatic injection** — append the override `<style>`/`<link>` *after* the remote module resolves (e.g. in the `.then()` of your dynamic `import()` of the remote).
63
+ > 3. **`!important`** on the override declarations (blunt but reliable).
64
+ >
65
+ > The plain-`:root` pattern below is correct **only** when you control bundling order (the non-MF pipeline in §1) so your overrides are emitted last.
66
+
67
+ For hosts that manage their own CSS pipeline (not MF injection), the supported load order is:
68
+
69
+ ```ts
70
+ import "ptech-tokens/index.css"; // 1. token primitives (unlayered :root)
71
+ import "ptechlibraryv2/dist/styles.css"; // 2. library recipes
72
+ import "./host-overrides.css"; // 3. host overrides — last wins
73
+ ```
74
+
75
+ ---
76
+
77
+ ### 2. Override patterns
78
+
79
+ #### Primitives vs. semantic aliases
80
+
81
+ `tokens.css` is structured in two layers of abstraction:
82
+
83
+ - **Brand and surface primitives** (`--color-ps-*`) — raw colour, radius, shadow, z-index, and typography values.
84
+ - **Component-scoped semantic tokens** (`--color-badge-*`, `--color-messagebar-*`, `--color-toast-*`) — computed via `color-mix()` from the primitives; they automatically update when a primitive changes.
85
+
86
+ To restyle every component that uses the aqua brand colour:
87
+
88
+ ```css
89
+ /* Override the primitive — all semantic tokens derived from it update automatically */
90
+ :root {
91
+ --color-ps-aqua: #00b8c7;
92
+ }
93
+
94
+ [data-theme="light"] {
95
+ --color-ps-aqua: #0090a0;
96
+ }
97
+ ```
98
+
99
+ To adjust a single component variant without touching the primitive:
100
+
101
+ ```css
102
+ /* Surgical override of a Badge semantic token */
103
+ :root {
104
+ --color-badge-info-soft: color-mix(in srgb, #00b8c7 18%, transparent);
105
+ }
106
+ ```
107
+
108
+ > **Warning:** Do not globally replace primitives (`--color-ps-bg`, `--color-ps-surface`, etc.) without auditing all `color-mix()` expressions that reference them. Those expressions rely on the primitive values to produce correct contrast ratios — an incompatible replacement can silently break border, shadow, and text legibility across all components.
109
+
110
+ ---
111
+
112
+ ### 3. Light / dark themes and scoped overrides
113
+
114
+ #### Default theme: dark
115
+
116
+ `:root` in `tokens.css` declares `color-scheme: dark` and sets dark-mode values for all tokens. This is the baseline — no attribute is needed to enable dark mode.
117
+
118
+ #### Light mode: `[data-theme="light"]`
119
+
120
+ Apply the attribute to `<html>` (or any ancestor element) to switch to light mode:
121
+
122
+ ```html
123
+ <html data-theme="light">
124
+ ```
125
+
126
+ The `[data-theme="light"]` block in `tokens.css` redefines all surface, text, brand, status, and shadow tokens for light surfaces. Shadow values are independently tuned for light backgrounds (lower opacity, lighter base colour). The `color-scheme: light` declaration inside that block tells the browser to apply light-mode scrollbar and system UI styles.
127
+
128
+ `prefers-color-scheme` is **not used** by `tokens.css` — theme switching is exclusively attribute-driven via `[data-theme="light"]`. This is intentional: it keeps the host app in full control of the active theme without relying on OS-level media queries.
129
+
130
+ #### Fonts
131
+
132
+ Typography is token-driven like every other primitive:
133
+
134
+ | Token | Default stack |
135
+ |---|---|
136
+ | `--font-sans` | `"Space Grotesk", ui-sans-serif, system-ui` |
137
+ | `--font-mono` | `"JetBrains Mono", ui-monospace, monospace` |
138
+ | `--font-brand` | Alias for the active brand display stack |
139
+ | `--font-body` | Alias for the active brand body stack |
140
+
141
+ **This package never ships font binaries** — it only declares the family stacks. Loading the actual files (`@font-face` or a `<link>` to a font CDN) is the host's responsibility; if a family isn't loaded, rendering falls back down the stack gracefully.
142
+
143
+ To override the fonts in a host app, swap the variable (use an `html` selector so it beats the library's injected `:root` copy regardless of Module Federation timing — same rule as colour overrides):
144
+
145
+ ```css
146
+ /* host stylesheet */
147
+ @font-face {
148
+ font-family: "Sansa Pro";
149
+ src: url("/fonts/SansaPro-Regular.woff2") format("woff2");
150
+ font-weight: 400;
151
+ font-display: swap;
152
+ }
153
+
154
+ html {
155
+ --font-sans: "Sansa Pro", ui-sans-serif, system-ui;
156
+ }
157
+ ```
158
+
159
+ Brand blocks may pre-declare brand font stacks with safe fallbacks. For Control Union, `--font-brand` uses Sansa Pro and `--font-body` uses Ubuntu first, then Calibri/system fallbacks. The host still owns the `@font-face` files.
160
+
161
+ #### Brands (multi-tenant theming)
162
+
163
+ The default brand (no attribute) is **Peterson Solution**. Additional brands are shipped as attribute blocks that override only the **brand primitives** — every semantic token (badge, messagebar, toast, focus ring, state colours) derives via `color-mix()` and re-themes automatically:
164
+
165
+ ```html
166
+ <html data-brand="cu"> <!-- Control Union, dark -->
167
+ <html data-brand="cu" data-theme="light"> <!-- Control Union, light -->
168
+ ```
169
+
170
+ `data-brand` and `data-theme` are independent axes and combine freely. To add a new brand, add a `[data-brand="x"]` block overriding the accent primitives (and a `[data-brand="x"][data-theme="light"]` block if the brand has its own light palette). No component or Tailwind changes are needed.
171
+
172
+ The built-in Control Union brand uses primitives extracted from `Control Union Style Guide 2023_v2.1.pdf`: Grey `#799495`, Cyan `#3eb2ed`, Dark Blue `#1b1e42`, Dark Grey `#4f6566`, and the light-blue gradient `#008bcb -> #3eb2ed`.
173
+
174
+ #### Scoped re-theming
175
+
176
+ Any ancestor element can carry `[data-theme="light"]` to re-theme only that subtree:
177
+
178
+ ```html
179
+ <!-- Dark-mode app -->
180
+ <div class="app">
181
+ <!-- Force a specific panel to render in light mode -->
182
+ <aside data-theme="light" class="sidebar">
183
+ <!-- All ptech-tokens variables resolve to light values here -->
184
+ </aside>
185
+ </div>
186
+ ```
187
+
188
+ Custom theme variants follow the same pattern — define a new attribute selector that overrides the relevant tokens:
189
+
190
+ ```css
191
+ [data-theme="high-contrast"] {
192
+ --color-ps-text: #ffffff;
193
+ --color-ps-bg: #000000;
194
+ --color-ps-border: #ffffff;
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Internals
201
+
202
+ ### File map
203
+
204
+ | File | Layer | Purpose |
205
+ |---|---|---|
206
+ | `src/tokens.css` | Unlayered `:root` | All CSS custom properties — dark defaults + `[data-theme="light"]` overrides |
207
+ | `src/foundation.css` | `@layer utilities`, `@layer components` | Reusable utility classes built on token vars |
208
+ | `src/index.css` | — | `@import` barrel: `tokens.css` then `foundation.css` |
209
+ | `src/index.ts` | - | Typed token name map for TypeScript consumers |
210
+
211
+ ### Token naming conventions
212
+
213
+ All tokens use a consistent prefix scheme derived from the actual variable names in `tokens.css`:
214
+
215
+ | Prefix | Category | Examples |
216
+ |---|---|---|
217
+ | `--color-ps-{name}` | Brand primitives | `--color-ps-aqua`, `--color-ps-indigo`, `--color-ps-yellow` |
218
+ | `--color-cu-{name}` | Control Union source palette | `--color-cu-cyan`, `--color-cu-dark-blue`, `--color-cu-grey`, `--color-cu-dark-grey`, `--color-cu-gradient-start`, `--color-cu-gradient-end` |
219
+ | `--color-ps-{role}` | Surface roles | `--color-ps-bg`, `--color-ps-surface`, `--color-ps-section`, `--color-ps-elevated`, `--color-ps-card`, `--color-ps-border`, `--color-ps-border-hover` |
220
+ | `--color-ps-row{,-hover}` | Flat row surfaces | Faint translucent fill for borderless list/settings rows |
221
+ | `--color-ps-text{-dim,-subtle}` | Text roles | `--color-ps-text`, `--color-ps-text-dim`, `--color-ps-text-subtle` |
222
+ | `--color-ps-{status}` | Status primitives | `--color-ps-success`, `--color-ps-warning`, `--color-ps-error` |
223
+ | `--color-focus-ring` | Focus | `--color-focus-ring` (computed via `color-mix`) |
224
+ | `--focus-ring-width`, `--focus-ring-offset` | Focus sizing | — |
225
+ | `--color-surface-sink`, `--color-brand-shade` | Depth anchors | Tone surfaces/tiles mix toward (theme-aware) / fixed brand shade |
226
+ | `--color-state-{hover,selected}`, `--opacity-disabled` | Interactive state | Hover/selected overlays + disabled opacity |
227
+ | `--font-{sans,mono,brand,body}` | Typography | `--font-sans` ("Space Grotesk"), `--font-mono` ("JetBrains Mono"), brand/body aliases |
228
+ | `--font-size-{xs…2xl}`, `--line-height-{xs…2xl}` | Type scale | Namespaced (not Tailwind's `--text-*`/`--leading-*`) — additive, consume via `var()` |
229
+ | `--space-{3xs…2xl}` | Spacing scale | 2 / 4 / 8 / 12 / 16 / 24 / 32 / 48 px |
230
+ | `--duration-{fast,normal,slow}`, `--ease-{standard,emphasized}` | Motion | 140 / 200 / 320 ms + easing curves |
231
+ | `--radius-{xs,sm,md,lg,xl,2xl,pill}` | Radius scale | 2 / 4 / 6 / 8 / 12 / 16 / 9999 px — deliberately matches Tailwind v4's `rounded-*` scale (the unlayered tokens override Tailwind's same-name theme vars, so utilities and tokens agree) |
232
+ | `--shadow-{sm,md,lg,inset-highlight,glow,glass}` | Elevation | Auto-retuned in light mode |
233
+ | `--shadow-ps-card{,-hover}` | Card semantics | Flat & lightweight panel/tile shadows; the standard for cards |
234
+ | `--z-{base,dropdown,sticky,overlay,drawer,modal,popover,tooltip,toast}` | Z-index | 0 → 80 |
235
+ | `--color-badge-{variant}-{role}` | Badge semantics | e.g. `--color-badge-info-soft`, `--color-badge-error-text` |
236
+ | `--color-messagebar-{variant}-{role}` | MessageBar semantics | e.g. `--color-messagebar-success-icon` |
237
+ | `--color-toast-{variant}-accent` | Toast semantics | e.g. `--color-toast-warning-accent` |
238
+
239
+ Component-scoped tokens (`--color-badge-*`, `--color-messagebar-*`, `--color-toast-*`) are computed with `color-mix(in srgb, ...)` from the brand and status primitives. They inherit theme switches automatically.
240
+
241
+ ### Foundation utility classes
242
+
243
+ Classes defined in `foundation.css`:
244
+
245
+ | Class | Layer | Description |
246
+ |---|---|---|
247
+ | `.scroll-slim` | utilities | Thin scrollbar, transparent until hover; uses `--color-ps-text-subtle` |
248
+ | `.line-clamp-2` | utilities | Two-line webkit text truncation |
249
+ | `.divider-hairline` | utilities | Gradient hairline using `--color-ps-border` |
250
+ | `.ps-backdrop` | utilities | Modal/overlay backdrop wash using `--color-ps-bg` |
251
+ | `.focus-ring` | components | `:focus-visible` ring driven by `--color-focus-ring` and `--focus-ring-width` |
252
+ | `.ps-card-solid` | components | Canonical single-layer card: solid `--color-ps-surface`, token border, `--radius-2xl` (16px), soft `--shadow-ps-card` — no gradient, no glow. One bordered card per region; never nest cards |
253
+ | `.ps-section-wrap` | components | Tinted `--color-ps-section` container (radius `--radius-2xl`, padding `--space-lg`) that groups cards/rows |
254
+ | `.ps-row` | components | Borderless flat list/settings row: `--color-ps-row` fill, `--radius-lg` (8px), lightens to `--color-ps-row-hover` on hover/focus-within. Layout (flex, padding) stays with the consumer |
255
+
256
+ ### Adding or removing tokens
257
+
258
+ **Adding a new primitive:** Add it to `tokens.css` under `:root` and, if it needs a light-mode value, also under `[data-theme="light"]`. Bump `patch` (new token, no breaking change).
259
+
260
+ **Adding a new semantic/component token:** Add it to `tokens.css` using `color-mix()` from existing primitives. Add a matching entry to `packages/library/src/index.css` under `@theme inline` if Tailwind utility classes are needed. Bump `minor`.
261
+
262
+ **Removing or renaming any shipped token:** This is a breaking change. Bump `major`, update the consuming library, and communicate to downstream host teams.
263
+
264
+ Do not add utility classes that have nothing to do with tokens (e.g., pure layout helpers). `foundation.css` is for recipes that depend on token variables.
265
+
266
+ ---
267
+
268
+ ## Versioning
269
+
270
+ Semver. Adding new tokens = minor. Renaming or removing a shipped token = major. Value tweaks that preserve intent (e.g., adjusting a shade) = patch. The consuming library depends on `"ptech-tokens": "workspace:*"` inside the monorepo; downstream hosts should pin a specific version range.
271
+
272
+ ---
273
+
274
+ ## Do / don't
275
+
276
+ - **Do** import `tokens.css` before Tailwind when setting up a host app CSS pipeline.
277
+ - **Do** override semantic tokens (`--color-badge-*`, `--color-messagebar-*`) for component-level adjustments; they are the stable surface of the API.
278
+ - **Do** apply `[data-theme="light"]` to `<html>` or a container element to switch or scope themes.
279
+ - **Do** use `foundation.css` utility classes (`.scroll-slim`, `.focus-ring`, etc.) in host app markup — they are part of the consumer API.
280
+ - **Do** bump the package version on any token change (see Versioning above).
281
+ - **Don't** modify `tokens.css` or `foundation.css` in host apps — override via your own stylesheet instead.
282
+ - **Don't** use `prefers-color-scheme` to gate token imports; `tokens.css` does not use it. Theme is controlled by `[data-theme="light"]` only.
283
+ - **Don't** replace a brand primitive globally without auditing all `color-mix()` expressions that depend on it.
284
+ - **Don't** hardcode hex colours in library components — always reference a CSS custom property from this package.
285
+ - **Don't** add new token variables directly to `packages/library/src/index.css`; tokens belong in this package.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "ptech-tokens",
3
+ "version": "0.4.0",
4
+ "description": "Design tokens + foundation utilities for ptech design system",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "files": [
9
+ "src",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ },
17
+ "./index.css": "./src/index.css",
18
+ "./tokens.css": "./src/tokens.css",
19
+ "./foundation.css": "./src/foundation.css"
20
+ },
21
+ "sideEffects": [
22
+ "*.css"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "design-tokens",
29
+ "css",
30
+ "tailwind"
31
+ ],
32
+ "license": "MIT"
33
+ }
@@ -0,0 +1,102 @@
1
+ /* =========================================================
2
+ ptech-tokens — foundation utilities
3
+ Shared between library and host. Pure recipes using tokens.
4
+ ========================================================= */
5
+
6
+ @layer utilities {
7
+ /* ---------- Scrollbar (slim, appears on hover) ---------- */
8
+ .scroll-slim {
9
+ scrollbar-width: thin;
10
+ scrollbar-color: transparent transparent;
11
+ }
12
+
13
+ .scroll-slim::-webkit-scrollbar {
14
+ width: 6px;
15
+ height: 6px;
16
+ }
17
+
18
+ .scroll-slim::-webkit-scrollbar-track {
19
+ background: transparent;
20
+ }
21
+
22
+ .scroll-slim::-webkit-scrollbar-thumb {
23
+ background: transparent;
24
+ border-radius: 999px;
25
+ }
26
+
27
+ .scroll-slim:hover {
28
+ scrollbar-color: color-mix(in srgb, var(--color-ps-text-subtle) 55%, transparent) transparent;
29
+ }
30
+
31
+ .scroll-slim:hover::-webkit-scrollbar-thumb {
32
+ background: color-mix(in srgb, var(--color-ps-text-subtle) 55%, transparent);
33
+ }
34
+
35
+ /* ---------- Text truncation ---------- */
36
+ .line-clamp-2 {
37
+ display: -webkit-box;
38
+ overflow: hidden;
39
+ -webkit-box-orient: vertical;
40
+ -webkit-line-clamp: 2;
41
+ }
42
+
43
+ /* ---------- Divider hairline ---------- */
44
+ .divider-hairline {
45
+ height: 1px;
46
+ background: linear-gradient(90deg,
47
+ transparent,
48
+ color-mix(in srgb, var(--color-ps-border) 55%, transparent),
49
+ transparent);
50
+ }
51
+
52
+ /* ---------- Modal backdrop ---------- */
53
+ .ps-backdrop {
54
+ background: linear-gradient(180deg,
55
+ color-mix(in srgb, var(--color-ps-bg) 65%, transparent) 0%,
56
+ color-mix(in srgb, var(--color-ps-bg) 78%, transparent) 100%);
57
+ background-color: color-mix(in srgb, var(--color-ps-bg) 70%, transparent);
58
+ }
59
+ }
60
+
61
+ @layer components {
62
+ /* ---------- Focus ring helper ---------- */
63
+ .focus-ring {
64
+ outline: none;
65
+ }
66
+
67
+ .focus-ring:focus-visible {
68
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--color-focus-ring);
69
+ }
70
+
71
+ /* ---------- Canonical card (single-layer panel/tile) ----------
72
+ Flat & lightweight standard: solid surface, token border, soft
73
+ shadow with a hairline inset highlight — no gradient, no glow.
74
+ One bordered card per view region; never nest cards. */
75
+ .ps-card-solid {
76
+ background-color: var(--color-ps-surface);
77
+ border: 1px solid var(--color-ps-border);
78
+ border-radius: var(--radius-2xl);
79
+ box-shadow: var(--shadow-inset-highlight), var(--shadow-ps-card);
80
+ }
81
+
82
+ /* ---------- Section wrap (tinted container grouping cards/rows) ---------- */
83
+ .ps-section-wrap {
84
+ background-color: var(--color-ps-section);
85
+ border-radius: var(--radius-2xl);
86
+ padding: var(--space-lg);
87
+ }
88
+
89
+ /* ---------- Flat list/settings row ----------
90
+ Borderless row with a faint fill; hover/focus-within lightens.
91
+ Layout (flex, padding, gaps) stays with the consumer. */
92
+ .ps-row {
93
+ background-color: var(--color-ps-row);
94
+ border-radius: var(--radius-lg);
95
+ transition: background-color var(--duration-fast) var(--ease-standard);
96
+ }
97
+
98
+ .ps-row:hover,
99
+ .ps-row:focus-within {
100
+ background-color: var(--color-ps-row-hover);
101
+ }
102
+ }
package/src/index.css ADDED
@@ -0,0 +1,2 @@
1
+ @import "./tokens.css";
2
+ @import "./foundation.css";
package/src/index.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ptech-tokens — typed token surface
3
+ *
4
+ * The source of truth for token *values* is `tokens.css` (unlayered `:root` custom
5
+ * properties, with `[data-theme="light"]` overrides). This module mirrors the token
6
+ * *names* as a typed, autocomplete-friendly map of `var(--…)` strings so TypeScript
7
+ * consumers can reference tokens without stringly-typed CSS variable names.
8
+ *
9
+ * Usage:
10
+ * import { token } from "ptech-tokens";
11
+ * const style = { color: token.color.text, borderRadius: token.radius.md };
12
+ *
13
+ * Keep this file in sync with `tokens.css` when adding or renaming a token.
14
+ * The CSS import is still required for the values to exist at runtime:
15
+ * import "ptech-tokens/tokens.css";
16
+ */
17
+
18
+ /** Wrap a CSS custom property name in a `var(--…)` reference. */
19
+ export const cssVar = (name: `--${string}`): string => `var(${name})`;
20
+
21
+ export const token = {
22
+ /** Brand, surface, text, status, focus, and component colours. */
23
+ color: {
24
+ // Brand primitives
25
+ aqua: "var(--color-ps-aqua)",
26
+ indigo: "var(--color-ps-indigo)",
27
+ yellow: "var(--color-ps-yellow)",
28
+ controlUnion: {
29
+ grey: "var(--color-cu-grey)",
30
+ cyan: "var(--color-cu-cyan)",
31
+ darkBlue: "var(--color-cu-dark-blue)",
32
+ darkGrey: "var(--color-cu-dark-grey)",
33
+ gradientStart: "var(--color-cu-gradient-start)",
34
+ gradientEnd: "var(--color-cu-gradient-end)",
35
+ white: "var(--color-cu-white)",
36
+ },
37
+ // Surface roles
38
+ bg: "var(--color-ps-bg)",
39
+ surface: "var(--color-ps-surface)",
40
+ section: "var(--color-ps-section)",
41
+ elevated: "var(--color-ps-elevated)",
42
+ card: "var(--color-ps-card)",
43
+ border: "var(--color-ps-border)",
44
+ borderHover: "var(--color-ps-border-hover)",
45
+ row: "var(--color-ps-row)",
46
+ rowHover: "var(--color-ps-row-hover)",
47
+ // Text roles
48
+ text: "var(--color-ps-text)",
49
+ textDim: "var(--color-ps-text-dim)",
50
+ textSubtle: "var(--color-ps-text-subtle)",
51
+ // Status primitives
52
+ success: "var(--color-ps-success)",
53
+ warning: "var(--color-ps-warning)",
54
+ error: "var(--color-ps-error)",
55
+ // On-accent (text/icon on brand/status fills)
56
+ onAccent: "var(--color-ps-on-accent)",
57
+ // Depth anchors
58
+ surfaceSink: "var(--color-surface-sink)",
59
+ brandShade: "var(--color-brand-shade)",
60
+ // Focus
61
+ focusRing: "var(--color-focus-ring)",
62
+ // Interactive state
63
+ stateHover: "var(--color-state-hover)",
64
+ stateSelected: "var(--color-state-selected)",
65
+ },
66
+
67
+ /** Font-family stacks. */
68
+ font: {
69
+ sans: "var(--font-sans)",
70
+ mono: "var(--font-mono)",
71
+ brand: "var(--font-brand)",
72
+ body: "var(--font-body)",
73
+ },
74
+
75
+ /** Font-size scale (paired with `lineHeight`). Namespaced to avoid Tailwind's `--text-*`. */
76
+ fontSize: {
77
+ xs: "var(--font-size-xs)",
78
+ sm: "var(--font-size-sm)",
79
+ md: "var(--font-size-md)",
80
+ lg: "var(--font-size-lg)",
81
+ xl: "var(--font-size-xl)",
82
+ "2xl": "var(--font-size-2xl)",
83
+ },
84
+
85
+ /** Line-height scale. */
86
+ lineHeight: {
87
+ xs: "var(--line-height-xs)",
88
+ sm: "var(--line-height-sm)",
89
+ md: "var(--line-height-md)",
90
+ lg: "var(--line-height-lg)",
91
+ xl: "var(--line-height-xl)",
92
+ "2xl": "var(--line-height-2xl)",
93
+ },
94
+
95
+ /** Spacing scale. */
96
+ space: {
97
+ "3xs": "var(--space-3xs)",
98
+ "2xs": "var(--space-2xs)",
99
+ xs: "var(--space-xs)",
100
+ sm: "var(--space-sm)",
101
+ md: "var(--space-md)",
102
+ lg: "var(--space-lg)",
103
+ xl: "var(--space-xl)",
104
+ "2xl": "var(--space-2xl)",
105
+ },
106
+
107
+ /** Corner radius scale. */
108
+ radius: {
109
+ xs: "var(--radius-xs)",
110
+ sm: "var(--radius-sm)",
111
+ md: "var(--radius-md)",
112
+ lg: "var(--radius-lg)",
113
+ xl: "var(--radius-xl)",
114
+ "2xl": "var(--radius-2xl)",
115
+ pill: "var(--radius-pill)",
116
+ },
117
+
118
+ /** Elevation / shadow scale. */
119
+ shadow: {
120
+ sm: "var(--shadow-sm)",
121
+ md: "var(--shadow-md)",
122
+ lg: "var(--shadow-lg)",
123
+ insetHighlight: "var(--shadow-inset-highlight)",
124
+ glow: "var(--shadow-glow)",
125
+ glass: "var(--shadow-glass)",
126
+ card: "var(--shadow-ps-card)",
127
+ cardHover: "var(--shadow-ps-card-hover)",
128
+ },
129
+
130
+ /** Z-index layer scale. */
131
+ z: {
132
+ base: "var(--z-base)",
133
+ dropdown: "var(--z-dropdown)",
134
+ sticky: "var(--z-sticky)",
135
+ overlay: "var(--z-overlay)",
136
+ drawer: "var(--z-drawer)",
137
+ modal: "var(--z-modal)",
138
+ popover: "var(--z-popover)",
139
+ tooltip: "var(--z-tooltip)",
140
+ toast: "var(--z-toast)",
141
+ },
142
+
143
+ /** Motion durations and easing curves. */
144
+ motion: {
145
+ durationFast: "var(--duration-fast)",
146
+ durationNormal: "var(--duration-normal)",
147
+ durationSlow: "var(--duration-slow)",
148
+ easeStandard: "var(--ease-standard)",
149
+ easeEmphasized: "var(--ease-emphasized)",
150
+ },
151
+
152
+ /** Focus ring sizing. */
153
+ focus: {
154
+ ring: "var(--color-focus-ring)",
155
+ width: "var(--focus-ring-width)",
156
+ offset: "var(--focus-ring-offset)",
157
+ },
158
+
159
+ /** Misc state. */
160
+ opacity: {
161
+ disabled: "var(--opacity-disabled)",
162
+ },
163
+ } as const;
164
+
165
+ /** A token group key (e.g. "color", "radius"). */
166
+ export type TokenGroup = keyof typeof token;
167
+
168
+ /** The full token map type. */
169
+ export type Tokens = typeof token;
package/src/tokens.css ADDED
@@ -0,0 +1,371 @@
1
+ /* =========================================================
2
+ ptech-tokens — design token primitives
3
+ These are UNLAYERED :root declarations.
4
+ When imported by a host, they win over library's @layer fallback.
5
+ ========================================================= */
6
+
7
+ :root {
8
+ color-scheme: dark;
9
+
10
+ /* ---------- Brand primitives ---------- */
11
+ --color-ps-aqua: #36a6a8;
12
+ --color-ps-indigo: #7579eb;
13
+ --color-ps-yellow: #f2c661;
14
+
15
+ /* ---------- Control Union brand primitives ----------
16
+ Source: Control Union Style Guide 2023 v2.1, Colour section. */
17
+ --color-cu-grey: #799495;
18
+ --color-cu-cyan: #3eb2ed;
19
+ --color-cu-dark-blue: #1b1e42;
20
+ --color-cu-dark-grey: #4f6566;
21
+ --color-cu-gradient-start: #008bcb;
22
+ --color-cu-gradient-end: #3eb2ed;
23
+ --color-cu-white: #ffffff;
24
+
25
+ /* ---------- Surface ---------- */
26
+ --color-ps-bg: #0f1229;
27
+ --color-ps-surface: #171a36;
28
+ /* Tinted container that groups cards/rows (dashboard sections, settings).
29
+ Sits between surface and elevated. */
30
+ --color-ps-section: #1a1f40;
31
+ --color-ps-elevated: #1f2348;
32
+ --color-ps-card: color-mix(in srgb, var(--color-ps-surface) 92%, var(--color-surface-sink));
33
+ --color-ps-border: #2a2f5a;
34
+ --color-ps-border-hover: #363d6a;
35
+
36
+ /* ---------- Flat row surfaces ----------
37
+ Faint fill for borderless list/settings rows ("flat row" pattern).
38
+ Translucent so they sit on bg, surface, or section alike. */
39
+ --color-ps-row: rgba(255, 255, 255, 0.03);
40
+ --color-ps-row-hover: rgba(255, 255, 255, 0.06);
41
+
42
+ /* ---------- Text ---------- */
43
+ --color-ps-text: #a9b1d6;
44
+ --color-ps-text-dim: #9ba3c7;
45
+ --color-ps-text-subtle: #7f89b0;
46
+
47
+ /* ---------- Status primitives ---------- */
48
+ --color-ps-success: #31d0a3;
49
+ --color-ps-warning: #f9b54c;
50
+ --color-ps-error: #ff6b6b;
51
+
52
+ /* ---------- On-accent ----------
53
+ Text/icon colour intended to sit ON a brand/status fill (aqua button, selected
54
+ day, timeline dot). Theme-invariant because the brand/status colours stay mid-tone
55
+ in both themes. Override if you re-brand to a light accent. */
56
+ --color-ps-on-accent: #f8fafc;
57
+
58
+ /* ---------- Depth anchors ---------- */
59
+ /* Tone that surfaces and filled tiles mix toward for depth. Theme-aware (see the
60
+ [data-theme="light"] block): deep in dark, light in light, so a filled tile stays
61
+ readable under the theme's text colour. Override to retune depth library-wide. */
62
+ --color-surface-sink: #0b0f25;
63
+ /* Fixed deep shade for brand-filled controls (e.g. primary button) so they stay rich
64
+ in both themes rather than washing out. */
65
+ --color-brand-shade: #0b1320;
66
+
67
+ /* ---------- Focus ----------
68
+ Bright, luminous ring per the design standard — lifted toward on-accent
69
+ (fully opaque) so it pops on dark surfaces instead of sinking into them. */
70
+ --color-focus-ring: color-mix(in srgb, var(--color-ps-aqua) 70%, var(--color-ps-on-accent));
71
+ --focus-ring-width: 2px;
72
+ --focus-ring-offset: 0px;
73
+
74
+ /* ---------- Typography ---------- */
75
+ --font-sans: "Space Grotesk", ui-sans-serif, system-ui;
76
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
77
+ --font-brand: var(--font-sans);
78
+ --font-body: var(--font-sans);
79
+
80
+ /* ---------- Radius scale ----------
81
+ Values match Tailwind v4's rounded-* scale on purpose: these unlayered
82
+ declarations override Tailwind's @layer theme vars of the same name, so
83
+ tokens and rounded-* utilities always agree. Moderate rounding per the
84
+ design standard: row/button=lg(8), card/icon-well=xl(12), panel=2xl(16). */
85
+ --radius-xs: 2px;
86
+ --radius-sm: 4px;
87
+ --radius-md: 6px;
88
+ --radius-lg: 8px;
89
+ --radius-xl: 12px;
90
+ --radius-2xl: 16px;
91
+ --radius-pill: 9999px;
92
+
93
+ /* ---------- Shadow / Elevation ---------- */
94
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.18);
95
+ --shadow-md: 0 10px 24px -14px rgba(0, 0, 0, 0.45);
96
+ --shadow-lg: 0 14px 32px -16px rgba(0, 0, 0, 0.55);
97
+ --shadow-inset-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
98
+ --shadow-glow: 0 12px 32px -12px color-mix(in srgb, var(--color-ps-aqua) 50%, transparent);
99
+ --shadow-glass:
100
+ inset 0 1px 2px rgba(255, 255, 255, 0.06),
101
+ 0 10px 28px -10px rgba(0, 0, 0, 0.45);
102
+ /* Card semantic shadows — flat & lightweight standard for panels/tiles. */
103
+ --shadow-ps-card: 0 2px 8px rgba(0, 0, 0, 0.12);
104
+ --shadow-ps-card-hover: 0 8px 24px -8px rgba(0, 0, 0, 0.35);
105
+
106
+ /* ---------- Z-index layer scale ---------- */
107
+ --z-base: 0;
108
+ --z-dropdown: 10;
109
+ --z-sticky: 20;
110
+ --z-overlay: 30;
111
+ --z-drawer: 40;
112
+ --z-modal: 50;
113
+ --z-popover: 60;
114
+ --z-tooltip: 70;
115
+ --z-toast: 80;
116
+
117
+ /* ---------- Spacing scale ---------- */
118
+ --space-3xs: 2px;
119
+ --space-2xs: 4px;
120
+ --space-xs: 8px;
121
+ --space-sm: 12px;
122
+ --space-md: 16px;
123
+ --space-lg: 24px;
124
+ --space-xl: 32px;
125
+ --space-2xl: 48px;
126
+
127
+ /* ---------- Font size / line-height scale ----------
128
+ Namespaced as --font-size-x and --line-height-x (deliberately NOT Tailwind's own
129
+ text or leading theme keys) so they stay additive and never override generated utilities. */
130
+ --font-size-xs: 0.75rem;
131
+ --line-height-xs: 1rem;
132
+ --font-size-sm: 0.875rem;
133
+ --line-height-sm: 1.25rem;
134
+ --font-size-md: 1rem;
135
+ --line-height-md: 1.5rem;
136
+ --font-size-lg: 1.125rem;
137
+ --line-height-lg: 1.75rem;
138
+ --font-size-xl: 1.375rem;
139
+ --line-height-xl: 1.875rem;
140
+ --font-size-2xl: 1.75rem;
141
+ --line-height-2xl: 2.25rem;
142
+
143
+ /* ---------- Motion (subtle, respects prefers-reduced-motion at usage site) ---------- */
144
+ --duration-fast: 140ms;
145
+ --duration-normal: 200ms;
146
+ --duration-slow: 320ms;
147
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
148
+ --ease-emphasized: cubic-bezier(0.2, 0, 0, 1.2);
149
+
150
+ /* ---------- Interactive state ---------- */
151
+ --opacity-disabled: 0.55;
152
+ --color-state-hover: color-mix(in srgb, var(--color-ps-text) 8%, transparent);
153
+ --color-state-selected: color-mix(in srgb, var(--color-ps-aqua) 16%, transparent);
154
+
155
+ /* ---------- Badge semantic tokens ----------
156
+ Standard badge look: soft tone tint + bright tone-coloured text — flat,
157
+ no dark surface mixes. solid0/solid1 kept for compat; both hold the
158
+ same flat value (stronger tint for the filled variant). */
159
+ --color-badge-neutral-soft: color-mix(in srgb, var(--color-ps-text) 8%, transparent);
160
+ --color-badge-neutral-solid0: color-mix(in srgb, var(--color-ps-text) 16%, transparent);
161
+ --color-badge-neutral-solid1: color-mix(in srgb, var(--color-ps-text) 16%, transparent);
162
+ --color-badge-neutral-border: color-mix(in srgb, var(--color-ps-text) 22%, transparent);
163
+ --color-badge-neutral-text: var(--color-ps-text-dim);
164
+
165
+ --color-badge-info-soft: color-mix(in srgb, var(--color-ps-aqua) 12%, transparent);
166
+ --color-badge-info-solid0: color-mix(in srgb, var(--color-ps-aqua) 24%, transparent);
167
+ --color-badge-info-solid1: color-mix(in srgb, var(--color-ps-aqua) 24%, transparent);
168
+ --color-badge-info-border: color-mix(in srgb, var(--color-ps-aqua) 32%, transparent);
169
+ --color-badge-info-text: color-mix(in srgb, var(--color-ps-aqua) 80%, var(--color-ps-text));
170
+
171
+ --color-badge-success-soft: color-mix(in srgb, var(--color-ps-success) 12%, transparent);
172
+ --color-badge-success-solid0: color-mix(in srgb, var(--color-ps-success) 24%, transparent);
173
+ --color-badge-success-solid1: color-mix(in srgb, var(--color-ps-success) 24%, transparent);
174
+ --color-badge-success-border: color-mix(in srgb, var(--color-ps-success) 32%, transparent);
175
+ --color-badge-success-text: color-mix(in srgb, var(--color-ps-success) 80%, var(--color-ps-text));
176
+
177
+ --color-badge-warning-soft: color-mix(in srgb, var(--color-ps-warning) 12%, transparent);
178
+ --color-badge-warning-solid0: color-mix(in srgb, var(--color-ps-warning) 24%, transparent);
179
+ --color-badge-warning-solid1: color-mix(in srgb, var(--color-ps-warning) 24%, transparent);
180
+ --color-badge-warning-border: color-mix(in srgb, var(--color-ps-warning) 32%, transparent);
181
+ --color-badge-warning-text: color-mix(in srgb, var(--color-ps-warning) 80%, var(--color-ps-text));
182
+
183
+ --color-badge-error-soft: color-mix(in srgb, var(--color-ps-error) 12%, transparent);
184
+ --color-badge-error-solid0: color-mix(in srgb, var(--color-ps-error) 24%, transparent);
185
+ --color-badge-error-solid1: color-mix(in srgb, var(--color-ps-error) 24%, transparent);
186
+ --color-badge-error-border: color-mix(in srgb, var(--color-ps-error) 32%, transparent);
187
+ --color-badge-error-text: color-mix(in srgb, var(--color-ps-error) 80%, var(--color-ps-text));
188
+
189
+ /* ---------- MessageBar semantic tokens ----------
190
+ Standard look: soft tone tint + tone-coloured text/icon, tinted border
191
+ as the attention signal — flat, no dark surface mixes. solid0/solid1
192
+ kept for compat; both hold the same flat value (filled variant). */
193
+ --color-messagebar-info-soft: color-mix(in srgb, var(--color-ps-aqua) 12%, transparent);
194
+ --color-messagebar-info-solid0: color-mix(in srgb, var(--color-ps-aqua) 24%, transparent);
195
+ --color-messagebar-info-solid1: color-mix(in srgb, var(--color-ps-aqua) 24%, transparent);
196
+ --color-messagebar-info-border: color-mix(in srgb, var(--color-ps-aqua) 32%, transparent);
197
+ --color-messagebar-info-text: color-mix(in srgb, var(--color-ps-aqua) 70%, var(--color-ps-text));
198
+ --color-messagebar-info-icon: color-mix(in srgb, var(--color-ps-aqua) 80%, var(--color-ps-text));
199
+
200
+ --color-messagebar-success-soft: color-mix(in srgb, var(--color-ps-success) 12%, transparent);
201
+ --color-messagebar-success-solid0: color-mix(in srgb, var(--color-ps-success) 24%, transparent);
202
+ --color-messagebar-success-solid1: color-mix(in srgb, var(--color-ps-success) 24%, transparent);
203
+ --color-messagebar-success-border: color-mix(in srgb, var(--color-ps-success) 32%, transparent);
204
+ --color-messagebar-success-text: color-mix(in srgb, var(--color-ps-success) 70%, var(--color-ps-text));
205
+ --color-messagebar-success-icon: color-mix(in srgb, var(--color-ps-success) 80%, var(--color-ps-text));
206
+
207
+ --color-messagebar-warning-soft: color-mix(in srgb, var(--color-ps-warning) 12%, transparent);
208
+ --color-messagebar-warning-solid0: color-mix(in srgb, var(--color-ps-warning) 24%, transparent);
209
+ --color-messagebar-warning-solid1: color-mix(in srgb, var(--color-ps-warning) 24%, transparent);
210
+ --color-messagebar-warning-border: color-mix(in srgb, var(--color-ps-warning) 32%, transparent);
211
+ --color-messagebar-warning-text: color-mix(in srgb, var(--color-ps-warning) 70%, var(--color-ps-text));
212
+ --color-messagebar-warning-icon: color-mix(in srgb, var(--color-ps-warning) 80%, var(--color-ps-text));
213
+
214
+ --color-messagebar-error-soft: color-mix(in srgb, var(--color-ps-error) 12%, transparent);
215
+ --color-messagebar-error-solid0: color-mix(in srgb, var(--color-ps-error) 24%, transparent);
216
+ --color-messagebar-error-solid1: color-mix(in srgb, var(--color-ps-error) 24%, transparent);
217
+ --color-messagebar-error-border: color-mix(in srgb, var(--color-ps-error) 32%, transparent);
218
+ --color-messagebar-error-text: color-mix(in srgb, var(--color-ps-error) 70%, var(--color-ps-text));
219
+ --color-messagebar-error-icon: color-mix(in srgb, var(--color-ps-error) 80%, var(--color-ps-text));
220
+
221
+ /* ---------- Toast semantic tokens ----------
222
+ Bright tone accents for the strip + icon, matching the badge text mix. */
223
+ --color-toast-info-accent: color-mix(in srgb, var(--color-ps-aqua) 80%, var(--color-ps-text));
224
+ --color-toast-success-accent: color-mix(in srgb, var(--color-ps-success) 80%, var(--color-ps-text));
225
+ --color-toast-warning-accent: color-mix(in srgb, var(--color-ps-warning) 80%, var(--color-ps-text));
226
+ --color-toast-error-accent: color-mix(in srgb, var(--color-ps-error) 80%, var(--color-ps-text));
227
+ }
228
+
229
+ /* =========================================================
230
+ Light mode overrides
231
+ ========================================================= */
232
+ [data-theme="light"] {
233
+ color-scheme: light;
234
+
235
+ --color-ps-aqua: #2b8f91;
236
+ --color-ps-indigo: #666ee1;
237
+ --color-ps-yellow: #ddb15b;
238
+
239
+ --color-ps-bg: #f5f6fb;
240
+ --color-ps-surface: #ffffff;
241
+ --color-ps-section: #e9edf5;
242
+ --color-ps-elevated: #edf0f8;
243
+ --color-ps-card: #ffffff;
244
+ --color-ps-border: #d4d9e8;
245
+ --color-ps-border-hover: #bfc8dc;
246
+
247
+ /* Rows flip to an ink-tinted overlay on light surfaces */
248
+ --color-ps-row: rgba(15, 18, 41, 0.04);
249
+ --color-ps-row-hover: rgba(15, 18, 41, 0.08);
250
+
251
+ --color-ps-text: #1c2344;
252
+ --color-ps-text-dim: #2b345b;
253
+ --color-ps-text-subtle: #5b6587;
254
+
255
+ --color-ps-success: #2db78f;
256
+ --color-ps-warning: #e0a545;
257
+ --color-ps-error: #e06a6a;
258
+
259
+ /* Focus ring: deeper saturated teal reads better on light surfaces
260
+ than the on-accent-lifted dark formula */
261
+ --color-focus-ring: color-mix(in srgb, var(--color-ps-aqua) 85%, transparent);
262
+
263
+ /* Deepen toward white in light mode so filled tiles/surfaces stay light under dark text.
264
+ (--color-brand-shade intentionally stays fixed so brand-filled controls remain rich.) */
265
+ --color-surface-sink: #ffffff;
266
+
267
+ /* Shadows tuned for light surfaces */
268
+ --shadow-sm: 0 1px 2px rgba(15, 18, 41, 0.08);
269
+ --shadow-md: 0 10px 24px -14px rgba(15, 18, 41, 0.22);
270
+ --shadow-lg: 0 14px 32px -16px rgba(15, 18, 41, 0.28);
271
+ --shadow-ps-card:
272
+ 0 1px 3px rgba(15, 18, 41, 0.06),
273
+ 0 1px 2px rgba(15, 18, 41, 0.04);
274
+ --shadow-ps-card-hover: 0 8px 24px -8px rgba(15, 18, 41, 0.18);
275
+ }
276
+
277
+ /* =========================================================
278
+ Brand: Control Union — data-brand="cu" on <html>
279
+ Default brand (no attribute) is Peterson Solution.
280
+ Only PRIMITIVES are overridden; every semantic token
281
+ (badge, messagebar, toast, focus ring, state colours)
282
+ derives via color-mix and re-themes automatically.
283
+ Combines freely with [data-theme="light"].
284
+ ========================================================= */
285
+ /* Palette extracted from Control Union Style Guide 2023 v2.1:
286
+ Grey #799495, Cyan #3eb2ed, Dark Blue #1b1e42, Dark Grey #4f6566,
287
+ and light-blue gradient #008bcb -> #3eb2ed.
288
+ Sansa Pro and Ubuntu are licensed/host-loaded font families. */
289
+ [data-brand="cu"] {
290
+ /* Brand font intent: declare stacks only; hosts load licensed font files. */
291
+ --font-brand: "Sansa Pro", "Calibri", "Segoe UI", ui-sans-serif, system-ui;
292
+ --font-body: "Ubuntu", "Calibri", "Segoe UI", ui-sans-serif, system-ui;
293
+ --font-sans: var(--font-body);
294
+
295
+ --color-ps-aqua: var(--color-cu-cyan);
296
+ --color-ps-indigo: var(--color-cu-dark-blue);
297
+ --color-ps-yellow: var(--color-cu-grey);
298
+ --color-ps-on-accent: var(--color-cu-dark-blue);
299
+
300
+ /* Dark mode is a restrained UI adaptation of CU dark blue plus cyan. */
301
+ --color-ps-bg: #101331;
302
+ --color-ps-surface: var(--color-cu-dark-blue);
303
+ --color-ps-section: #20244b;
304
+ --color-ps-elevated: #262b56;
305
+ --color-ps-card: #171b3d;
306
+ --color-ps-border: color-mix(in srgb, var(--color-cu-grey) 34%, var(--color-cu-dark-blue));
307
+ --color-ps-border-hover: color-mix(in srgb, var(--color-cu-cyan) 42%, var(--color-cu-grey));
308
+
309
+ --color-ps-row: color-mix(in srgb, var(--color-cu-white) 5%, transparent);
310
+ --color-ps-row-hover: color-mix(in srgb, var(--color-cu-cyan) 13%, transparent);
311
+
312
+ --color-ps-text: #f7fbfc;
313
+ --color-ps-text-dim: #d8e6e9;
314
+ --color-ps-text-subtle: #a9bec2;
315
+
316
+ --color-ps-success: #5dd39e;
317
+ --color-ps-warning: #f6c75f;
318
+ --color-ps-error: #ff7b7b;
319
+
320
+ --color-focus-ring: color-mix(in srgb, var(--color-cu-cyan) 78%, var(--color-cu-white));
321
+ --color-surface-sink: #0b0e28;
322
+ --color-brand-shade: var(--color-cu-dark-blue);
323
+
324
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.22);
325
+ --shadow-md: 0 12px 26px -16px rgba(0, 0, 0, 0.55);
326
+ --shadow-lg: 0 18px 42px -22px rgba(0, 0, 0, 0.68);
327
+ --shadow-ps-card: 0 2px 10px rgba(0, 0, 0, 0.18);
328
+ --shadow-ps-card-hover: 0 10px 28px -10px rgba(0, 0, 0, 0.46);
329
+ }
330
+
331
+ [data-brand="cu"][data-theme="light"] {
332
+ /* Darkened from the official cyan gradient for small-text contrast on white. */
333
+ --color-ps-aqua: #0077aa;
334
+ --color-ps-indigo: var(--color-cu-dark-blue);
335
+ --color-ps-yellow: var(--color-cu-grey);
336
+ --color-ps-on-accent: var(--color-cu-white);
337
+
338
+ /* Keep light mode neutral like the default theme. CU appears in accent
339
+ controls, selected states, icons, focus, and filled brand surfaces. */
340
+ --color-ps-bg: #f5f6fb;
341
+ --color-ps-surface: #ffffff;
342
+ --color-ps-section: #e9edf5;
343
+ --color-ps-elevated: #edf0f8;
344
+ --color-ps-card: #ffffff;
345
+ --color-ps-border: #d4d9e8;
346
+ --color-ps-border-hover: #bfc8dc;
347
+
348
+ --color-ps-row: rgba(15, 18, 41, 0.04);
349
+ --color-ps-row-hover: rgba(15, 18, 41, 0.08);
350
+
351
+ --color-ps-text: var(--color-cu-dark-blue);
352
+ --color-ps-text-dim: var(--color-cu-dark-grey);
353
+ --color-ps-text-subtle: var(--color-cu-grey);
354
+
355
+ --color-ps-success: #218e63;
356
+ --color-ps-warning: #b97713;
357
+ --color-ps-error: #c64343;
358
+
359
+ --color-focus-ring: color-mix(in srgb, var(--color-cu-gradient-start) 82%, transparent);
360
+ --color-surface-sink: var(--color-cu-white);
361
+ --color-state-hover: color-mix(in srgb, var(--color-ps-text) 8%, transparent);
362
+ --color-state-selected: color-mix(in srgb, var(--color-ps-aqua) 16%, transparent);
363
+
364
+ --shadow-sm: 0 1px 2px rgba(15, 18, 41, 0.08);
365
+ --shadow-md: 0 10px 24px -14px rgba(15, 18, 41, 0.22);
366
+ --shadow-lg: 0 16px 36px -18px rgba(27, 30, 66, 0.28);
367
+ --shadow-ps-card:
368
+ 0 1px 3px rgba(15, 18, 41, 0.06),
369
+ 0 1px 2px rgba(27, 30, 66, 0.04);
370
+ --shadow-ps-card-hover: 0 8px 24px -8px rgba(15, 18, 41, 0.18);
371
+ }