@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,25 +1,22 @@
1
- /* ============================================================================
2
- * Confirm the kit's confirmation dialog: a centred panel over a scrim, for
3
- * the question a destructive action has to ask. Fully token-driven so it
4
- * re-themes across accents + light/dark. Shares its scrim + focus-trap
5
- * behaviour with the drawer (see overlay.js).
1
+ /* Confirm — the kit's confirmation dialog: a centred panel over a scrim, for the
2
+ * question a destructive action has to ask. Fully token-driven, and it shares
3
+ * its scrim and focus-trap behaviour with the drawer (see overlay.js).
6
4
  *
7
- * Motion is the drawer's, deliberately: --dur-med/--ease, and `visibility`
8
- * switched rather than transitioned. A transitioned visibility is still
5
+ * `visibility` is SWITCHED rather than transitioned: transitioned, it is still
9
6
  * `hidden` in the frame the class lands, so openConfirm()'s focus() would have
10
- * nothing to focus and the reader would be left on <body>. The root owns the
11
- * switch; the panel and scrim inherit it.
7
+ * nothing to focus. The root owns the switch; panel and scrim inherit it.
12
8
  *
13
- * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
+ * Local tokens so the panel and scrim re-theme cleanly:
14
10
  * --confirm-surface panel background --confirm-shadow panel elevation
15
- * --confirm-scrim backdrop colour (--scrim already re-themes per theme)
16
- * ========================================================================== */
11
+ * --confirm-scrim backdrop colour
12
+ *
13
+ * why: docs/specification.md#motion */
17
14
 
18
15
  .ui-confirm {
19
16
  --confirm-surface: var(--surface-2);
20
17
  --confirm-shadow: var(--shadow-lg);
21
18
  --confirm-scrim: var(--scrim);
22
- --confirm-w: 420px;
19
+ --confirm-w: var(--panel-md);
23
20
 
24
21
  position: fixed;
25
22
  inset: 0;
@@ -14,7 +14,7 @@
14
14
  --drawer-surface: var(--surface-2);
15
15
  --drawer-shadow: var(--shadow-lg);
16
16
  --drawer-scrim: var(--scrim);
17
- --drawer-w: 420px; /* left|right panels: width */
17
+ --drawer-w: var(--panel-md); /* left|right panels: width */
18
18
  --drawer-h: 45vh; /* top|bottom panels: height */
19
19
 
20
20
  position: fixed;
@@ -102,9 +102,9 @@
102
102
 
103
103
  /* -- Sizes ----------------------------------------------------------------- */
104
104
  /* left|right → width; top|bottom → height. Same names, axis chosen by side. */
105
- .ui-drawer--right.ui-drawer--sm, .ui-drawer--left.ui-drawer--sm { --drawer-w: 320px; }
106
- .ui-drawer--right.ui-drawer--md, .ui-drawer--left.ui-drawer--md { --drawer-w: 420px; }
107
- .ui-drawer--right.ui-drawer--lg, .ui-drawer--left.ui-drawer--lg { --drawer-w: 560px; }
105
+ .ui-drawer--right.ui-drawer--sm, .ui-drawer--left.ui-drawer--sm { --drawer-w: var(--panel-sm); }
106
+ .ui-drawer--right.ui-drawer--md, .ui-drawer--left.ui-drawer--md { --drawer-w: var(--panel-md); }
107
+ .ui-drawer--right.ui-drawer--lg, .ui-drawer--left.ui-drawer--lg { --drawer-w: var(--panel-lg); }
108
108
  .ui-drawer--top.ui-drawer--sm, .ui-drawer--bottom.ui-drawer--sm { --drawer-h: 30vh; }
109
109
  .ui-drawer--top.ui-drawer--md, .ui-drawer--bottom.ui-drawer--md { --drawer-h: 45vh; }
110
110
  .ui-drawer--top.ui-drawer--lg, .ui-drawer--bottom.ui-drawer--lg { --drawer-h: 72vh; }
@@ -138,11 +138,14 @@
138
138
  background: transparent;
139
139
  border: 1px solid transparent;
140
140
  border-radius: var(--radius-sm);
141
- transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
141
+ transition:
142
+ color var(--dur-fast) var(--ease),
143
+ background var(--dur-fast) var(--ease),
144
+ border-color var(--dur-fast) var(--ease);
142
145
  }
143
146
  .ui-drawer__close:hover { color: var(--strong); background: var(--surface); }
