@apliteni/apliteni-ui 0.12.0 → 0.23.2

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/react/dist/index.css +19 -0
  3. package/react/dist/index.d.ts +43 -2
  4. package/react/dist/index.js +98 -6
  5. package/src/assets/icons.js +5 -26
  6. package/src/components/account-nav.js +1 -1
  7. package/src/components/confirm.js +27 -28
  8. package/src/components/drawer.js +27 -28
  9. package/src/components/dropdown.js +26 -23
  10. package/src/components/index.js +9 -13
  11. package/src/components/loading.js +126 -0
  12. package/src/components/nav.js +7 -20
  13. package/src/components/overlay.js +7 -16
  14. package/src/components/shell.js +10 -9
  15. package/src/components/success.js +8 -17
  16. package/src/components/topbar.js +8 -12
  17. package/src/index.css +6 -1
  18. package/src/index.js +1 -0
  19. package/src/inline.js +4 -0
  20. package/src/styles/badge.css +1 -1
  21. package/src/styles/base.css +5 -18
  22. package/src/styles/button.css +18 -5
  23. package/src/styles/callout.css +37 -26
  24. package/src/styles/card.css +1 -1
  25. package/src/styles/code.css +7 -1
  26. package/src/styles/confirm.css +10 -13
  27. package/src/styles/drawer.css +9 -6
  28. package/src/styles/dropdown.css +14 -7
  29. package/src/styles/empty.css +1 -1
  30. package/src/styles/feedback.css +25 -16
  31. package/src/styles/footer.css +15 -6
  32. package/src/styles/input.css +28 -2
  33. package/src/styles/layout.css +14 -10
  34. package/src/styles/loading.css +129 -0
  35. package/src/styles/motion.css +21 -39
  36. package/src/styles/nav.css +14 -12
  37. package/src/styles/reduced-motion.css +21 -0
  38. package/src/styles/success.css +8 -8
  39. package/src/styles/table.css +20 -17
  40. package/src/styles/topbar.css +27 -12
  41. package/src/tokens/accents.css +10 -14
  42. package/src/tokens/tokens.css +95 -22
@@ -1,24 +1,19 @@
1
- /* ============================================================================
2
- * apliteni-ui MOTION LIBRARY
3
- *
4
- * A small, token-driven, framework-agnostic motion system: reusable entrance,
5
- * micro-interaction, attention, and scroll-reveal effects as plain classes.
6
- * Every timing reads the brand motion vocabulary (--duration-* / --easing-*,
7
- * synced from design-system) so motion re-themes and stays consistent.
1
+ /* apliteni-ui — motion library. Reusable entrance, micro-interaction, attention
2
+ * and scroll-reveal effects as plain classes, every timing reading the brand
3
+ * motion vocabulary.
8
4
  *
9
5
  * Per-effect overrides via custom properties on the element:
10
- * --m-dur duration (default: per effect)
11
- * --m-ease timing function (default: per effect)
12
- * --m-delay start delay (default: 0s)
13
- * --m-dist travel distance for slides (default: 16px)
6
+ * --m-dur duration --m-ease timing function
7
+ * --m-delay start delay --m-dist slide travel (default 16px)
8
+ *
9
+ * Scroll reveals need `initReveal()` from apliteni-ui/motion. Without JS,
10
+ * [data-reveal] content stays fully visible.
14
11
  *
15
- * Scroll reveals need one line of JS the optional `initReveal()` hook from
16
- * apliteni-ui/motion. Without JS, [data-reveal] content stays fully visible
17
- * (progressive enhancement, never hidden behind a script that didn't run).
12
+ * The global prefers-reduced-motion net lives in reduced-motion.css and is NOT
13
+ * @imported from here the icon gates read each stylesheet on its own and
14
+ * refuse a sheet that pulls in another.
18
15
  *
19
- * A single global prefers-reduced-motion block at the end neutralises ALL of
20
- * the above — plus any component animation in the kit — as the safety net.
21
- * ========================================================================== */
16
+ * why: docs/specification.md#motion */
22
17
 
