@xenosystem/elements-react 0.0.1 → 0.0.3

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 (49) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +41 -41
  3. package/package.json +58 -58
  4. package/src/chrome-separated.css +132 -132
  5. package/src/chrome-unified.css +72 -72
  6. package/src/containers/Badges.css +116 -116
  7. package/src/containers/avatar.css +70 -70
  8. package/src/containers/card.css +75 -75
  9. package/src/containers/chip.css +103 -103
  10. package/src/containers/list-row.css +132 -132
  11. package/src/containers/message-bubble.css +112 -112
  12. package/src/containers/panel.css +120 -120
  13. package/src/containers/table.css +102 -102
  14. package/src/containers/tile.css +17 -17
  15. package/src/content/callout.css +77 -77
  16. package/src/content/caret.css +30 -30
  17. package/src/content/code-block.css +159 -159
  18. package/src/content/collapsible.css +80 -80
  19. package/src/content/inline-code.css +14 -14
  20. package/src/content/model-picker.css +219 -219
  21. package/src/content/source-card.css +99 -99
  22. package/src/content/sources-disclosure.css +118 -118
  23. package/src/controls.css +453 -453
  24. package/src/fonts.css +34 -34
  25. package/src/forms/Checkbox.css +114 -114
  26. package/src/forms/RadioGroup.css +115 -115
  27. package/src/goo.css +97 -97
  28. package/src/icon-motion.css +1909 -1909
  29. package/src/layout/ResizablePanel.css +79 -79
  30. package/src/nav/sidebar.css +296 -296
  31. package/src/overlays/date-time-picker.css +238 -238
  32. package/src/overlays/menu.css +195 -195
  33. package/src/overlays/modal.css +167 -167
  34. package/src/overlays/picker-field.css +81 -81
  35. package/src/overlays/pill-filter.css +129 -129
  36. package/src/overlays/reveal.css +56 -56
  37. package/src/overlays/segmented-control.css +185 -185
  38. package/src/overlays/tabs.css +143 -143
  39. package/src/overlays/tooltip.css +126 -126
  40. package/src/scrollbar.css +184 -184
  41. package/src/size.css +179 -179
  42. package/src/status/progress-bar.css +88 -88
  43. package/src/status/spinner.css +33 -33
  44. package/src/status/statusindicator.css +65 -65
  45. package/src/status/steptimeline.css +137 -137
  46. package/src/status/thinkingcube.css +75 -75
  47. package/src/xeno-element.css +60 -60
  48. package/src/xeno-elements.css +71 -71
  49. package/src/xeno-theme.css +99 -99