144
147
  .ui-drawer__close:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
145
- .ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
148
+ .ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.1; }
146
149
 
147
150
  .ui-drawer__body {
148
151
  flex: 1 1 auto;
@@ -21,7 +21,7 @@
21
21
  border: 1px solid var(--border);
22
22
  border-radius: var(--radius-pill);
23
23
  padding: 7px 11px 7px 13px;
24
- transition: border-color 0.2s ease, background 0.2s ease;
24
+ transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
25
25
  }
26
26
  .ui-dropdown__trigger:hover { border-color: var(--accent); }
27
27
  .ui-dropdown__trigger:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
@@ -34,7 +34,7 @@
34
34
  border-right: 1.6px solid var(--muted);
35
35
  border-bottom: 1.6px solid var(--muted);
36
36
  transform: rotate(45deg) translateY(-1px);
37
- transition: transform 0.2s ease;
37
+ transition: transform var(--dur-fast) var(--ease);
38
38
  flex: none;
39
39
  }
40
40
  .ui-dropdown.open .ui-dropdown__chevron { transform: rotate(-135deg) translateY(2px); }
@@ -53,7 +53,12 @@
53
53
  opacity: 0;
54
54
  visibility: hidden;
55
55
  transform: translateY(-6px);
56
- transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
56
+ transition:
57
+ opacity var(--dur-med) var(--ease),
58
+ transform var(--dur-med) var(--ease),
59
+ /* `linear`, not var(--ease): visibility is discrete, so easing it buys nothing and a
60
+ curve leaving [0, 1] flips it mid-fade. Same rule the drawer states at length. */
61
+ visibility var(--dur-med) linear;
57
62
  z-index: var(--z-dropdown);
58
63
  }
59
64
  .ui-dropdown__panel.is-end { left: auto; right: 0; }
@@ -70,7 +75,7 @@
70
75
  cursor: pointer;
71
76
  text-decoration: none;
72
77
  color: var(--text);
73
- transition: background 0.15s ease;
78
+ transition: background var(--dur-fast) var(--ease);
74
79
  }
75
80
  .ui-dropdown__item:hover { background: var(--surface); }
76
81
  .ui-dropdown__item:focus-visible { outline: none; background: var(--surface); box-shadow: var(--ring); }
@@ -78,11 +83,11 @@
78
83
  .ui-dropdown__label { font-family: var(--font-sans); font-size: 12.5px; color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
79
84
  .ui-dropdown__desc { font-size: 11px; color: var(--muted); line-height: 1.35; }
80
85
  .ui-dropdown__ic { display: inline-flex; flex: none; color: var(--muted); }
81
- .ui-dropdown__ic svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
86
+ .ui-dropdown__ic svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.1; }
82
87
 
83
88
  /* Trailing selected tick (listbox variant) */
84
89
  .ui-dropdown__tick { margin-left: auto; flex: none; display: none; color: var(--accent); }
85
- .ui-dropdown__tick svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.2; }
90
+ .ui-dropdown__tick svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.4; }
86
91
  .ui-dropdown__item.is-selected .ui-dropdown__tick { display: inline-flex; }
87
92
  .ui-dropdown__item.is-selected .ui-dropdown__label { color: var(--accent); }
88
93
 
@@ -106,7 +111,9 @@
106
111
  .ui-dropdown__badge.is-accent { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
107
112
 
108
113
  /* Disabled + danger rows */
109
- .ui-dropdown__item.is-disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
114
+ .ui-dropdown__item.is-disabled { color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
115
+ .ui-dropdown__item.is-disabled .ui-dropdown__label,
116
+ .ui-dropdown__item.is-disabled .ui-dropdown__desc { color: inherit; }
110
117
  .ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
111
118
  .ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--pink); }
112
119
 
@@ -56,7 +56,7 @@
56
56
  }
57
57
  .ui-empty__sub {
58
58
  font-size: var(--text-sm);
59
- max-width: 340px;
59
+ max-width: var(--prose-caption);
60
60
  margin: 0;
61
61
  line-height: 1.55;
62
62
  }
@@ -27,7 +27,10 @@
27
27
  box-shadow: 0 10px 26px var(--ui-fb-pill-glow),
28
28
  0 2px 6px color-mix(in srgb, var(--shadow-ink) 28%, transparent),
29
29
  inset 0 1px 0 color-mix(in srgb, var(--sheen) 28%, transparent);