23
18
  /* -- Entrances -------------------------------------------------------------
24
19
  * `both` fill so the element starts at the "from" frame (hidden) and holds
@@ -31,7 +26,7 @@
31
26
  .m-scale-in,
32
27
  .m-blur-in {
33
28
  animation-duration: var(--m-dur, var(--dur-slow));
34
- animation-timing-function: var(--m-ease, var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)));
29
+ animation-timing-function: var(--m-ease, var(--ease-out));
35
30
  animation-delay: var(--m-delay, 0s);
36
31
  animation-fill-mode: both;
37
32
  }
@@ -78,7 +73,7 @@
78
73
  box-shadow: var(--shadow-md);
79
74
  }
80
75
  .m-press {
81
- transition: transform var(--duration-instant, 80ms) var(--easing-sharp, cubic-bezier(0.4, 0, 0.6, 1));
76
+ transition: transform var(--dur-instant) var(--ease-sharp);
82
77
  }
83
78
  .m-press:active { transform: scale(0.97); }
84
79
 
@@ -100,12 +95,12 @@
100
95
  color-mix(in srgb, var(--text) 12%, transparent),
101
96
  transparent
102
97
  );
103
- animation: m-shimmer var(--duration-crawl, 1s) var(--easing-linear, linear) infinite;
98
+ animation: m-shimmer var(--duration-crawl, 1s) linear infinite; /* motion: ambient — a skeleton sweeps while the data is away */
104
99
  }
105
100
  @keyframes m-shimmer { to { transform: translateX(100%); } }
106
101
 
107
102
  /* -- Attention ------------------------------------------------------------- */
108
- .m-pulse { animation: m-pulse 2s var(--ease) infinite; }
103
+ .m-pulse { animation: m-pulse 2s var(--ease) infinite; } /* motion: ambient — attention loop with no interaction origin */
109
104
  @keyframes m-pulse {
110
105
  0%, 100% { transform: scale(1); opacity: 1; }
111
106
  50% { transform: scale(1.04); opacity: 0.86; }
@@ -126,7 +121,7 @@
126
121
  .m-draw [data-draw] {
127
122
  stroke-dasharray: var(--m-len, 48);
128
123
  stroke-dashoffset: var(--m-len, 48);
129
- animation: m-draw var(--dur-slow) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)) forwards;
124
+ animation: m-draw var(--dur-slow) var(--ease-out) forwards;
130
125
  animation-delay: var(--m-delay, 0s);
131
126
  }
132
127
  @keyframes m-draw { to { stroke-dashoffset: 0; } }
@@ -140,8 +135,8 @@
140
135
  opacity: 0;
141
136
  transform: translateY(18px);
142
137
  transition:
143
- opacity var(--dur-slow) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)),
144
- transform var(--dur-slow) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1));
138
+ opacity var(--dur-slow) var(--ease-out),
139
+ transform var(--dur-slow) var(--ease-out);
145
140
  transition-delay: calc(var(--reveal-i, 0) * var(--delay-2));
146
141
  }
147
142
  .js-reveal [data-reveal].is-revealed {
@@ -149,18 +144,5 @@
149
144
  transform: none;
150
145
  }
151
146
 
152
- /* -- Global reduced-motion safety net -------------------------------------
153
- * Neutralises every animation/transition in the kit the library's effects,
154
- * component motion (button loader, badge pulse, feedback check), and smooth
155
- * scroll. iteration-count:1 lets one-shot animations settle on their final
156
- * frame (e.g. a checkmark stays drawn) instead of snapping to the start. */
157
- @media (prefers-reduced-motion: reduce) {
158
- html { scroll-behavior: auto !important; }
159
- *, ::before, ::after {
160
- animation-duration: 0.01ms !important;
161
- animation-iteration-count: 1 !important;
162
- transition-duration: 0.01ms !important;
163
- scroll-behavior: auto !important;
164
- }
165
- [data-reveal] { opacity: 1 !important; transform: none !important; }
166
- }
147
+ /* The global reduced-motion net lives in reduced-motion.css, imported beside this
148
+ * file from src/index.css it has to reach the React bundle as well. */
@@ -44,9 +44,9 @@
44
44
  cursor: pointer;
45
45
  text-align: left;
46
46
  font-family: inherit;
47
- transition: background 0.16s var(--ease), color 0.16s var(--ease);
47
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
48
48
  }
49
- .ui-nav .ui-nav__item { color: var(--text); } /* (0,2,0): a host sheet's a:link is (0,1,1) — why: docs/adr/0007-one-page-shell-built-from-the-kits-own-nav.md */
49
+ .ui-nav .ui-nav__item { color: var(--text); } /* (0,2,0): a host sheet's a:link is (0,1,1) — why: docs/specification.md#the-page-shell */
50
50
  .ui-nav__item:hover { background: var(--surface-2); color: var(--strong); }
