@apliteni/apliteni-ui 0.2.4 → 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 CHANGED
@@ -9,7 +9,7 @@ and light with **accent sub-themes**. Showcased and reviewed in **Storybook**, a
9
9
  published on **ui.apli.tech**.
10
10
 
11
11
  - 🎨 **Live site + Storybook** → [ui.apli.tech](https://ui.apli.tech)
12
- - 📦 **Package** → `@apliteni/apliteni-ui` (GitHub Packages)
12
+ - 📦 **Package** → [`@apliteni/apliteni-ui`](https://www.npmjs.com/package/@apliteni/apliteni-ui) (public npm)
13
13
 
14
14
  ## Why HTML + CSS (not React)
15
15
 
@@ -21,25 +21,12 @@ of truth — the portal imports it with no rewrite and no framework drift. Story
21
21
 
22
22
  ## Install
23
23
 
24
- The package lives in **GitHub Packages** (private to the `apliteni` org). Point the
25
- `@apliteni` scope at it and authenticate with a GitHub token that has `read:packages`.
26
-
27
- `.npmrc` in the consuming repo:
28
-
29
- ```
30
- @apliteni:registry=https://npm.pkg.github.com
31
- //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
32
- ```
33
-
34
- Then:
24
+ Published on the **public npm registry** no scope config, no token:
35
25
 
36
26
  ```bash
37
- NODE_AUTH_TOKEN=$(gh auth token) npm install @apliteni/apliteni-ui
27
+ npm install @apliteni/apliteni-ui
38
28
  ```
39
29
 
40
- In CI, `NODE_AUTH_TOKEN` is the workflow's `GITHUB_TOKEN`. In Docker, pass it as a
41
- build secret (see the strategy portal's Dockerfile for the pattern).
42
-
43
30
  ## Use it
44
31
 
45
32
  ```js
@@ -117,15 +104,15 @@ site/ # ui.apli.tech landing page + static server
117
104
  ## Develop
118
105
 
119
106
  ```bash
120
- NODE_AUTH_TOKEN=$(gh auth token) npm install
107
+ npm install
121
108
  npm run storybook # http://localhost:6006
122
109
  npm run build-storybook # -> storybook-static/
123
110
  node site/build.mjs # -> site/public/ (landing + kit.css)
124
111
  ```
125
112
 
126
- ## Publish (GitHub Packages)
113
+ ## Publish (public npm)
127
114
 
128
- Versioned publish runs from CI on a GitHub Release:
115
+ Versioned publish runs from CI on a GitHub Release (needs the `NPM_TOKEN` secret):
129
116
 
130
117
  ```bash
131
118
  npm version patch # or minor / major — bumps package.json + tags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Apliteni shared design system & UI kit — tokens, components and the deck theme, framework-agnostic (HTML + CSS).",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -43,6 +43,8 @@
43
43
  "@storybook/blocks": "^8.6.14",
44
44
  "@storybook/html": "^8.6.14",
45
45
  "@storybook/html-vite": "^8.6.14",
46
+ "axe-core": "^4.12.1",
47
+ "jsdom": "^29.1.1",
46
48
  "storybook": "^8.6.14",
47
49
  "vite": "^5.4.11"
48
50
  }
@@ -25,6 +25,9 @@ const P = {
25
25
  search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
26
26
  globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18z"/>',
27
27
  sparkle: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2 2M16 16l2 2M18 6l-2 2M8 16l-2 2"/>',
28
+ card: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
29
+ doc: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/>',
30
+ chart: '<path d="M3 3v18h18"/><path d="M8 17v-5M13 17V8M18 17v-8"/>',
28
31
  };
29
32
 
30
33
  export const icon = (name, cls = '') =>
@@ -48,6 +48,28 @@ export function card({ title, sub, body = '', variant, pad, icon: ic } = {}) {
48
48
  return `<div class="${cls}">${head}${body}</div>`;
49
49
  }
50
50
 
51
+ // ---- Aurora — ambient warm backdrop --------------------------------------
52
+ // Layered blurred glow "blobs" + optional paper grain. Colours default to the
53
+ // accent glow tokens, so the field re-themes with the sub-theme. Drop it as the
54
+ // first child of a positioned wrapper (or use fixed: true for full-bleed) and
55
+ // give the real content a normal stacking context — it sits on top.
56
+ const AURORA_PRESET = [
57
+ { tone: 'accent', x: '22%', y: '12%', size: '64%', delay: '0s' },
58
+ { tone: 'teal', x: '84%', y: '24%', size: '56%', delay: '-9s' },
59
+ { tone: 'warm', x: '54%', y: '92%', size: '70%', delay: '-17s' },
60
+ ];
61
+ export function aurora({ blobs = AURORA_PRESET, grain = false, fixed = false, animated = true, className = '' } = {}) {
62
+ const items = blobs.map((b) => {
63
+ const tone = b.tone || 'accent';
64
+ const style = `--au-x:${b.x ?? '50%'};--au-y:${b.y ?? '50%'};--au-size:${b.size ?? '60%'}`
65
+ + (b.delay != null ? `;--au-delay:${b.delay}` : '');
66
+ return `<span class="ui-aurora__blob ui-aurora__blob--${tone}" style="${style}"></span>`;
67
+ }).join('');
68
+ const grainEl = grain ? '<span class="ui-aurora__grain"></span>' : '';
69
+ const cls = cx('ui-aurora', animated && 'ui-aurora--animated', fixed && 'ui-aurora--fixed', className);
70
+ return `<div class="${cls}" aria-hidden="true">${items}${grainEl}</div>`;
71
+ }
72
+
51
73
  // ---- Segmented control ---------------------------------------------------
52
74
  export function segmented({ options = [], active = 0, size, block, name = 'seg' } = {}) {
53
75
  const cls = cx('ui-seg', size && `ui-seg--${size}`, block && 'ui-seg--block');
@@ -73,16 +95,57 @@ export function accentPicker({ active = 'default', options = ['default', 'phoeni
73
95
  }
74
96
 
75
97
  // ---- Field / input -------------------------------------------------------
76
- export function field({ label, hint, error, control } = {}) {
77
- return `<div class="ui-field">${label ? `<label class="ui-field__label">${esc(label)}</label>` : ''}${control || ''}${error ? `<div class="ui-field__error">${icon('alert')}${esc(error)}</div>` : hint ? `<div class="ui-field__hint">${esc(hint)}</div>` : ''}</div>`;
98
+ // Auto-generated ids let field() tie its <label for> to the control it wraps.
99
+ // A module counter (never Date/random) keeps ids unique within a rendered page.
100
+ let _uid = 0;
101
+ const nextId = (p = 'ui') => `${p}-${++_uid}`;
102
+ // Give the first form control in `html` an id (or reuse one it already carries)
103
+ // so a <label> can point at it. Returns { html, id }; id is null if there's no
104
+ // form element to name.
105
+ function withControlId(html) {
106
+ const existing = html.match(/<(?:input|textarea|select)\b[^>]*\bid="([^"]+)"/);
107
+ if (existing) return { html, id: existing[1] };
108
+ let id = null;
109
+ const out = html.replace(/<(input|textarea|select)\b/, (m) => { id = nextId('field'); return `${m} id="${id}"`; });
110
+ return { html: out, id };
78
111
  }
79
- export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, name } = {}) {
80
- const el = `<input class="${cx('ui-input', invalid && 'is-invalid')}" type="${type}" placeholder="${esc(placeholder)}" value="${esc(value)}"${name ? ` name="${name}"` : ''}${disabled ? ' disabled' : ''}>`;
112
+
113
+ export function field({ label, hint, error, control = '', id } = {}) {
114
+ let ctl = control;
115
+ let forId = id;
116
+ if (label && control) {
117
+ const r = withControlId(control);
118
+ ctl = r.html;
119
+ forId = id || r.id;
120
+ }
121
+ const lab = label
122
+ ? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}</label>`
123
+ : '';
124
+ const foot = error
125
+ ? `<div class="ui-field__error">${icon('alert')}${esc(error)}</div>`
126
+ : hint ? `<div class="ui-field__hint">${esc(hint)}</div>` : '';
127
+ return `<div class="ui-field">${lab}${ctl}${foot}</div>`;
128
+ }
129
+ // `ariaLabel` gives a standalone control (no wrapping field) an accessible name.
130
+ export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, name, id, ariaLabel } = {}) {
131
+ const attrs = `${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}`;
132
+ const el = `<input class="${cx('ui-input', invalid && 'is-invalid')}" type="${type}" placeholder="${esc(placeholder)}" value="${esc(value)}"${attrs}>`;
81
133
  if (!ic) return el;
82
134
  return `<div class="ui-input-group"><span class="ui-input-group__icon">${icon(ic)}</span>${el}</div>`;
83
135
  }
84
- export function textarea({ placeholder = '', value = '', rows = 4 } = {}) {
85
- return `<textarea class="ui-textarea" rows="${rows}" placeholder="${esc(placeholder)}">${esc(value)}</textarea>`;
136
+ export function textarea({ placeholder = '', value = '', rows = 4, name, id, ariaLabel } = {}) {
137
+ return `<textarea class="ui-textarea" rows="${rows}" placeholder="${esc(placeholder)}"${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}>${esc(value)}</textarea>`;
138
+ }
139
+ // Native <select>. Pass a `label` via field() or an `ariaLabel` for a bare one —
140
+ // a select with neither has no accessible name.
141
+ export function select({ options = [], value, name, id, ariaLabel, disabled } = {}) {
142
+ const opts = options.map((o) => {
143
+ const label = typeof o === 'string' ? o : o.label;
144
+ const val = typeof o === 'string' ? o : (o.value ?? o.label);
145
+ const sel = value != null && String(val) === String(value);
146
+ return `<option value="${esc(val)}"${sel ? ' selected' : ''}>${esc(label)}</option>`;
147
+ }).join('');
148
+ return `<select class="ui-select"${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}>${opts}</select>`;
86
149
  }
87
150
  export function checkbox({ label, checked, type = 'checkbox', name } = {}) {
88
151
  return `<label class="ui-check"><input type="${type}"${name ? ` name="${name}"` : ''}${checked ? ' checked' : ''}><span>${label}</span></label>`;
@@ -13,8 +13,8 @@ export const ACCOUNT_NAV = [
13
13
  ['access', 'key', 'Access &amp; agents'],
14
14
  ];
15
15
 
16
- const sidebar = (nav, active) =>
17
- `<nav class="ui-side"><div class="cap">Account</div>` +
16
+ const sidebar = (nav, active, cap = 'Account') =>
17
+ `<nav class="ui-side"><div class="cap">${cap}</div>` +
18
18
  nav.map(([id, ic, label, href, target]) =>
19
19
  `<a href="${href || '#' + id}"${target ? ` target="${target}"` : ''}${id === active ? ' class="on"' : ''}>${icon(ic)}${label}</a>`).join('') +
20
20
  `<div class="ssep"></div><a class="out" href="#logout">${icon('logout')}Sign out</a></nav>`;
@@ -25,6 +25,8 @@ export function accountShell({
25
25
  account = {},
26
26
  nav = ACCOUNT_NAV,
27
27
  active = 'prefs',
28
+ cap = 'Account',
29
+ showSwitch = false,
28
30
  crumb,
29
31
  title = '',
30
32
  sub = '',
@@ -32,11 +34,11 @@ export function accountShell({
32
34
  } = {}) {
33
35
  return `<div style="position:relative;overflow:hidden;min-height:100vh">
34
36
  <span class="ui-glow ui-glow--purple" style="top:-120px;right:6%;opacity:.35"></span>
35
- ${topbar({ word, view: 'text', versions, account: { ...account, active, nav } })}
37
+ ${topbar({ word, view: 'text', showSwitch, versions, account: { ...account, active, nav } })}
36
38
  <div class="ui-shell">
37
- ${sidebar(nav, active)}
39
+ ${sidebar(nav, active, cap)}
38
40
  <div class="ui-shell__page">
39
- <div class="ui-shell__crumbs">Account / <b>${crumb || title}</b></div>
41
+ <div class="ui-shell__crumbs">${cap} / <b>${crumb || title}</b></div>
40
42
  <h1>${title}</h1>
41
43
  <div class="sub">${sub}</div>
42
44
  <div class="ui-card-stack">${body}</div>
@@ -2,6 +2,7 @@
2
2
  // (brand, Deck/Text, theme toggle, version switcher, account menu).
3
3
  import { brand } from '../assets/brand.js';
4
4
  import { icon, sun, moon } from '../assets/icons.js';
5
+ import { esc } from './index.js';
5
6
 
6
7
  const THEME_KEY = 'apliteni-strategy-theme';
7
8
 
@@ -18,12 +19,12 @@ export function deckTextSwitch(active = 'deck') {
18
19
  export function versionSwitcher(versions = [], activeIdx = 0) {
19
20
  const cur = versions[activeIdx]?.label || '';
20
21
  const opts = versions.map((v, i) =>
21
- `<div class="vopt" role="option" data-active="${i === activeIdx ? '1' : '0'}">` +
22
+ `<div class="vopt" role="option" aria-selected="${i === activeIdx}" data-active="${i === activeIdx ? '1' : '0'}">` +
22
23
  `<span><div class="vname">${v.label}</div><div class="vmeta">${v.meta || ''}</div></span>` +
23
24
  `<span class="vbadge ${v.badge === 'live' ? 'live' : 'arch'}">${v.badge || 'archive'}</span></div>`).join('');
24
- return `<div class="vsw" data-vsw><button class="vsw__btn" aria-haspopup="listbox" aria-expanded="false">` +
25
+ return `<div class="vsw" data-vsw><button class="vsw__btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Version — ${esc(cur)}">` +
25
26
  `<span class="lbl">version:</span><span class="cur">${cur}</span><span class="car"></span></button>` +
26
- `<div class="vsw__menu" role="listbox">${opts}</div></div>`;
27
+ `<div class="vsw__menu" role="listbox" aria-label="Version">${opts}</div></div>`;
27
28
  }
28
29
 
29
30
  // `nav` ([id, icon, label, href?, target?][]) mirrors the account sidebar so the
package/src/index.css CHANGED
@@ -6,6 +6,7 @@
6
6
  @import "./tokens/tokens.css";
7
7
  @import "./tokens/accents.css";
8
8
  @import "./styles/base.css";
9
+ @import "./styles/aurora.css";
9
10
  @import "./styles/button.css";
10
11
  @import "./styles/card.css";
11
12
  @import "./styles/badge.css";
package/src/inline.js CHANGED
@@ -24,6 +24,7 @@ export const topbarCss = read('styles/topbar.css');
24
24
  // Individual component stylesheets, addressable by name.
25
25
  export const styles = {
26
26
  base: baseCss,
27
+ aurora: read('styles/aurora.css'),
27
28
  button: read('styles/button.css'),
28
29
  card: read('styles/card.css'),
29
30
  badge: read('styles/badge.css'),
@@ -41,6 +42,7 @@ export const styles = {
41
42
  export const cssText = [
42
43
  tokensCss,
43
44
  styles.base,
45
+ styles.aurora,
44
46
  styles.button,
45
47
  styles.card,
46
48
  styles.badge,
@@ -0,0 +1,93 @@
1
+ /* ============================================================================
2
+ * aurora — ambient warm backdrop: layered blurred glow "blobs" + optional
3
+ * paper grain. The kit's one signature page background, so every app gets one
4
+ * warm identity for free.
5
+ *
6
+ * Markup comes from aurora() in components/index.js, but you can hand-roll it:
7
+ * <div class="ui-aurora ui-aurora--animated" aria-hidden="true">
8
+ * <span class="ui-aurora__blob ui-aurora__blob--accent" style="--au-x:12%;--au-y:-4%"></span>
9
+ * ...
10
+ * <span class="ui-aurora__grain"></span>
11
+ * </div>
12
+ *
13
+ * Colours default to the accent glow tokens, so the whole field re-themes with
14
+ * the sub-theme (phoenix / ocean / emerald) and holds up in dark AND light with
15
+ * no per-accent rules. Override --au-1/2/3 on .ui-aurora to retune.
16
+ * ========================================================================== */
17
+
18
+ .ui-aurora {
19
+ /* Tone slots — mixed to a visible alpha off the accent families so the field
20
+ actually reads as an aurora (the --glow-* tokens are tuned faint for the
21
+ small .ui-bg-* utilities). --purple-light + --grad-to re-point per accent,
22
+ so the whole field re-themes; --cyan stays a fixed teal. Override to retune. */
23
+ --au-1: color-mix(in srgb, var(--purple-light) 52%, transparent);
24
+ --au-2: color-mix(in srgb, var(--cyan) 40%, transparent);
25
+ --au-3: color-mix(in srgb, var(--grad-to) 44%, transparent);
26
+
27
+ position: absolute;
28
+ inset: 0;
29
+ z-index: 0;
30
+ overflow: hidden;
31
+ pointer-events: none;
32
+ }
33
+ /* Light theme: the accent families are darker solids, so dial the mix right
34
+ down — a whisper of tint, not a wash. */
35
+ :root[data-theme="light"] .ui-aurora {
36
+ --au-1: color-mix(in srgb, var(--purple-light) 18%, transparent);
37
+ --au-2: color-mix(in srgb, var(--cyan) 15%, transparent);
38
+ --au-3: color-mix(in srgb, var(--grad-to) 17%, transparent);
39
+ }
40
+
41
+ /* Full-bleed: pin to the viewport behind the whole page. Give real content a
42
+ normal (non-negative) stacking context and it sits on top automatically. */
43
+ .ui-aurora--fixed {
44
+ position: fixed;
45
+ z-index: -1;
46
+ }
47
+
48
+ /* A blurred radial blob. Position via --au-x / --au-y (its centre), scale via
49
+ --au-size. Centring uses `translate` so the drift animation owns `transform`. */
50
+ .ui-aurora__blob {
51
+ position: absolute;
52
+ top: var(--au-y, 50%);
53
+ left: var(--au-x, 50%);
54
+ width: var(--au-size, 60%);
55
+ aspect-ratio: 1;
56
+ translate: -50% -50%;
57
+ border-radius: 50%;
58
+ background: radial-gradient(circle at center, var(--au-tone), transparent 68%);
59
+ filter: blur(70px);
60
+ will-change: transform;
61
+ }
62
+ .ui-aurora__blob--accent { --au-tone: var(--au-1); }
63
+ .ui-aurora__blob--teal { --au-tone: var(--au-2); }
64
+ .ui-aurora__blob--warm { --au-tone: var(--au-3); }
65
+
66
+ /* Slow ambient drift. Staggered per blob via --au-delay so they never line up. */
67
+ .ui-aurora--animated .ui-aurora__blob {
68
+ animation: ui-aurora-drift 26s ease-in-out infinite alternate;
69
+ animation-delay: var(--au-delay, 0s);
70
+ }
71
+ @keyframes ui-aurora-drift {
72
+ from { transform: translate(-2%, -1%) scale(1); }
73
+ to { transform: translate(3%, 2%) scale(1.12); }
74
+ }
75
+
76
+ /* Paper grain — a low-opacity fractal-noise veil. `overlay` in dark keeps it
77
+ from muddying the blacks; `multiply` in light reads as fine tooth on paper. */
78
+ .ui-aurora__grain {
79
+ position: absolute;
80
+ inset: 0;
81
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
82
+ background-size: 160px 160px;
83
+ opacity: 0.5;
84
+ mix-blend-mode: overlay;
85
+ }
86
+ :root[data-theme="light"] .ui-aurora__grain {
87
+ opacity: 0.4;
88
+ mix-blend-mode: multiply;
89
+ }
90
+
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .ui-aurora__blob { animation: none; }
93
+ }
@@ -26,6 +26,22 @@
26
26
  .ui-badge--danger { color: var(--pink); background: var(--glow-pink); }
27
27
  .ui-badge--archive { color: var(--muted); background: var(--surface); }
28
28
 
29
+ /* Semantic status variants — for record/row status in data tables
30
+ (paid / verified / pending / failed …). Same chip, mapped to meaning. */
31
+ .ui-badge--success { color: var(--green); background: var(--glow-green); }
32
+ .ui-badge--pending { color: var(--amber); background: color-mix(in srgb, var(--amber) 15%, transparent); }
33
+ .ui-badge--neutral { color: var(--muted); background: var(--surface-2); }
34
+
35
+ /* On the white light app the glow-tints wash out — give status chips a solid
36
+ tinted fill so they read at a glance. */
37
+ :root[data-theme="light"] .ui-badge--success,
38
+ :root[data-theme="light"] .ui-badge--live { color: #1f7a38; background: #dff3e4; }
39
+ :root[data-theme="light"] .ui-badge--pending,
40
+ :root[data-theme="light"] .ui-badge--warn { color: #8a5e00; background: #fbedd2; }
41
+ :root[data-theme="light"] .ui-badge--danger { color: #b7295f; background: #fbe0ea; }
42
+ :root[data-theme="light"] .ui-badge--neutral,
43
+ :root[data-theme="light"] .ui-badge--archive { color: #5c6270; background: #eceff4; }
44
+
29
45
  /* Pill: same shape, sentence case, a touch larger — a metadata chip */
30
46
  .ui-pill {
31
47
  display: inline-flex;
@@ -11,6 +11,19 @@
11
11
  position: relative;
12
12
  }
13
13
 
14
+ /* Light theme is an all-white app, so a filled card can't rely on a darker-
15
+ than-page surface the way the dark deck does — give it a hairline border +
16
+ soft shadow to read as a panel. Dark keeps the borderless deck look. */
17
+ :root[data-theme="light"] .ui-card {
18
+ border: 1px solid var(--border);
19
+ box-shadow: 0 1px 2px rgba(20, 25, 45, .04), 0 4px 14px rgba(20, 25, 45, .05);
20
+ }
21
+
22
+ /* A table wider than its card scrolls INSIDE the card instead of bleeding past
23
+ the rounded corners / clipping its last column. Clipping to the card's
24
+ border-box also keeps zebra stripes from escaping the radius. */
25
+ .ui-card:has(> .ui-table) { overflow-x: auto; }
26
+
14
27
  /* Vertical stacks space cards with the container's gap — see .ui-card-stack.
15
28
  (No `.ui-card + .ui-card` margin: it leaks into grid/flex rows and misaligns.) */
16
29
  .ui-card-stack { display: flex; flex-direction: column; gap: var(--space-5); }
@@ -34,6 +34,31 @@
34
34
  .ui-table tbody tr { transition: background var(--dur-fast) var(--ease); }
35
35
  .ui-table--hover tbody tr:hover { background: var(--surface-2); }
36
36
 
37
+ /* ── Data-table modifiers (composable) ──────────────────────────────────────
38
+ * Compose for a dense financial ledger: .ui-table--dense --zebra --hover.
39
+ * --dense compact row rhythm for many-row / many-column data
40
+ * --zebra row striping instead of per-row rules; 2px header rule; the
41
+ * (accent) hover tint out-ranks the stripe (defined after it)
42
+ * Cell helpers: .ui-table__num (right, tabular), .ui-table__code (plain
43
+ * monospace id — never a boxed code chip inside a table). */
44
+ .ui-table--dense th { padding: 0 14px 10px 0; }
45
+ .ui-table--dense td { padding: 10px 14px; font-size: var(--text-sm); white-space: nowrap; }
46
+ .ui-table--dense th:first-child,
47
+ .ui-table--dense td:first-child { padding-left: 12px; }
48
+ .ui-table--dense th:last-child,
49
+ .ui-table--dense td:last-child { padding-right: 12px; }
50
+
51
+ .ui-table--zebra th { border-bottom-width: 2px; }
52
+ .ui-table--zebra td { border-bottom: 0; }
53
+ .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
54
+ .ui-table--zebra.ui-table--hover tbody tr:hover td,
55
+ .ui-table--zebra tbody tr:hover td { background: color-mix(in srgb, var(--accent) 8%, transparent); }
56
+
57
+ .ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }
58
+ .ui-table th.ui-table__num { text-align: right; }
59
+ .ui-table__num--strong { color: var(--strong); font-weight: var(--weight-semibold); }
60
+ .ui-table__code { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--muted); }
61
+
37
62
  /* Empty state inside a table area */
38
63
  .ui-empty {
39
64
  text-align: center;
@@ -21,16 +21,22 @@
21
21
  align-items: center;
22
22
  gap: 14px;
23
23
  }
24
- .topbar .brand {
24
+ /* Brand lockup (mark + wordmark) — self-contained so the mark stays optically
25
+ centered with the word, with an even gap, in ANY context (topbar, auth cards,
26
+ consent). These rules used to be scoped to `.topbar`, so a lockup dropped into
27
+ the consent/auth card fell back to inline layout: the mark rode the text
28
+ baseline with no gap between it and the word. */
29
+ .brand {
25
30
  display: inline-flex;
26
31
  align-items: center;
27
32
  gap: 9px;
28
- font-size: 13px;
29
33
  font-weight: 600;
30
34
  color: var(--strong);
31
35
  text-decoration: none;
32
36
  }
33
- .topbar .brand svg { display: block; flex: none; }
37
+ .brand svg { display: block; flex: none; }
38
+ /* Topbar wordmark runs a touch smaller than the auth-card lockup. */
39
+ .topbar .brand { font-size: 13px; }
34
40
  .topbar .spacer { flex: 1; }
35
41
 
36
42
  /* Deck / Text (segmented, pill) */
@@ -149,19 +149,21 @@
149
149
  --pink: #d63c72;
150
150
  --amber: #b5730a;
151
151
 
152
- --bg: #f6f6f8;
152
+ /* A WHITE app: the page and cards are both white; cards are delineated by a
153
+ hairline border + soft shadow (see card.css), not a grey canvas. */
154
+ --bg: #ffffff;
153
155
  --bg-elevated: #ffffff;
154
156
  --surface: #ffffff;
155
- --surface-2: #f0f1f5;
156
- --surface-3: #e7e9f0;
157
+ --surface-2: #f5f6f9;
158
+ --surface-3: #eceef3;
157
159
  --seg-active-bg: #ffffff; /* white pill raised above the grey track in light */
158
- --border: #d7dae2;
159
- --border-strong: #c3c7d4;
160
+ --border: #e4e7ee;
161
+ --border-strong: #cdd2dc;
160
162
 
161
- --text: #1e232b;
162
- --strong: #0f0f13;
163
- --dim: #3d434e;
164
- --muted: #565c68;
163
+ --text: #1a1e27;
164
+ --strong: #0c0e13;
165
+ --dim: #3a404b;
166
+ --muted: #5c6270;
165
167
 
166
168
  --accent: #6a2dcc;
167
169
  --accent-strong: #6a2dcc; /* already dark enough for white text */