30
- transition: transform .2s var(--easing-spring, cubic-bezier(.34,1.56,.64,1)), opacity .16s ease, box-shadow .2s ease;
30
+ transition:
31
+ transform var(--dur-fast) var(--ease-spring),
32
+ opacity var(--dur-fast) var(--ease),
33
+ box-shadow var(--dur-fast) var(--ease);
31
34
  }
32
35
  .ui-fbpill.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -100%) translateY(-9px) scale(1); }
33
36
  .ui-fbpill:hover {
@@ -37,19 +40,19 @@
37
40
  }
38
41
  .ui-fbpill.show:hover { transform: translate(-50%, -100%) translateY(-12px) scale(1.04); }
39
42
  .ui-fbpill:active { transform: translate(-50%, -100%) translateY(-8px) scale(.98); }
40
- .ui-fbpill svg { width: 16px; height: 16px; color: currentColor; filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--shadow-ink) 25%, transparent)); }
43
+ .ui-fbpill svg { width: 16px; height: 16px; stroke-width: 2.4; color: currentColor; filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--shadow-ink) 25%, transparent)); }
41
44
  .ui-fbpill::before {
42
45
  content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
43
46
  background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--sheen) 55%, transparent), transparent); transform: skewX(-18deg);
44
47
  }
45
- .ui-fbpill.show::before { animation: uiFbSheen 1.15s ease .12s 1; }
46
- .ui-fbpill:hover::before { animation: uiFbSheen .7s ease 1; }
48
+ .ui-fbpill.show::before { animation: uiFbSheen 1.15s ease .12s 1; } /* motion: ambient — a decorative sheen crossing the pill; its length is the width it travels, not a response */
49
+ .ui-fbpill:hover::before { animation: uiFbSheen .7s ease 1; } /* motion: ambient — the same sweep, quicker on hover so it finishes inside the gesture */
47
50
  @keyframes uiFbSheen { 0% { left: -60%; } 100% { left: 130%; } }
48
51
 
49
52
  /* Scrim behind the open composer */
50
53
  .ui-fbscrim {
51
54
  position: fixed; inset: 0; z-index: 70; background: var(--scrim);
52
- opacity: 0; pointer-events: none; transition: opacity .2s ease;
55
+ opacity: 0; pointer-events: none; transition: opacity var(--dur-med) var(--ease);
53
56
  }
54
57
  .ui-fbscrim.show { opacity: 1; pointer-events: auto; }
55
58
 
@@ -58,47 +61,53 @@
58
61
  position: fixed; z-index: 71; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(14px);
59
62
  width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); border-radius: 18px;
60
63
  box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none;
61
- transition: opacity .2s ease, transform .2s ease; overflow: hidden;
64
+ transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease); overflow: hidden;
62
65
  }
63
66
  .ui-fbcomposer.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
64
67
 
65
68
  .ui-fbc__head { display: flex; align-items: center; gap: 12px; padding: 17px 20px 0; }