51
51
  .ui-nav__item.is-active {
52
52
  background: var(--surface-3);
@@ -70,7 +70,7 @@
70
70
  .ui-nav__item.is-active:focus-visible { box-shadow: var(--ring); }
71
71
 
72
72
  .ui-nav__ic { display: inline-flex; flex: none; }
73
- .ui-nav__ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 1.7; opacity: 0.72; }
73
+ .ui-nav__ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; opacity: 0.72; }
74
74
  .ui-nav__item.is-active .ui-nav__ic svg,
75
75
  .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); }
76
76
  .ui-nav__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -82,7 +82,9 @@
82
82
  /* Tabbing onto it is the same "about to click this" the pointer means. The ring
83
83
  says where you are; only this says what the row does. */
84
84
  .ui-nav__item.is-danger:focus-visible { background: var(--glow-pink); color: var(--pink); }
85
- .ui-nav__item.is-disabled { color: var(--muted); opacity: 0.55; cursor: default; pointer-events: none; }
85
+ /* A row that is off. No opacity: the label was already --muted and the fade
86
+ only pulled it toward the rail behind it (#220). */
87
+ .ui-nav__item.is-disabled { color: var(--disabled-ink); cursor: default; pointer-events: none; }
86
88
 
87
89
  /* The current row under the pointer. It rests on --surface-3, which is also what
88
90
  a resting row hovers to, so the one row with no hover response was the row the
@@ -123,7 +125,7 @@
123
125
  4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
124
126
  it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
125
127
  Emerald. So the accent wash is painted on a base surface, never a raised one.
126
- See #157 and docs/adr/0006-the-accent-is-measured-against-its-own-wash.md. */
128
+ See #157 and docs/specification.md#colour-and-contrast. */
127
129
  .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface-3); color: var(--accent); }
128
130
 
129
131
  /* Collapsible group */
@@ -134,7 +136,7 @@
134
136
  border-right: 1.6px solid var(--muted);
135
137
  border-bottom: 1.6px solid var(--muted);
136
138
  transform: rotate(-45deg);
137
- transition: transform 0.18s var(--ease);
139
+ transition: transform var(--dur-fast) var(--ease);
138
140
  }
139
141
  .ui-nav__group.is-open > .ui-nav__toggle .ui-nav__caret { transform: rotate(45deg); }
140
142
  .ui-nav__sub {
@@ -165,7 +167,7 @@
165
167
  .ui-nav--side.is-collapsed .ui-nav__caret,
166
168
  .ui-nav--side.is-collapsed .ui-nav__sub { display: none; }
167
169
  .ui-nav--side.is-collapsed .ui-nav__item { justify-content: center; padding: 11px 0; gap: 0; }
168
- .ui-nav--side.is-collapsed .ui-nav__ic svg { width: 19px; height: 19px; }
170
+ .ui-nav--side.is-collapsed .ui-nav__ic svg { width: 19px; height: 19px; stroke-width: 2; }
169
171
  .ui-nav--side.is-collapsed .ui-nav__item.is-active::before { left: 3px; }
170
172
  .ui-nav--side.is-collapsed .ui-nav__foot { text-align: center; }
171
173
 
@@ -188,7 +190,7 @@
188
190
  font-size: 14px;
189
191
  font-weight: 500;
190
192
  border-radius: 10px 10px 0 0;
191
- transition: color 0.16s var(--ease), background 0.16s var(--ease);
193
+ transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
192
194
  }
193
195
  .ui-nav__tab:hover { color: var(--strong); }
194
196
  .ui-nav__tab:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 8px; }
@@ -204,7 +206,7 @@
204
206
  background: var(--accent);
205
207
  opacity: 0;
206
208
  transform: scaleX(0.4);
207
- transition: opacity 0.16s var(--ease), transform 0.16s var(--ease);
209
+ transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
208
210
  }
209
211
  .ui-nav--tabs.is-underline .ui-nav__tab.is-active::after { opacity: 1; transform: scaleX(1); }
210
212
 
@@ -216,7 +218,7 @@
216
218
  color: var(--accent);
217
219
  }
218
220
 
219
- .ui-nav__tab.is-disabled { color: var(--muted); opacity: 0.5; cursor: default; }
221
+ .ui-nav__tab.is-disabled { color: var(--disabled-ink); cursor: default; }
220
222
  .ui-nav__tab .ui-nav__badge { height: 18px; }