package/src/fonts.css CHANGED
@@ -1,34 +1,34 @@
1
- /*
2
- * The XENO typeface — vendored, not merely declared.
3
- *
4
- * `font-family: 'Inter'` alone SILENTLY falls back when Inter is not installed, and on Windows that
5
- * means Segoe UI: a fine reading face and the wrong one for dense dark chrome. The app then looks
6
- * identical to having declared nothing, which is most of why an interface reads as cheap. It is a
7
- * live bug in xeno-hub today. Vendoring the woff2 means it works offline and needs no CSP exception.
8
- *
9
- * OPT-IN, deliberately. This is a library, not an app: forcing ~100 kB of font into every consumer's
10
- * bundle is not ours to decide, and a product with its own licensed face must be able to say no. Same
11
- * shape as the `motion` entry point — the platform SHIPS it so nobody re-derives it, and the consumer
12
- * chooses.
13
- *
14
- * import '@xenosystem/elements-react/fonts.css'
15
- *
16
- * A product that skips this gets the fallback stack below and should know that it has.
17
- */
18
- @import '@fontsource-variable/inter/index.css';
19
-
20
- .xeno {
21
- /*
22
- * `--xeno-font-*` and NOT `--xeno-text-*`. That prefix is already the colour ramp, and reusing it
23
- * would silently overwrite `--xeno-text` app-wide — a documented failure, not a hypothetical.
24
- */
25
- --xeno-font-sans: 'Inter Variable', 'Inter', -apple-system, 'Segoe UI', system-ui, sans-serif;
26
- --xeno-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
27
-
28
- font-family: var(--xeno-font-sans);
29
- /* cv11 disambiguates 1/l/I — the one that matters most in dense UI and in code-adjacent labels. */
30
- font-feature-settings: 'cv02' 1, 'cv03' 1, 'cv04' 1, 'cv11' 1, 'ss01' 1;
31
- font-optical-sizing: auto;
32
- /* Inter runs a touch wide at UI sizes. */
33
- letter-spacing: -0.006em;
34
- }
1
+ /*
2
+ * The XENO typeface — vendored, not merely declared.
3
+ *
4
+ * `font-family: 'Inter'` alone SILENTLY falls back when Inter is not installed, and on Windows that
5
+ * means Segoe UI: a fine reading face and the wrong one for dense dark chrome. The app then looks
6
+ * identical to having declared nothing, which is most of why an interface reads as cheap. It is a
7
+ * live bug in xeno-hub today. Vendoring the woff2 means it works offline and needs no CSP exception.
8
+ *
9
+ * OPT-IN, deliberately. This is a library, not an app: forcing ~100 kB of font into every consumer's
10
+ * bundle is not ours to decide, and a product with its own licensed face must be able to say no. Same
11
+ * shape as the `motion` entry point — the platform SHIPS it so nobody re-derives it, and the consumer
12
+ * chooses.
13
+ *
14
+ * import '@xenosystem/elements-react/fonts.css'
15
+ *
16
+ * A product that skips this gets the fallback stack below and should know that it has.
17
+ */
18
+ @import '@fontsource-variable/inter/index.css';
19
+
20
+ .xeno {
21
+ /*
22
+ * `--xeno-font-*` and NOT `--xeno-text-*`. That prefix is already the colour ramp, and reusing it
23
+ * would silently overwrite `--xeno-text` app-wide — a documented failure, not a hypothetical.
24
+ */
25
+ --xeno-font-sans: 'Inter Variable', 'Inter', -apple-system, 'Segoe UI', system-ui, sans-serif;
26
+ --xeno-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
27
+
28
+ font-family: var(--xeno-font-sans);
29
+ /* cv11 disambiguates 1/l/I — the one that matters most in dense UI and in code-adjacent labels. */
30
+ font-feature-settings: 'cv02' 1, 'cv03' 1, 'cv04' 1, 'cv11' 1, 'ss01' 1;
31
+ font-optical-sizing: auto;
32
+ /* Inter runs a touch wide at UI sizes. */
33
+ letter-spacing: -0.006em;
34
+ }
@@ -1,114 +1,114 @@
1
- /*
2
- * `<Checkbox>` — rounded-SQUARE box + optional label. CSS-first: paint is keyed off `data-selection`
3
- * (off|on|mixed) and `data-availability`. Monochrome shell — only theme tokens, no literal colours.
4
- */
5
- .xeno-checkbox {
6
- display: inline-flex;
7
- align-items: center;
8
- gap: 8px;
9
- margin: 0;
10
- padding: 0;
11
- background: transparent;
12
- border: none;
13
- color: var(--xeno-text);
14
- font-family: inherit;
15
- font-size: 14px;
16
- line-height: 1;
17
- text-align: left;
18
- cursor: pointer;
19
- user-select: none;
20
- }
21
-
22
- .xeno-checkbox-box {
23
- display: inline-flex;
24
- align-items: center;
25
- justify-content: center;
26
- flex: none;
27
- width: 17px;
28
- height: 17px;
29
- /* The stamped glyph inherits this via currentColor; only visible once the box is filled. */
30
- color: var(--xeno-on-accent);
31
- background: var(--xeno-canvas);
32
- border: 1px solid var(--xeno-border);
33
- border-radius: var(--xeno-radius-sm);
34
- transition:
35
- background var(--xeno-dur) var(--xeno-ease),
36
- border-color var(--xeno-dur) var(--xeno-ease);
37
- }
38
-
39
- .xeno-checkbox:hover:not(:disabled) .xeno-checkbox-box {
40
- border-color: var(--xeno-muted);
41
- }
42
-
43
- .xeno-checkbox[data-selection='on'] .xeno-checkbox-box,
44
- .xeno-checkbox[data-selection='mixed'] .xeno-checkbox-box {
45
- background: var(--xeno-text);
46
- border-color: var(--xeno-text);
47
- }
48
-
49
- /* ── Ticking ──────────────────────────────────────────────────────────────────────────
50
- * The tick DRAWS itself, heel to tip. Nothing new is authored here: `xeno.check` already carries that
51
- * animation in `icon-motion.css`, where it plays on hover — the glyph's own rule sets `--part-anim`,
52
- * `--part-dur` and the `stroke-dasharray` it needs. All this does is play the SAME animation on the
53
- * event that actually matters for a checkbox, which is the tick appearing.
54
- *
55
- * Reading it through `var(--part-anim)` rather than naming the keyframe keeps the two loosely coupled:
56
- * redraw the check glyph's animation and the checkbox follows it, with no edit here.
57
- *
58
- * The 900ms the gallery uses is a showcase pace; a control that answers a click wants ~a third of that.
59
- *
60
- * Everything is gated on `data-motion="ready"`, which the component sets only after `checked` has
61
- * genuinely changed — otherwise a form rendered with ticked boxes draws all of them on load. */
62
- .xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-part[data-part='0'] {
63
- --part-dur: 340ms;
64
- animation: var(--part-anim) var(--part-dur) var(--xeno-icon-ease);
65
- }
66
- /* The box answers with it: a quick compress and overshoot, so the fill lands with the tick instead of
67
- snapping into place a beat ahead of it. */
68
- .xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-checkbox-box,
69
- .xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-checkbox-box {
70
- animation: xeno-checkbox-stamp 280ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
71
- }
72
- @keyframes xeno-checkbox-stamp {
73
- 0% { scale: 0.86; }
74
- 60% { scale: 1.06; }
75
- 100% { scale: 1; }
76
- }
77
- /* Indeterminate has no stroke to draw — the bar just arrives, widening from the middle. */
78
- .xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-part[data-part='0'] {
79
- animation: xeno-checkbox-dash 240ms var(--xeno-ease, cubic-bezier(0.22, 1, 0.36, 1));
80
- }
81
- @keyframes xeno-checkbox-dash {
82
- 0% { scale: 0 1; opacity: 0; }
83
- 100% { scale: 1 1; opacity: 1; }
84
- }
85
-
86
- .xeno-checkbox-label {
87
- color: var(--xeno-text);
88
- }
89
-
90
- .xeno-checkbox:focus-visible {
91
- outline: none;
92
- }
93
- .xeno-checkbox:focus-visible .xeno-checkbox-box {
94
- box-shadow:
95
- 0 0 0 2px var(--xeno-canvas),
96
- 0 0 0 4px var(--xeno-border);
97
- }
98
-
99
- .xeno-checkbox:disabled,
100
- .xeno-checkbox[data-availability='disabled'] {
101
- opacity: 0.4;
102
- cursor: not-allowed;
103
- }
104
-
105
- @media (prefers-reduced-motion: reduce) {
106
- .xeno-checkbox-box {
107
- transition: none;
108
- }
109
- /* The tick is still there — it just does not draw itself in. */
110
- .xeno-checkbox-box,
111
- .xeno-checkbox .xeno-part {
112
- animation: none !important;
113
- }
114
- }
1
+ /*
2
+ * `<Checkbox>` — rounded-SQUARE box + optional label. CSS-first: paint is keyed off `data-selection`
3
+ * (off|on|mixed) and `data-availability`. Monochrome shell — only theme tokens, no literal colours.
4
+ */
5
+ .xeno-checkbox {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: 8px;
9
+ margin: 0;
10
+ padding: 0;
11
+ background: transparent;
12
+ border: none;
13
+ color: var(--xeno-text);
14
+ font-family: inherit;
15
+ font-size: 14px;
16
+ line-height: 1;
17
+ text-align: left;
18
+ cursor: pointer;
19
+ user-select: none;
20
+ }
21
+
22
+ .xeno-checkbox-box {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ flex: none;
27
+ width: 17px;
28
+ height: 17px;
29
+ /* The stamped glyph inherits this via currentColor; only visible once the box is filled. */
30
+ color: var(--xeno-on-accent);
31
+ background: var(--xeno-canvas);
32
+ border: 1px solid var(--xeno-border);
33
+ border-radius: var(--xeno-radius-sm);
34
+ transition:
35
+ background var(--xeno-dur) var(--xeno-ease),
36
+ border-color var(--xeno-dur) var(--xeno-ease);
37
+ }
38
+
39
+ .xeno-checkbox:hover:not(:disabled) .xeno-checkbox-box {
40
+ border-color: var(--xeno-muted);
41
+ }
42
+
43
+ .xeno-checkbox[data-selection='on'] .xeno-checkbox-box,
44
+ .xeno-checkbox[data-selection='mixed'] .xeno-checkbox-box {
45
+ background: var(--xeno-text);
46
+ border-color: var(--xeno-text);
47
+ }
48
+
49
+ /* ── Ticking ──────────────────────────────────────────────────────────────────────────
50
+ * The tick DRAWS itself, heel to tip. Nothing new is authored here: `xeno.check` already carries that
51
+ * animation in `icon-motion.css`, where it plays on hover — the glyph's own rule sets `--part-anim`,
52
+ * `--part-dur` and the `stroke-dasharray` it needs. All this does is play the SAME animation on the
53
+ * event that actually matters for a checkbox, which is the tick appearing.
54
+ *
55
+ * Reading it through `var(--part-anim)` rather than naming the keyframe keeps the two loosely coupled:
56
+ * redraw the check glyph's animation and the checkbox follows it, with no edit here.
57
+ *
58
+ * The 900ms the gallery uses is a showcase pace; a control that answers a click wants ~a third of that.
59
+ *
60
+ * Everything is gated on `data-motion="ready"`, which the component sets only after `checked` has
61
+ * genuinely changed — otherwise a form rendered with ticked boxes draws all of them on load. */
62
+ .xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-part[data-part='0'] {
63
+ --part-dur: 340ms;
64
+ animation: var(--part-anim) var(--part-dur) var(--xeno-icon-ease);
65
+ }
66
+ /* The box answers with it: a quick compress and overshoot, so the fill lands with the tick instead of
67
+ snapping into place a beat ahead of it. */
68
+ .xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-checkbox-box,
69
+ .xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-checkbox-box {
70
+ animation: xeno-checkbox-stamp 280ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
71
+ }
72
+ @keyframes xeno-checkbox-stamp {
73
+ 0% { scale: 0.86; }
74
+ 60% { scale: 1.06; }
75
+ 100% { scale: 1; }
76
+ }
77
+ /* Indeterminate has no stroke to draw — the bar just arrives, widening from the middle. */
78
+ .xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-part[data-part='0'] {
79
+ animation: xeno-checkbox-dash 240ms var(--xeno-ease, cubic-bezier(0.22, 1, 0.36, 1));
80
+ }
81
+ @keyframes xeno-checkbox-dash {
82
+ 0% { scale: 0 1; opacity: 0; }
83
+ 100% { scale: 1 1; opacity: 1; }
84
+ }
85
+
86
+ .xeno-checkbox-label {
87
+ color: var(--xeno-text);
88
+ }
89
+
90
+ .xeno-checkbox:focus-visible {
91
+ outline: none;
92
+ }
93
+ .xeno-checkbox:focus-visible .xeno-checkbox-box {
94
+ box-shadow:
95
+ 0 0 0 2px var(--xeno-canvas),
96
+ 0 0 0 4px var(--xeno-border);
97
+ }
98
+
99
+ .xeno-checkbox:disabled,
100
+ .xeno-checkbox[data-availability='disabled'] {
101
+ opacity: 0.4;
102
+ cursor: not-allowed;
103
+ }
104
+
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .xeno-checkbox-box {
107
+ transition: none;
108
+ }
109
+ /* The tick is still there — it just does not draw itself in. */
110
+ .xeno-checkbox-box,
111
+ .xeno-checkbox .xeno-part {
112
+ animation: none !important;
113
+ }
114
+ }
@@ -1,115 +1,115 @@
1
- /*
2
- * `<RadioGroup>` / `<RadioRow>` — SQUARE markers (radius-xs) with a smaller inner square, never circles.
3
- * CSS-first: paint is keyed off `data-selection` (off|on) and `data-availability`; layout off
4
- * `data-orientation`. Monochrome shell — only theme tokens, no literal colours.
5
- */
6
- .xeno-radiogroup {
7
- display: flex;
8
- flex-direction: column;
9
- gap: 2px;
10
- }
11
- .xeno-radiogroup[data-orientation='horizontal'] {
12
- flex-direction: row;
13
- gap: 12px;
14
- }
15
-
16
- .xeno-radio {
17
- display: inline-flex;
18
- align-items: center;
19
- gap: 9px;
20
- margin: 0;
21
- padding: 6px 8px;
22
- background: transparent;
23
- border: 1px solid transparent;
24
- border-radius: var(--xeno-radius-md);
25
- color: var(--xeno-text);
26
- font-family: inherit;
27
- font-size: 14px;
28
- line-height: 1;
29
- text-align: left;
30
- cursor: pointer;
31
- user-select: none;
32
- transition: background var(--xeno-dur) var(--xeno-ease);
33
- }
34
- .xeno-radio:hover:not(:disabled) {
35
- background: var(--xeno-hover);
36
- }
37
-
38
- .xeno-radio-marker {
39
- display: inline-flex;
40
- align-items: center;
41
- justify-content: center;
42
- flex: none;
43
- width: 16px;
44
- height: 16px;
45
- background: var(--xeno-canvas);
46
- border: 1px solid var(--xeno-border);
47
- border-radius: var(--xeno-radius-xs);
48
- transition: border-color var(--xeno-dur) var(--xeno-ease);
49
- }
50
- .xeno-radio:hover:not(:disabled) .xeno-radio-marker {
51
- border-color: var(--xeno-muted);
52
- }
53
- .xeno-radio[data-selection='on'] .xeno-radio-marker {
54
- border-color: var(--xeno-text);
55
- }
56
-
57
- /* The mark is a SQUARE, and at 8px it has to be drawn like one. It carried `--xeno-radius-xs` (3px),
58
- which is 37% of its own width — enough rounding that at this size it reads as a dot rather than as a
59
- square. `--xeno-radius-hair` puts it at 19%, in the same family as the switch knob's 21%. */
60
- .xeno-radio-dot {
61
- width: 8px;
62
- height: 8px;
63
- background: var(--xeno-text);
64
- border-radius: var(--xeno-radius-hair, 1.5px);
65
- scale: 0;
66
- opacity: 0;
67
- transition:
68
- scale var(--xeno-dur) var(--xeno-ease),
69
- opacity var(--xeno-dur) var(--xeno-ease);
70
- }
71
- .xeno-radio[data-selection='on'] .xeno-radio-dot {
72
- scale: 1;
73
- opacity: 1;
74
- }
75
- /* Choosing STAMPS: the square overshoots and settles, so the choice lands rather than fades in. Only on
76
- the way in — letting go is quiet, and the transition above still handles it.
77
- Armed at the group, which is safe HERE and would not be on a multi-select: exactly one radio can be
78
- on, so the only one whose rule starts matching when the group arms is the one that just changed. */
79
- .xeno-radiogroup[data-motion='ready'] .xeno-radio[data-selection='on'] .xeno-radio-dot {
80
- animation: xeno-radio-stamp 260ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
81
- }
82
- @keyframes xeno-radio-stamp {
83
- 0% { scale: 0; opacity: 0; }
84
- 45% { scale: 1.22; opacity: 1; }
85
- 100% { scale: 1; opacity: 1; }
86
- }
87
-
88
- .xeno-radio-label {
89
- color: var(--xeno-text);
90
- }
91
-
92
- .xeno-radio:focus-visible {
93
- outline: none;
94
- box-shadow:
95
- 0 0 0 2px var(--xeno-canvas),
96
- 0 0 0 4px var(--xeno-border);
97
- }
98
-
99
- .xeno-radio:disabled,
100
- .xeno-radio[data-availability='disabled'] {
101
- opacity: 0.4;
102
- cursor: not-allowed;
103
- }
104
-
105
- @media (prefers-reduced-motion: reduce) {
106
- .xeno-radio,
107
- .xeno-radio-marker,
108
- .xeno-radio-dot {
109
- transition: none;
110
- }
111
- /* The mark still appears — it just does not stamp. */
112
- .xeno-radio-dot {
113
- animation: none !important;
114
- }
115
- }
1
+ /*
2
+ * `<RadioGroup>` / `<RadioRow>` — SQUARE markers (radius-xs) with a smaller inner square, never circles.
3
+ * CSS-first: paint is keyed off `data-selection` (off|on) and `data-availability`; layout off
4
+ * `data-orientation`. Monochrome shell — only theme tokens, no literal colours.
5
+ */
6
+ .xeno-radiogroup {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 2px;
10
+ }
11
+ .xeno-radiogroup[data-orientation='horizontal'] {
12
+ flex-direction: row;
13
+ gap: 12px;
14
+ }
15
+
16
+ .xeno-radio {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ gap: 9px;
20
+ margin: 0;
21
+ padding: 6px 8px;
22
+ background: transparent;
23
+ border: 1px solid transparent;
24
+ border-radius: var(--xeno-radius-md);
25
+ color: var(--xeno-text);
26
+ font-family: inherit;
27
+ font-size: 14px;
28
+ line-height: 1;
29
+ text-align: left;
30
+ cursor: pointer;
31
+ user-select: none;
32
+ transition: background var(--xeno-dur) var(--xeno-ease);
33
+ }
34
+ .xeno-radio:hover:not(:disabled) {
35
+ background: var(--xeno-hover);
36
+ }
37
+
38
+ .xeno-radio-marker {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ flex: none;
43
+ width: 16px;
44
+ height: 16px;
45
+ background: var(--xeno-canvas);
46
+ border: 1px solid var(--xeno-border);
47
+ border-radius: var(--xeno-radius-xs);
48
+ transition: border-color var(--xeno-dur) var(--xeno-ease);
49
+ }
50
+ .xeno-radio:hover:not(:disabled) .xeno-radio-marker {
51
+ border-color: var(--xeno-muted);
52
+ }
53
+ .xeno-radio[data-selection='on'] .xeno-radio-marker {
54
+ border-color: var(--xeno-text);
55
+ }
56
+
57
+ /* The mark is a SQUARE, and at 8px it has to be drawn like one. It carried `--xeno-radius-xs` (3px),
58
+ which is 37% of its own width — enough rounding that at this size it reads as a dot rather than as a
59
+ square. `--xeno-radius-hair` puts it at 19%, in the same family as the switch knob's 21%. */
60
+ .xeno-radio-dot {
61
+ width: 8px;
62
+ height: 8px;
63
+ background: var(--xeno-text);
64
+ border-radius: var(--xeno-radius-hair, 1.5px);
65
+ scale: 0;
66
+ opacity: 0;
67
+ transition:
68
+ scale var(--xeno-dur) var(--xeno-ease),
69
+ opacity var(--xeno-dur) var(--xeno-ease);
70
+ }
71
+ .xeno-radio[data-selection='on'] .xeno-radio-dot {
72
+ scale: 1;
73
+ opacity: 1;
74
+ }
75
+ /* Choosing STAMPS: the square overshoots and settles, so the choice lands rather than fades in. Only on
76
+ the way in — letting go is quiet, and the transition above still handles it.
77
+ Armed at the group, which is safe HERE and would not be on a multi-select: exactly one radio can be
78
+ on, so the only one whose rule starts matching when the group arms is the one that just changed. */
79
+ .xeno-radiogroup[data-motion='ready'] .xeno-radio[data-selection='on'] .xeno-radio-dot {
80
+ animation: xeno-radio-stamp 260ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
81
+ }
82
+ @keyframes xeno-radio-stamp {
83
+ 0% { scale: 0; opacity: 0; }
84
+ 45% { scale: 1.22; opacity: 1; }
85
+ 100% { scale: 1; opacity: 1; }
86
+ }
87
+
88
+ .xeno-radio-label {
89
+ color: var(--xeno-text);
90
+ }
91
+
92
+ .xeno-radio:focus-visible {
93
+ outline: none;
94
+ box-shadow:
95
+ 0 0 0 2px var(--xeno-canvas),
96
+ 0 0 0 4px var(--xeno-border);
97
+ }
98
+
99
+ .xeno-radio:disabled,
100
+ .xeno-radio[data-availability='disabled'] {
101
+ opacity: 0.4;
102
+ cursor: not-allowed;
103
+ }
104
+
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .xeno-radio,
107
+ .xeno-radio-marker,
108
+ .xeno-radio-dot {
109
+ transition: none;
110
+ }
111
+ /* The mark still appears — it just does not stamp. */
112
+ .xeno-radio-dot {
113
+ animation: none !important;
114
+ }
115
+ }