66
69
  .ui-fbc__chip { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
67
- .ui-fbc__chip svg { width: 14px; height: 14px; flex: none; opacity: .85; }
70
+ .ui-fbc__chip svg { width: 14px; height: 14px; flex: none; opacity: .85; stroke-width: 2.7; }
68
71
  .ui-fbc__x { margin-left: auto; background: var(--surface-2); color: var(--dim); border: 0; width: 28px; height: 28px; border-radius: 8px; cursor: pointer; display: grid; place-items: center; flex: none; }
69
72
  .ui-fbc__x:hover { color: var(--strong); }
70
- .ui-fbc__x svg { width: 14px; height: 14px; }
73
+ .ui-fbc__x svg { width: 14px; height: 14px; stroke-width: 2.7; }
71
74
  .ui-fbc__quote { margin: 14px 20px 0; background: var(--surface-2); border-radius: 12px; padding: 12px 15px; display: flex; gap: 11px; }
72
75
  .ui-fbc__qm { color: var(--accent); font-size: 22px; font-weight: var(--weight-bold); line-height: .9; flex: none; opacity: .55; }
73
76
  .ui-fbc__quote q { quotes: none; color: var(--dim); font-style: italic; font-size: 13.5px; line-height: 1.55; }
74
77
  .ui-fbc__body { padding: 14px 20px 0; }
75
78
  .ui-fbcomposer textarea {
76
79
  width: 100%; min-height: 96px; resize: vertical; font-family: var(--font-sans); font-size: 14.5px; line-height: 1.55;
77
- color: var(--strong); background: var(--surface-2); border: 0; border-radius: 12px; padding: 13px 15px; transition: box-shadow .18s ease;
80
+ color: var(--strong); background: var(--surface-2); border: 0; border-radius: 12px; padding: 13px 15px; transition: box-shadow var(--dur-fast) var(--ease);
78
81
  }
79
82
  .ui-fbcomposer textarea::placeholder { color: var(--muted); }
80
83
  .ui-fbcomposer textarea:focus { outline: none; box-shadow: 0 0 0 2px var(--accent); }
81
84
  .ui-fbc__foot { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 16px 20px 18px; }
82
85
 
83
- .ui-fbbtn { font-family: var(--font-sans); font-size: 13.5px; font-weight: var(--weight-semibold); border-radius: 10px; padding: 9px 17px; cursor: pointer; border: 0; transition: filter .15s, background .15s, color .15s; }
86
+ .ui-fbbtn { font-family: var(--font-sans); font-size: 13.5px; font-weight: var(--weight-semibold); border-radius: 10px; padding: 9px 17px; cursor: pointer; border: 0; transition: filter var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
84
87
  .ui-fbbtn.ghost { background: transparent; color: var(--dim); }
85
88
  .ui-fbbtn.ghost:hover { color: var(--strong); background: var(--surface-2); }
86
89
  .ui-fbbtn.primary { background: var(--accent-strong); color: var(--accent-contrast); display: inline-flex; align-items: center; gap: 9px; }
87
90
  .ui-fbbtn.primary:hover { filter: brightness(1.08); }
88
- .ui-fbbtn.primary:disabled { opacity: .6; cursor: default; filter: none; }
91
+ /* Same flat pair as .ui-btn:disabled, for the same reason (#220). */
92
+ .ui-fbbtn.primary:disabled {
93
+ background: var(--disabled-surface);
94
+ color: var(--disabled-ink);
95
+ cursor: default;
96
+ filter: none;
97
+ }
89
98
  .ui-fbc__send { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
90
- .ui-fbspin { flex: none; width: 14px; height: 14px; box-sizing: border-box; border-radius: 50%; border: 2px solid color-mix(in srgb, currentColor 40%, transparent); border-top-color: currentColor; animation: uiFbSpin .7s linear infinite; }
99
+ .ui-fbspin { flex: none; width: 14px; height: 14px; box-sizing: border-box; border-radius: 50%; border: 2px solid color-mix(in srgb, currentColor 40%, transparent); border-top-color: currentColor; animation: uiFbSpin .7s linear infinite; } /* motion: ambient — a spinner turns until the request answers */
91
100
  @keyframes uiFbSpin { to { transform: rotate(360deg); } }
92
101
 
93
102
  /* Success state */
94
103
  .ui-fbc__done { padding: 30px 26px 24px; text-align: center; background: radial-gradient(112% 60% at 50% 0%, var(--glow-green), transparent 62%); }
95
104
  .ui-fbc__ck { width: 88px; height: 88px; margin: 0 auto 13px; display: grid; place-items: center; }
96
- .ui-fbck { width: 88px; height: 88px; overflow: visible; display: block; transform-origin: center; animation: uiFbCkSpring 1s cubic-bezier(.22,1,.3,1) both; }
105
+ .ui-fbck { width: 88px; height: 88px; overflow: visible; display: block; transform-origin: center; animation: uiFbCkSpring 1s cubic-bezier(.22,1,.3,1) both; } /* motion: choreographed — the frame the mark and the comet are timed against */
97
106
  .ui-fbck-t { fill: none; stroke: var(--surface-2); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
98
- .ui-fbck-m { fill: none; stroke: var(--green); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 118; stroke-dashoffset: 118; animation: uiFbCkDraw .6s cubic-bezier(.5,0,.3,1) .1s forwards; }
99
- .ui-fbck-s { fill: none; stroke: var(--sheen); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 15 400; stroke-dashoffset: 133; filter: drop-shadow(0 0 8px var(--green)) drop-shadow(0 0 15px var(--green)); opacity: 0; animation: uiFbCkComet .6s cubic-bezier(.5,0,.3,1) .1s forwards; }
107
+ .ui-fbck-m { fill: none; stroke: var(--green); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 118; stroke-dashoffset: 118; animation: uiFbCkDraw .6s cubic-bezier(.5,0,.3,1) .1s forwards; /* motion: choreographed — the mark draws while the spring is still settling */ }
108
+ .ui-fbck-s { fill: none; stroke: var(--sheen); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 15 400; stroke-dashoffset: 133; filter: drop-shadow(0 0 8px var(--green)) drop-shadow(0 0 15px var(--green)); opacity: 0; animation: uiFbCkComet .6s cubic-bezier(.5,0,.3,1) .1s forwards; /* motion: choreographed — the comet rides the mark, so it shares its duration and delay exactly */ }
100
109
  .ui-fbc__done h4 { color: var(--strong); font-size: 19px; font-weight: var(--weight-semibold); letter-spacing: -.01em; margin-bottom: 7px; }
101
- .ui-fbc__done p { color: var(--dim); font-size: 13.5px; line-height: 1.55; max-width: 38ch; margin: 0 auto; }
110
+ .ui-fbc__done p { color: var(--dim); font-size: 13.5px; line-height: 1.55; max-width: var(--prose-caption); margin: 0 auto; }
102
111
  .ui-fbc__done p b { color: var(--text); font-weight: var(--weight-semibold); }
103
112
  .ui-fbc__done .row { margin-top: 18px; display: flex; gap: 10px; justify-content: center; }
104
113
  @keyframes uiFbCkDraw { to { stroke-dashoffset: 0; } }
@@ -21,7 +21,7 @@
21
21
  }
22
22
  .ui-footer__in {
23
23
  width: 100%;
24
- max-width: 1180px;
24
+ max-width: var(--container);
25
25
  margin: 0 auto;
26
26
  padding: 40px clamp(14px, 2.4vw, 26px) 30px;
27
27
  }
@@ -34,6 +34,10 @@
34
34
  flex-wrap: wrap;
35
35
  padding-bottom: 30px;
36
36
  }
37
+ /* Under --panel-sm, and deliberately: this is a flex TRACK in a wrapping
38
+ row, not a panel and not a paragraph — its width is what decides when the
39
+ footer breaks into columns, so it answers to the row rather than to a
40
+ scale. The one width below the floor the measure gate draws. See #208. */
37
41
  .ui-footer__brand { max-width: 300px; }
38
42
  .ui-footer .ui-footer__brand .brand { font-size: 15px; }
39
43
  .ui-footer__tagline {
@@ -52,7 +56,10 @@
52
56
  border: 1px solid var(--footer-border);
53
57
  color: var(--footer-link);
54
58
  background: var(--surface);
55
- transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
59
+ transition:
60
+ color var(--dur-fast) var(--ease),
61
+ border-color var(--dur-fast) var(--ease),
62
+ transform var(--dur-fast) var(--ease);
56
63
  }
57
64
  .ui-footer__social:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }
58
65
  .ui-footer__social svg { width: 17px; height: 17px; display: block; }
@@ -76,7 +83,7 @@
76
83
  font-size: 13.5px;
77
84
  color: var(--footer-link);
78
85
  text-decoration: none;
79
- transition: color 0.15s ease;
86
+ transition: color var(--dur-fast) var(--ease);
80
87
  }