221
223
 
222
224
  /* -- Breadcrumbs --------------------------------------------------------- */
@@ -239,7 +241,7 @@
239
241
  border-radius: 7px;
240
242
  text-decoration: none;
241
243
  font-size: 13px;
242
- transition: color 0.16s var(--ease), background 0.16s var(--ease);
244
+ transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
243
245
  }
244
246
  /* Carries the block's class as well as its own, for the reason .ui-nav__item
245
247
  does above: a linked crumb is an <a>, and a host stylesheet's `a:link` is
@@ -248,4 +250,4 @@
248
250
  a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface-2); }
249
251
  a.ui-nav__crumb:focus-visible { outline: none; box-shadow: var(--ring); }
250
252
  .ui-nav__crumb.is-current { color: var(--strong); font-weight: 500; }
251
- .ui-nav__crumb .ui-nav__ic svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.7; }
253
+ .ui-nav__crumb .ui-nav__ic svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.5; }
@@ -0,0 +1,21 @@
1
+ /* apliteni-ui — reduced-motion net. One file, because it has to reach every
2
+ * bundle the kit publishes and a second copy would drift; a consumer who imports
3
+ * both bundles gets it twice, which costs nothing, since every rule here is
4
+ * idempotent and `!important`.
5
+ *
6
+ * `iteration-count: 1` lets one-shot animations settle on their final frame — a
7
+ * checkmark stays drawn — instead of snapping back to the start.
8
+ *
9
+ * why: docs/specification.md#reduced-motion-travels-with-the-stylesheet
10
+ * why: docs/specification.md#motion */
11
+
12
+ @media (prefers-reduced-motion: reduce) {
13
+ html { scroll-behavior: auto !important; }
14
+ *, ::before, ::after {
15
+ animation-duration: 0.01ms !important;
16
+ animation-iteration-count: 1 !important;
17
+ transition-duration: 0.01ms !important;
18
+ scroll-behavior: auto !important;
19
+ }
20
+ [data-reveal] { opacity: 1 !important; transform: none !important; }
21
+ }
@@ -35,13 +35,13 @@
35
35
  .ui-sx__glow--a {
36
36
  width: 58%; top: -14%; left: 50%; translate: -50% 0;
37
37
  background: radial-gradient(circle at center, var(--sx-tint), transparent 68%);
38
- animation: ui-sx-float 14s ease-in-out infinite alternate;
38
+ animation: ui-sx-float 14s ease-in-out infinite alternate; /* motion: ambient — a background glow that drifts on its own clock; `alternate` needs a symmetric curve, which var(--ease) is not */
39
39
  }
40
40
  .ui-sx__glow--b {
41
41
  width: 52%; bottom: -20%; left: 12%;
42
42
  background: radial-gradient(circle at center,
43
43
  color-mix(in srgb, var(--accent) 30%, transparent), transparent 68%);
44
- animation: ui-sx-float 18s ease-in-out -6s infinite alternate;
44
+ animation: ui-sx-float 18s ease-in-out -6s infinite alternate; /* motion: ambient — the second glow, offset in period and phase so the pair never lines up */
45
45
  }
46
46
  :root[data-theme="light"] .ui-sx__glow { opacity: 0.7; }
47
47
  @keyframes ui-sx-float {
@@ -63,7 +63,7 @@
63
63
  letter-spacing: -.02em; line-height: 1.2; margin: 0;
64
64
  }
65
65
  .ui-sx__body {
66
- color: var(--dim); font-size: 14.5px; line-height: 1.6; margin: 9px 0 0; max-width: 44ch;
66
+ color: var(--dim); font-size: 14.5px; line-height: 1.6; margin: 9px 0 0; max-width: var(--prose-caption);
67
67
  }
68
68
  .ui-sx__actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
69
69
 
@@ -104,21 +104,21 @@
104
104
  /* Soft tinted disc that springs in behind the tick. */
105
105
  .ui-sx__disc {
106
106
  fill: var(--sx-tint); stroke: var(--sx-track); stroke-width: 1.5;
107
- transform-origin: center; animation: ui-sx-pop .5s cubic-bezier(.22,1,.3,1) both;
107
+ transform-origin: center; animation: ui-sx-pop .5s cubic-bezier(.22,1,.3,1) both; /* motion: choreographed — first beat of the check: disc, then tick at .28s, then ring at .2s */
108
108
  }