81
88
  .ui-footer__links a:hover { color: var(--footer-link-hover); }
82
89
 
@@ -94,7 +101,7 @@
94
101
  }
95
102
  .ui-footer__legal { color: var(--footer-link); }
96
103
  .ui-footer__legal-links { display: flex; gap: 22px; flex-wrap: wrap; }
97
- .ui-footer__legal-links a { color: var(--footer-link); text-decoration: none; transition: color 0.15s ease; }
104
+ .ui-footer__legal-links a { color: var(--footer-link); text-decoration: none; transition: color var(--dur-fast) var(--ease); }
98
105
  .ui-footer__legal-links a:hover { color: var(--footer-link-hover); }
99
106
  .ui-footer__switcher { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; }
100
107
 
@@ -110,13 +117,15 @@
110
117
  .ui-footer--app .ui-footer__in { max-width: none; padding: 12px clamp(14px, 2.4vw, 22px); }
111
118
  .ui-footer--app .ui-footer__bar { padding-top: 0; border-top: 0; font-size: 12.5px; }
112
119
 
113
- /* Responsive — columns collapse to a stacked layout on narrow screens. */
120
+ /* Responsive — columns collapse to a stacked layout on narrow screens. Two of
121
+ the kit's three steps, and the second one is the phone step rather than a
122
+ fourth value of the footer's own: docs/specification.md#breakpoints. */
114
123
  @media (max-width: 720px) {
115
124
  .ui-footer__top { flex-direction: column; gap: 30px; }
116
125
  .ui-footer__nav { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 30px; }
117
126
  .ui-footer__brand { max-width: none; }
118
127
  }