109
109
  /* The tick, stroked on via dash-offset (path length ~34). */
110
110
  .ui-sx__tick {
111
111
  fill: none; stroke: var(--sx-green); stroke-width: 5;
112
112
  stroke-linecap: round; stroke-linejoin: round;
113
113
  stroke-dasharray: 34; stroke-dashoffset: 34;
114
- animation: ui-sx-draw .5s cubic-bezier(.6,0,.35,1) .28s forwards;
114
+ animation: ui-sx-draw .5s cubic-bezier(.6,0,.35,1) .28s forwards; /* motion: choreographed — the tick draws while the disc is still settling */
115
115
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--green) 55%, transparent));
116
116
  }
117
117
  /* Burst ring that expands and fades once, framing the moment. */
118
118
  .ui-sx__ring {
119
119
  fill: none; stroke: var(--sx-green); stroke-width: 2;
120
120
  transform-origin: center; opacity: 0;
121
- animation: ui-sx-burst .7s cubic-bezier(.3,.7,.4,1) .2s forwards;
121
+ animation: ui-sx-burst .7s cubic-bezier(.3,.7,.4,1) .2s forwards; /* motion: choreographed — the ring outlives the tick so the moment ends on the frame */
122
122
  }
123
123
 
124
124
  @keyframes ui-sx-pop {
@@ -137,7 +137,7 @@
137
137
  .ui-sx__piece {
138
138
  position: absolute; top: -8%; width: 8px; height: 8px; border-radius: 2px;
139
139
  opacity: 0; transform-origin: center;
140
- animation: ui-sx-fall 2.4s cubic-bezier(.2,.6,.35,1) var(--sx-d, 0s) forwards;
140
+ animation: ui-sx-fall 2.4s cubic-bezier(.2,.6,.35,1) var(--sx-d, 0s) forwards; /* motion: ambient — fall time is set by the height of the panel, not by an interaction */
141
141
  scale: var(--sx-s, 1);
142
142
  }
143
143
  .ui-sx__piece--a { background: var(--accent); }
@@ -162,7 +162,7 @@
162
162
  background: conic-gradient(var(--green) var(--sx-a, 360deg), var(--surface-2) 0);
163
163
  mask: radial-gradient(circle, transparent 5px, #000 6px);
164
164
  -webkit-mask: radial-gradient(circle, transparent 5px, #000 6px);
165
- animation: ui-sx-sweep var(--sx-secs, 5s) linear forwards;
165
+ animation: ui-sx-sweep var(--sx-secs, 5s) linear forwards; /* motion: ambient — the caller sets the countdown; the ring just spends it */
166
166
  }
167
167
  @keyframes ui-sx-sweep { from { --sx-a: 360deg; } to { --sx-a: 0deg; } }
168
168
  .ui-sx__count-text { color: var(--muted); font-size: 13px; }
@@ -15,11 +15,11 @@
15
15
  text-transform: uppercase;
16
16
  color: var(--muted);
17
17
  font-weight: var(--weight-semibold);
18
- padding: 0 16px 11px 0;
18
+ padding: 0 var(--space-4) var(--space-3) 0;
19
19
  border-bottom: 1px solid var(--border);
20
20
  }
21
21
  .ui-table td {
22
- padding: 15px 16px 15px 0;
22
+ padding: var(--space-4) var(--space-4) var(--space-4) 0;
23
23
  border-bottom: 1px solid var(--border);
24
24
  color: var(--dim);
25
25
  vertical-align: middle;
@@ -39,11 +39,13 @@
39
39
  * continuous while letting the row ends round; table padding (which only applies
40
40
  * in the separate model) insets the rows so the fill clears the container edge.
41
41
  * Zebra tables keep the original full-bleed tint (finance ledger), so they're
42
- * excluded here — their hover rule lives further down. */
42
+ * excluded here — their hover rule lives further down.
43
+ * The inset was 6px, which is a tie between --space-1 and --space-2; it rounds
44
+ * up, because what it buys is clearance and clearance rounds away from the edge. */
43
45
  .ui-table--hover:not(.ui-table--zebra) {
44
46
  border-collapse: separate;
45
47
  border-spacing: 0;
46
- padding: 0 6px;
48
+ padding: 0 var(--space-2);
47
49
  }
48
50
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td { background: var(--surface-2); }
49
51
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td:first-child {
@@ -51,19 +53,20 @@
51
53
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td:last-child {
52
54
  border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
53
55
 
54
- /* ── Data-table modifiers (composable) ──────────────────────────────────────
55
- * Compose for a dense financial ledger: .ui-table--dense --zebra --hover.
56
- * --dense compact row rhythm for many-row / many-column data
57
- * --zebra row striping instead of per-row rules; 2px header rule; the
58
- * (accent) hover tint out-ranks the stripe (defined after it)
59
- * Cell helpers: .ui-table__num (right, tabular), .ui-table__code (plain
60
- * monospace id — never a boxed code chip inside a table). */
61
- .ui-table--dense th { padding: 0 14px 10px 0; }
62
- .ui-table--dense td { padding: 10px 14px; font-size: var(--text-sm); white-space: nowrap; }
56
+ /* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
57
+ * --dense --space-3 across, --space-2 down one step in from the base
58
+ * table, and both round DOWN, because the modifier exists to fit
59
+ * more rows and rounding up spends the distinction it is for.
60
+ * --zebra row striping; 2px header rule; the hover tint out-ranks the stripe
61
+ * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
62
+ * why: docs/specification.md#spacing-and-rhythm
63
+ * held by: stories/table-rhythm.test.js */
64
+ .ui-table--dense th { padding: 0 var(--space-3) var(--space-2) 0; }
65
+ .ui-table--dense td { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); white-space: nowrap; }
63
66
  .ui-table--dense th:first-child,
64
- .ui-table--dense td:first-child { padding-left: 12px; }
67
+ .ui-table--dense td:first-child { padding-left: var(--space-3); }
65
68
  .ui-table--dense th:last-child,
66
- .ui-table--dense td:last-child { padding-right: 12px; }
69
+ .ui-table--dense td:last-child { padding-right: var(--space-3); }
67
70
 
68
71
  .ui-table--zebra th { border-bottom-width: 2px; }
69
72
  .ui-table--zebra td { border-bottom: 0; }
@@ -78,9 +81,9 @@
78
81
  * checkboxes sat flush against the tint with a measured 0px gap.
79
82
  * Header and body are inset together, or the columns stop lining up. */
80
83
  .ui-table--zebra th:first-child,
81
- .ui-table--zebra td:first-child { padding-left: 12px; }
84
+ .ui-table--zebra td:first-child { padding-left: var(--space-3); }
82
85
  .ui-table--zebra th:last-child,
83
- .ui-table--zebra td:last-child { padding-right: 12px; }
86
+ .ui-table--zebra td:last-child { padding-right: var(--space-3); }
84
87
  .ui-table--zebra.ui-table--hover tbody tr:hover td,
85
88
  .ui-table--zebra tbody tr:hover td { background: color-mix(in srgb, var(--accent) 8%, transparent); }
86
89
 
@@ -14,7 +14,7 @@
14
14
  }
15
15
  .topbar__in {
16
16
  width: 100%;
17
- max-width: 1180px;
17
+ max-width: var(--container);
18
18
  margin: 0 auto;
19
19
  padding: 0 clamp(14px, 2.4vw, 26px);
20
20
  display: flex;
@@ -59,7 +59,7 @@
59
59
  color: var(--muted);
60
60
  padding: 4px 13px;
61
61
  border-radius: 24px;
62
- transition: color 0.2s ease, background 0.2s ease;
62
+ transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
63
63
  }
64
64
  .dtsw a:hover { color: var(--text); }
65
65
  .dtsw a.cur { color: var(--strong); background: var(--surface); box-shadow: var(--shadow-sm); }
@@ -75,11 +75,14 @@
75
75
  color: var(--text);
76
76
  border: 1px solid var(--border);
77
77
  border-radius: 10px;
78
- transition: transform 0.15s ease, background 0.35s ease, border-color 0.35s ease;
78
+ transition:
79
+ transform var(--dur-fast) var(--ease),
80
+ background var(--dur-fast) var(--ease),
81
+ border-color var(--dur-fast) var(--ease);
79
82
  }
80
83
  .toggle:hover { transform: translateY(-1px); border-color: var(--border-strong); }
81
84
  .toggle .ic { width: 17px; height: 17px; display: inline-block; color: var(--accent); }
82
- .toggle .ic svg { display: block; width: 100%; height: 100%; }
85
+ .toggle .ic svg { display: block; width: 100%; height: 100%; stroke-width: 2.2; }
83
86
 
84
87
  /* Version switcher */
85
88
  .vsw { position: relative; }
@@ -96,7 +99,7 @@
96
99
  border: 1px solid var(--border);
97
100
  border-radius: 24px;
98
101
  padding: 7px 11px 7px 13px;
99
- transition: border-color 0.2s ease, background 0.2s ease;
102
+ transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
100
103
  }
101
104
  .vsw__btn:hover { border-color: var(--accent); }
102
105
  .vsw__btn .lbl { color: var(--muted); }
@@ -106,7 +109,7 @@
106
109
  border-right: 1.6px solid var(--muted);
107
110
  border-bottom: 1.6px solid var(--muted);
108
111
  transform: rotate(45deg) translateY(-1px);
109
- transition: transform 0.2s ease;
112
+ transition: transform var(--dur-fast) var(--ease);
110
113
  }
111
114
  .vsw.open .vsw__btn .car { transform: rotate(-135deg) translateY(2px); }
112
115
  .vsw__menu {
@@ -122,7 +125,13 @@
122
125
  opacity: 0;
123
126
  visibility: hidden;
124
127
  transform: translateY(-6px);
125
- transition: 0.18s ease;
128
+ /* Named properties, not the bare shorthand: `transition: 0.18s` is `all`, and `all`
129
+ includes visibility — held at the OLD value for the whole duration, so the menu is
130
+ still `hidden` in the frame it opens. */
131
+ transition:
132
+ opacity var(--dur-med) var(--ease),
133
+ transform var(--dur-med) var(--ease),
134
+ visibility var(--dur-med) linear;
126
135
  z-index: var(--z-dropdown);
127
136
  }
128
137
  .vsw.open .vsw__menu { opacity: 1; visibility: visible; transform: translateY(0); }