119
- @media (max-width: 460px) {
128
+ @media (max-width: 560px) {
120
129
  .ui-footer__nav { grid-template-columns: 1fr; }
121
130
  .ui-footer__bar { flex-direction: column; align-items: flex-start; }
122
131
  .ui-footer__switcher { margin-left: 0; }
@@ -15,6 +15,7 @@
15
15
  .ui-field__req { color: var(--pink); margin-left: 4px; }
16
16
  .ui-field__hint { color: var(--muted); font-size: var(--text-sm); margin-top: 7px; line-height: 1.5; }
17
17
  .ui-field__error { color: var(--pink); font-size: var(--text-sm); margin-top: 7px; display: flex; align-items: center; gap: 6px; }
18
+ .ui-field__error svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
18
19
 
19
20
  .ui-input,
20
21
  .ui-textarea,
@@ -46,8 +47,15 @@
46
47
  .ui-input::placeholder,
47
48
  .ui-textarea::placeholder { color: var(--muted); opacity: 0.9; }
48
49
 
50
+ /* The value a reader typed is still the value; it goes quiet, it does not fade
51
+ into the field under it the way an opacity would take both down together. #220 */
49
52
  .ui-input:disabled,
50
- .ui-textarea:disabled { opacity: 0.5; cursor: not-allowed; }
53
+ .ui-textarea:disabled {
54
+ background: var(--disabled-surface);
55
+ border-color: var(--disabled-border);
56
+ color: var(--disabled-ink);
57
+ cursor: not-allowed;
58
+ }
51
59
 
52
60
  .ui-input.is-invalid,
53
61
  .ui-textarea.is-invalid { border-color: var(--pink); }
@@ -64,7 +72,7 @@
64
72
  color: var(--muted);
65
73
  pointer-events: none;
66
74
  }
67
- .ui-input-group__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 1.8; }
75
+ .ui-input-group__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.2; }
68
76
 
69
77
  .ui-select {
70
78
  appearance: none;
@@ -98,6 +106,24 @@
98
106
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
99
107
  position: relative;
100
108
  }
109
+ /* The target, not the ink. The box above is drawn at 19x19 because that is what
110
+ sits beside a 15px label without shouting; WCAG 2.5.8 wants 24 CSS px of
111
+ pointer target, which is a different question. This overlay answers it and
112
+ the checkbox keeps its size. ::after is the tick and the dot, so the target
113
+ is ::before — it paints first, under both, and is transparent anyway.
114
+ Centred, so the overhang is 2.5px on every side: 11px of gap to the label
115
+ inside the same <label> (which toggles the same input, so an overlap there
116
+ would cost nothing) and 14px between stacked rows in the specimen. Measured
117
+ in stories/guidelines/accessibility-floor.test.js. #219 */
118
+ .ui-check input::before {
119
+ content: '';
120
+ position: absolute;
121
+ left: 50%;
122
+ top: 50%;
123
+ width: 24px;
124
+ height: 24px;
125
+ transform: translate(-50%, -50%);
126
+ }
101
127
  .ui-check input[type="radio"] { border-radius: 50%; }
102
128
  .ui-check input:hover { border-color: var(--accent); }
103
129
  .ui-check input:checked { background: var(--accent); border-color: var(--accent); }
@@ -114,12 +114,14 @@
114
114
  The width is what keeps the auto margins honest: an auto inline margin takes
115
115
  a grid item out of stretch, and Chrome then lays it out at its max-content
116
116
  width, which is how the payout ledger pushed the page 529px sideways at 375.
117
- The 860px is the floor for hand-written markup shell.js validates what it
118
- writes into --ui-app-main and falls back to this same number. */
117
+ --measure is the ONE source for this width now. shell.js used to carry its
118
+ own copy of the number and a test held the two in step; it now omits the
119
+ property rather than writing a default, so an absent or unparseable maxWidth
120
+ lands here, on the token, instead of on a second literal. See issue #198. */
119
121
  .ui-app__main {
120
122
  min-width: 0;
121
123
  width: 100%;
122
- max-width: var(--ui-app-main, 860px);
124
+ max-width: var(--ui-app-main, var(--measure));
123
125
  margin-inline: auto;
124
126
  padding: 40px clamp(24px, 4vw, 56px) 72px;
125
127
  }
@@ -134,7 +136,7 @@
134
136
  letter-spacing: var(--tracking-tight);
135
137
  margin-bottom: 8px;
136
138
  }
137
- .ui-app__sub { color: var(--dim); font-size: 15px; line-height: 1.6; max-width: 60ch; margin-bottom: 32px; }
139
+ .ui-app__sub { color: var(--dim); font-size: 15px; line-height: 1.6; max-width: var(--prose-body); margin-bottom: 32px; }
138
140
  .ui-app__body { display: flex; flex-direction: column; gap: var(--space-5); }
139
141
  /* A flex child's min-width is its content by default, so one wide block — a
140
142
  ledger, a code sample — pushes the whole page sideways instead of scrolling
@@ -219,7 +221,7 @@
219
221
  position: relative;
220
222
  z-index: 1;
221
223
  width: 100%;
222
- max-width: 420px;
224
+ max-width: var(--panel-md);
223
225
  background: var(--surface);
224
226
  border-radius: 22px;
225
227
  padding: 38px 36px 32px;
@@ -245,7 +247,7 @@
245
247
  background: var(--glow-purple);
246
248
  color: var(--accent);
247
249
  }
248
- .ui-scope__icon svg { width: 21px; height: 21px; stroke: currentColor; fill: none; stroke-width: 1.7; }
250
+ .ui-scope__icon svg { width: 21px; height: 21px; stroke: currentColor; fill: none; stroke-width: 1.8; }
249
251
  .ui-scope__t { color: var(--strong); font-size: 14px; font-weight: 500; }
250
252
  .ui-scope__d { color: var(--dim); font-size: 13px; margin-top: 2px; line-height: 1.5; }
251
253
 
@@ -275,7 +277,7 @@
275
277
  letter-spacing: -0.025em;
276
278
  font-weight: 700;
277
279
  color: var(--strong);
278
- max-width: 14ch;
280
+ max-width: var(--prose-display);
279
281
  margin: 0 auto 22px;
280
282
  }
281
283
  .ui-hero__title .grad {
@@ -287,7 +289,7 @@
287
289
  .ui-hero__sub {
288
290
  font-size: clamp(16px, 2.1vw, 20px);
289
291
  color: var(--dim);
290
- max-width: 52ch;
292
+ max-width: var(--prose-lede);
291
293
  margin: 0 auto 34px;
292
294
  line-height: 1.6;
293
295
  }
@@ -298,7 +300,9 @@
298
300
  display: grid;
299
301
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
300
302
  gap: 20px;
301
- max-width: 1080px;
303
+ /* Was 1080px — a third page-scale number with nothing arguing for it. The
304
+ feature row is a top-level band and lines up with the page it sits on. */
305
+ max-width: var(--container);
302
306
  margin: 0 auto;
303
307
  padding: 0 26px;
304
308
  }
@@ -314,6 +318,6 @@
314
318
  .ui-feature h3 { color: var(--strong); font-size: 17px; font-weight: 600; margin-bottom: 7px; }
315
319
  .ui-feature p { color: var(--dim); font-size: 14.5px; line-height: 1.6; }
316
320
 
317
- .ui-section-head { text-align: center; max-width: 620px; margin: 0 auto clamp(40px, 6vh, 64px); }
321
+ .ui-section-head { text-align: center; max-width: var(--panel-lg); margin: 0 auto clamp(40px, 6vh, 64px); }
318
322
  .ui-section-head h2 { color: var(--strong); font-size: clamp(28px, 4vw, 40px); font-weight: 700; letter-spacing: -0.02em; margin-bottom: 14px; }
319
323
  .ui-section-head p { color: var(--dim); font-size: 17px; line-height: 1.6; }