@@ -133,7 +142,7 @@
133
142
  padding: 10px 13px;
134
143
  border-radius: 10px;
135
144
  cursor: pointer;
136
- transition: background 0.15s ease;
145
+ transition: background var(--dur-fast) var(--ease);
137
146
  }
138
147
  .vopt:hover { background: var(--surface); }
139
148
  .vopt .vname { font-family: var(--font-sans); font-size: 12.5px; color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
@@ -151,7 +160,9 @@
151
160
  }
152
161
  .vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
153
162
  .vbadge.arch { color: var(--muted); background: var(--surface); }
154
- @media (max-width: 600px) { .vsw__btn .lbl { display: none; } }
163
+ /* The switcher keeps its glyph and drops its label where the shell folds — the
164
+ same step the rail folds at: docs/specification.md#breakpoints. */
165
+ @media (max-width: 720px) { .vsw__btn .lbl { display: none; } }
155
166
 
156
167
  /* Account — avatar + dropdown. Gated: hidden until the app confirms a session
157
168
  and adds `.on` (mirrors the portal's /auth/me reveal). */
@@ -169,7 +180,7 @@
169
180
  place-items: center;
170
181
  cursor: pointer;
171
182
  letter-spacing: 0.02em;
172
- transition: border-color 0.2s ease;
183
+ transition: border-color var(--dur-fast) var(--ease);
173
184
  }
174
185
  .acct .avatar:hover { border-color: var(--accent); }
175
186
  .amenu {
@@ -184,7 +195,11 @@
184
195
  opacity: 0;
185
196
  visibility: hidden;
186
197
  transform: translateY(-6px);
187
- transition: 0.16s ease;
198
+ /* Named properties — see .vsw__menu above. */
199
+ transition:
200
+ opacity var(--dur-med) var(--ease),
201
+ transform var(--dur-med) var(--ease),
202
+ visibility var(--dur-med) linear;
188
203
  z-index: var(--z-dropdown);
189
204
  }
190
205
  .acct.open .amenu { opacity: 1; visibility: visible; transform: translateY(0); }
@@ -196,7 +211,7 @@
196
211
  .amenu a { display: flex; align-items: center; gap: 11px; padding: 10px 15px; color: var(--text); text-decoration: none; font-family: var(--font-sans); font-size: 13px; cursor: pointer; }
197
212
  .amenu a:hover { background: var(--surface); }
198
213
  .amenu a.cur { color: var(--accent); }
199
- .amenu a svg { width: 19px; height: 19px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; opacity: 0.85; }
214
+ .amenu a svg { width: 19px; height: 19px; flex: none; stroke: currentColor; fill: none; stroke-width: 2; opacity: 0.85; }
200
215
  .amenu .asep { height: 1px; background: var(--border); margin: 5px 0; }
201
216
  .amenu a.aout { color: var(--muted); }
202
217
  .amenu a.aout:hover { color: var(--pink); }
@@ -1,17 +1,19 @@
1
- /* ============================================================================
2
- * Accent sub-themes — an orthogonal `data-accent` dimension on top of
3
- * `data-theme` (dark/light). Set both on <html>:
1
+ /* Accent sub-themes — an orthogonal `data-accent` dimension on top of
2
+ * `data-theme`. Set both on <html>:
4
3
  *
5
4
  * <html data-theme="dark" data-accent="phoenix">
6
5
  *
7
- * Each sub-theme only re-points the accent family (--accent, --purple*,
8
- * --glow-purple, --ring, --grad-*). Surfaces, text and signal colours (green =
9
- * live, pink = danger) stay put, so every accent works in light AND dark and
10
- * every component follows with zero component-level changes.
6
+ * Each sub-theme only re-points the accent family. Surfaces, text and signal
7
+ * colours (green = live, pink = danger) stay put, so every accent works in
8
+ * light AND dark and every component follows with no component-level change.
9
+ *
10
+ * --ring is NOT here: it is declared once in tokens.css as var(--accent), so
11
+ * re-pointing --accent re-points the focus ring too.
11
12
  *
12
13
  * Selectors carry two attributes + :root, so they always out-specify the
13
14
  * single-attribute theme blocks in tokens.css regardless of import order.
14
- * ========================================================================== */
15
+ *
16
+ * why: docs/specification.md#the-focus-ring */
15
17
 
16
18
  /* ---- Phoenix — ember / rising fire (the strategy's namesake) ------------- */
17
19
  :root[data-theme="dark"][data-accent="phoenix"] {
@@ -25,7 +27,6 @@
25
27
  but not its own wash over a card, which lifts the ground toward the ink. The
26
28
  hue is untouched. See issue #157. */
27
29
  --glow-purple: rgba(255, 106, 61, 0.15);
28
- --ring: 0 0 0 3px rgba(255, 106, 61, 0.38);
29
30
  --grad-from: #ff7a45;
30
31
  --grad-to: #ffcf6a;
31
32
  }
@@ -39,7 +40,6 @@
39
40
  --accent-strong: var(--accent);
40
41
  --accent-contrast: #ffffff;
41
42
  --glow-purple: rgba(214, 74, 18, 0.10);
42
- --ring: 0 0 0 3px rgba(214, 74, 18, 0.28);
43
43
  --grad-from: #d64a12;
44
44
  --grad-to: #e8992a;
45
45
  }
@@ -55,7 +55,6 @@
55
55
  /* Thinned from 0.18 for WCAG AA, the same wash-over-a-card pair as dark
56
56
  Phoenix and from the same alpha. The hue is untouched. See issue #157. */
57
57
  --glow-purple: rgba(59, 157, 255, 0.15);
58
- --ring: 0 0 0 3px rgba(59, 157, 255, 0.38);
59
58
  --grad-from: #3b9dff;
60
59
  --grad-to: #20dcf5;
61
60
  }
@@ -83,7 +82,6 @@
83
82
  rather than the accent — in light a glow tints --purple-light, which
84
83
  stories/signal-contrast.test.js holds. See issue #157. */
85
84
  --glow-purple: rgba(21, 104, 214, 0.10);
86
- --ring: 0 0 0 3px rgba(21, 104, 214, 0.28);
87
85
  --grad-from: #1568d6;
88
86
  --grad-to: #17a2c0;
89
87
  }
@@ -97,7 +95,6 @@
97
95
  --accent-strong: var(--accent);
98
96
  --accent-contrast: #04180f;
99
97
  --glow-purple: rgba(22, 201, 138, 0.18);
100
- --ring: 0 0 0 3px rgba(22, 201, 138, 0.38);
101
98
  --grad-from: #16c98a;
102
99
  --grad-to: #20dcf5;
103
100
  }
@@ -113,7 +110,6 @@
113
110
  /* Thinned from 0.10 for WCAG AA, the same grey-panel pair as light Ocean and
114
111
  from the same alpha. See issue #157. */
115
112
  --glow-purple: rgba(11, 156, 104, 0.08);
116
- --ring: 0 0 0 3px rgba(11, 156, 104, 0.28);
117
113
  --grad-from: #0b9c68;
118
114
  --grad-to: #12b0a0;
119
115
  }