@@ -0,0 +1,129 @@
1
+ /* Loading and permission-denied — the screen-scale states. Markup comes from
2
+ * components/loading.js; this file owns layout and nothing else.
3
+ *
4
+ * The shimmer is .m-skeleton from styles/motion.css, so reduced motion is
5
+ * already handled by the global net and there is one shimmer in the kit.
6
+ *
7
+ * .ui-denied deliberately borrows every colour .ui-empty uses: to a reader the
8
+ * two are the same event — what you came for is not here — and giving denial its
9
+ * own red made it read as a fault the reader had committed. The lock says which
10
+ * of the two it is.
11
+ *
12
+ * why: docs/specification.md#pending-and-denied-states */
13
+
14
+ /* Visually hidden, still read aloud. The kit had no such utility, and the live
15
+ region needs one: its message is for assistive tech only — the sighted
16
+ reader is already looking at the skeleton. */
17
+ .ui-sr {
18
+ position: absolute;
19
+ width: 1px;
20
+ height: 1px;
21
+ margin: -1px;
22
+ padding: 0;
23
+ overflow: hidden;
24
+ clip: rect(0 0 0 0);
25
+ clip-path: inset(50%);
26
+ white-space: nowrap;
27
+ border: 0;
28
+ }
29
+
30
+ /* -- The live region ------------------------------------------------------- */
31
+ .ui-busy {
32
+ display: block;
33
+ }
34
+
35
+ /* -- Skeleton -------------------------------------------------------------- */
36
+ .ui-skel {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 12px;
40
+ }
41
+ .ui-skel__bar {
42
+ display: block;
43
+ height: 13px;
44
+ width: 100%;
45
+ border-radius: var(--radius-xs);
46
+ }
47
+ /* Last line of a paragraph run stops short, the way real prose does — a block
48
+ of four identical full-width bars reads as a table, not as text. Any explicit
49
+ width from skeleton({ lines: [...] }) is inline and still wins. */
50
+ .ui-skel > .ui-skel__bar:last-child:not(:only-child) {
51
+ width: 62%;
52
+ }
53
+
54
+ /* Table shape: the column rhythm the real table will have, so the placeholder
55
+ does not resize the page out from under the reader when the rows land. */
56
+ .ui-skel--table {
57
+ gap: 0;
58
+ }
59
+ .ui-skel--table .ui-skel__row {
60
+ display: grid;
61
+ grid-template-columns: 1.6fr repeat(calc(var(--skel-cols) - 1), 1fr);
62
+ gap: 18px;
63
+ align-items: center;
64
+ padding: 14px 0;
65
+ border-bottom: 1px solid var(--border);
66
+ }
67
+ .ui-skel--table .ui-skel__row:last-child {
68
+ border-bottom: 0;
69
+ }
70
+ .ui-skel--table .ui-skel__bar {
71
+ height: 11px;
72
+ }
73
+ .ui-skel--table .ui-skel__row--head .ui-skel__bar {
74
+ height: 9px;
75
+ width: 58%;
76
+ opacity: 0.7;
77
+ }
78
+
79
+ /* -- Permission denied ----------------------------------------------------- */
80
+ .ui-denied {
81
+ display: flex;
82
+ flex-direction: column;
83
+ align-items: center;
84
+ text-align: center;
85
+ gap: 8px;
86
+ padding: 48px 24px;
87
+ color: var(--muted);
88
+ }
89
+ .ui-denied__seal {
90
+ width: 44px;
91
+ height: 44px;
92
+ margin-bottom: 6px;
93
+ border-radius: 50%;
94
+ display: grid;
95
+ place-items: center;
96
+ color: var(--dim);
97
+ background: color-mix(in srgb, var(--text) 7%, transparent);
98
+ }
99
+ .ui-denied__seal svg {
100
+ width: 20px;
101
+ height: 20px;
102
+ stroke: currentColor;
103
+ fill: none;
104
+ stroke-width: 1.9;
105
+ }
106
+ .ui-denied__title {
107
+ color: var(--text);
108
+ font-size: var(--text-md);
109
+ font-weight: var(--weight-medium);
110
+ margin: 2px 0 0;
111
+ }
112
+ .ui-denied__sub {
113
+ font-size: var(--text-sm);
114
+ max-width: var(--prose-caption);
115
+ margin: 0;
116
+ line-height: 1.55;
117
+ }
118
+ /* The missing scope, verbatim. A reader who can name what they lack can ask
119
+ for it; "insufficient permissions" sends them to a ticket to find out. */
120
+ .ui-denied__need {
121
+ font-size: var(--text-sm);
122
+ margin-top: 4px;
123
+ }
124
+ .ui-denied__actions {
125
+ display: flex;
126
+ gap: 10px;
127
+ justify-content: center;
128
+ margin-top: 12px;
129
+ }