@xenosystem/elements-react 0.0.1
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/LICENSE +18 -0
- package/README.md +41 -0
- package/dist/XenoElement.d.ts +39 -0
- package/dist/XenoElement.js +25 -0
- package/dist/containers/Avatar.d.ts +42 -0
- package/dist/containers/Avatar.js +29 -0
- package/dist/containers/Badges.d.ts +61 -0
- package/dist/containers/Badges.js +43 -0
- package/dist/containers/Card.d.ts +24 -0
- package/dist/containers/Card.js +5 -0
- package/dist/containers/Chip.d.ts +34 -0
- package/dist/containers/Chip.js +17 -0
- package/dist/containers/DataTable.d.ts +28 -0
- package/dist/containers/DataTable.js +13 -0
- package/dist/containers/ListRow.d.ts +37 -0
- package/dist/containers/ListRow.js +40 -0
- package/dist/containers/MessageBubble.d.ts +33 -0
- package/dist/containers/MessageBubble.js +9 -0
- package/dist/containers/Panel.d.ts +24 -0
- package/dist/containers/Panel.js +6 -0
- package/dist/containers/Table.d.ts +39 -0
- package/dist/containers/Table.js +20 -0
- package/dist/containers/Tile.d.ts +19 -0
- package/dist/containers/Tile.js +7 -0
- package/dist/content/Callout.d.ts +32 -0
- package/dist/content/Callout.js +10 -0
- package/dist/content/Caret.d.ts +9 -0
- package/dist/content/Caret.js +5 -0
- package/dist/content/CodeBlock.d.ts +36 -0
- package/dist/content/CodeBlock.js +50 -0
- package/dist/content/Collapsible.d.ts +24 -0
- package/dist/content/Collapsible.js +19 -0
- package/dist/content/InlineCode.d.ts +12 -0
- package/dist/content/InlineCode.js +5 -0
- package/dist/content/ModelPicker.d.ts +45 -0
- package/dist/content/ModelPicker.js +124 -0
- package/dist/content/SourceCard.d.ts +23 -0
- package/dist/content/SourceCard.js +18 -0
- package/dist/content/SourcesDisclosure.d.ts +26 -0
- package/dist/content/SourcesDisclosure.js +20 -0
- package/dist/controls/Button.d.ts +39 -0
- package/dist/controls/Button.js +16 -0
- package/dist/controls/ConfirmButton.d.ts +22 -0
- package/dist/controls/ConfirmButton.js +22 -0
- package/dist/controls/IconButton.d.ts +45 -0
- package/dist/controls/IconButton.js +15 -0
- package/dist/controls/Switch.d.ts +11 -0
- package/dist/controls/Switch.js +23 -0
- package/dist/controls/TextInput.d.ts +13 -0
- package/dist/controls/TextInput.js +6 -0
- package/dist/controls/Textarea.d.ts +8 -0
- package/dist/controls/Textarea.js +5 -0
- package/dist/controls/ToggleButton.d.ts +23 -0
- package/dist/controls/ToggleButton.js +23 -0
- package/dist/controls/util.d.ts +48 -0
- package/dist/controls/util.js +23 -0
- package/dist/forms/Checkbox.d.ts +18 -0
- package/dist/forms/Checkbox.js +28 -0
- package/dist/forms/RadioGroup.d.ts +51 -0
- package/dist/forms/RadioGroup.js +83 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +81 -0
- package/dist/layout/ResizablePanel.d.ts +40 -0
- package/dist/layout/ResizablePanel.js +108 -0
- package/dist/nav/Sidebar.d.ts +60 -0
- package/dist/nav/Sidebar.js +84 -0
- package/dist/overlays/DateTimePicker.d.ts +71 -0
- package/dist/overlays/DateTimePicker.js +191 -0
- package/dist/overlays/Menu.d.ts +28 -0
- package/dist/overlays/Menu.js +32 -0
- package/dist/overlays/MenuItem.d.ts +71 -0
- package/dist/overlays/MenuItem.js +15 -0
- package/dist/overlays/Modal.d.ts +36 -0
- package/dist/overlays/Modal.js +27 -0
- package/dist/overlays/PickerField.d.ts +32 -0
- package/dist/overlays/PickerField.js +40 -0
- package/dist/overlays/PillFilter.d.ts +25 -0
- package/dist/overlays/PillFilter.js +41 -0
- package/dist/overlays/Popover.d.ts +32 -0
- package/dist/overlays/Popover.js +38 -0
- package/dist/overlays/Reveal.d.ts +20 -0
- package/dist/overlays/Reveal.js +45 -0
- package/dist/overlays/SegmentedControl.d.ts +23 -0
- package/dist/overlays/SegmentedControl.js +43 -0
- package/dist/overlays/Tabs.d.ts +32 -0
- package/dist/overlays/Tabs.js +142 -0
- package/dist/overlays/Tooltip.d.ts +23 -0
- package/dist/overlays/Tooltip.js +61 -0
- package/dist/status/ProgressBar.d.ts +20 -0
- package/dist/status/ProgressBar.js +11 -0
- package/dist/status/Spinner.d.ts +16 -0
- package/dist/status/Spinner.js +12 -0
- package/dist/status/StatusIndicator.d.ts +19 -0
- package/dist/status/StatusIndicator.js +11 -0
- package/dist/status/StepTimeline.d.ts +26 -0
- package/dist/status/StepTimeline.js +16 -0
- package/dist/status/ThinkingCube.d.ts +24 -0
- package/dist/status/ThinkingCube.js +11 -0
- package/dist/useDialog.d.ts +45 -0
- package/dist/useDialog.js +122 -0
- package/dist/useGooPill.d.ts +57 -0
- package/dist/useGooPill.js +76 -0
- package/dist/useMenu.d.ts +50 -0
- package/dist/useMenu.js +119 -0
- package/dist/useTabs.d.ts +88 -0
- package/dist/useTabs.js +93 -0
- package/package.json +58 -0
- package/src/chrome-separated.css +132 -0
- package/src/chrome-unified.css +72 -0
- package/src/containers/Badges.css +116 -0
- package/src/containers/avatar.css +70 -0
- package/src/containers/card.css +75 -0
- package/src/containers/chip.css +103 -0
- package/src/containers/list-row.css +132 -0
- package/src/containers/message-bubble.css +112 -0
- package/src/containers/panel.css +120 -0
- package/src/containers/table.css +102 -0
- package/src/containers/tile.css +17 -0
- package/src/content/callout.css +77 -0
- package/src/content/caret.css +30 -0
- package/src/content/code-block.css +159 -0
- package/src/content/collapsible.css +80 -0
- package/src/content/inline-code.css +14 -0
- package/src/content/model-picker.css +219 -0
- package/src/content/source-card.css +99 -0
- package/src/content/sources-disclosure.css +118 -0
- package/src/controls.css +453 -0
- package/src/fonts.css +34 -0
- package/src/forms/Checkbox.css +114 -0
- package/src/forms/RadioGroup.css +115 -0
- package/src/goo.css +97 -0
- package/src/icon-motion.css +1909 -0
- package/src/layout/ResizablePanel.css +79 -0
- package/src/nav/sidebar.css +296 -0
- package/src/overlays/date-time-picker.css +238 -0
- package/src/overlays/menu.css +195 -0
- package/src/overlays/modal.css +167 -0
- package/src/overlays/picker-field.css +81 -0
- package/src/overlays/pill-filter.css +129 -0
- package/src/overlays/reveal.css +56 -0
- package/src/overlays/segmented-control.css +185 -0
- package/src/overlays/tabs.css +143 -0
- package/src/overlays/tooltip.css +126 -0
- package/src/scrollbar.css +184 -0
- package/src/size.css +179 -0
- package/src/status/progress-bar.css +88 -0
- package/src/status/spinner.css +33 -0
- package/src/status/statusindicator.css +65 -0
- package/src/status/steptimeline.css +137 -0
- package/src/status/thinkingcube.css +75 -0
- package/src/xeno-element.css +60 -0
- package/src/xeno-elements.css +71 -0
- package/src/xeno-theme.css +99 -0
|
@@ -0,0 +1,1909 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
XENO icon motion — bespoke per-glyph hover animations
|
|
3
|
+
-----------------------------------------------------------------------------
|
|
4
|
+
The renderer stamps a `data-glyph` attribute on every icon it draws
|
|
5
|
+
(`<svg class="xeno-element" data-glyph="search"> … </svg>`), gives every shape
|
|
6
|
+
inside it `class="xeno-part"`, and numbers each shape with a `data-part` index
|
|
7
|
+
(its position in the geometry).
|
|
8
|
+
|
|
9
|
+
Each glyph animates the moving parts that carry its MEANING: some drive their
|
|
10
|
+
individual PARTS (a bell's clapper, an eye's pupil, a list's rows), others move
|
|
11
|
+
as a WHOLE icon (a gear spins, an arrow travels). A glyph is one or the other —
|
|
12
|
+
never both — so nothing double-animates.
|
|
13
|
+
|
|
14
|
+
A single pair of triggers plays the right keyframe when the icon itself is
|
|
15
|
+
hovered, or when any interactive host that wraps it is hovered (buttons, chips,
|
|
16
|
+
toggles, list rows, menu items, tabs, …). Animations use transform and opacity
|
|
17
|
+
ONLY (compositor-friendly, no layout thrash); every keyframe starts and ends at
|
|
18
|
+
the identity transform so icons always rest cleanly. Motion is switched off
|
|
19
|
+
entirely under prefers-reduced-motion.
|
|
20
|
+
============================================================================= */
|
|
21
|
+
|
|
22
|
+
/* ---- Base: defaults every glyph + every part inherits ---------------------- */
|
|
23
|
+
.xeno-element[data-glyph] {
|
|
24
|
+
transform-box: fill-box;
|
|
25
|
+
transform-origin: center;
|
|
26
|
+
--xeno-icon-anim: none;
|
|
27
|
+
--xeno-icon-dur: 520ms;
|
|
28
|
+
--xeno-icon-ease: var(--xeno-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
29
|
+
}
|
|
30
|
+
.xeno-element[data-glyph] .xeno-part {
|
|
31
|
+
transform-box: fill-box;
|
|
32
|
+
transform-origin: center;
|
|
33
|
+
--part-anim: none;
|
|
34
|
+
--part-dur: 520ms;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ---- Per-glyph mapping + keyframes ----------------------------------------- */
|
|
38
|
+
|
|
39
|
+
/* menu (parts) — three bars ripple left-to-right, one after another */
|
|
40
|
+
.xeno-element[data-glyph='menu'] .xeno-part { transform-origin: left center; }
|
|
41
|
+
.xeno-element[data-glyph='menu'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-menu-0; }
|
|
42
|
+
.xeno-element[data-glyph='menu'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-menu-1; }
|
|
43
|
+
.xeno-element[data-glyph='menu'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-menu-2; }
|
|
44
|
+
@keyframes xeno-pt-menu-0 { 0% { transform: translateX(0); } 22% { transform: translateX(3px); } 48% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
45
|
+
@keyframes xeno-pt-menu-1 { 0% { transform: translateX(0); } 16% { transform: translateX(0); } 38% { transform: translateX(3px); } 62% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
46
|
+
@keyframes xeno-pt-menu-2 { 0% { transform: translateX(0); } 30% { transform: translateX(0); } 54% { transform: translateX(3px); } 78% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
47
|
+
|
|
48
|
+
/* search (whole) — a scanning sweep with a lens-zoom */
|
|
49
|
+
.xeno-element[data-glyph='search'] { --xeno-icon-anim: xeno-ic-search; --xeno-icon-dur: 600ms; }
|
|
50
|
+
@keyframes xeno-ic-search {
|
|
51
|
+
0% { transform: translate(0, 0) scale(1); }
|
|
52
|
+
20% { transform: translate(2px, -2px) scale(1.08); }
|
|
53
|
+
40% { transform: translate(3px, 1px) scale(1.12); }
|
|
54
|
+
60% { transform: translate(-1px, 2px) scale(1.08); }
|
|
55
|
+
80% { transform: translate(-2px, -1px) scale(1.04); }
|
|
56
|
+
100% { transform: translate(0, 0) scale(1); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* plus (parts) — the plus draws itself: the vertical bar grows in, then the horizontal, each with a small overshoot */
|
|
60
|
+
.xeno-element[data-glyph='plus'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-plus-v; --part-dur: 540ms; transform-origin: center; }
|
|
61
|
+
.xeno-element[data-glyph='plus'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-plus-h; --part-dur: 540ms; transform-origin: center; }
|
|
62
|
+
@keyframes xeno-pt-plus-v {
|
|
63
|
+
0% { transform: scaleY(0); }
|
|
64
|
+
45% { transform: scaleY(1.15); }
|
|
65
|
+
70% { transform: scaleY(0.95); }
|
|
66
|
+
100% { transform: scaleY(1); }
|
|
67
|
+
}
|
|
68
|
+
@keyframes xeno-pt-plus-h {
|
|
69
|
+
0%, 20% { transform: scaleX(0); }
|
|
70
|
+
60% { transform: scaleX(1.15); }
|
|
71
|
+
80% { transform: scaleX(0.95); }
|
|
72
|
+
100% { transform: scaleX(1); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* minus (whole) — the bar collapses hard to a stub, holds, then springs back (decisive remove) */
|
|
76
|
+
.xeno-element[data-glyph='minus'] { --xeno-icon-anim: xeno-ic-minus; --xeno-icon-dur: 460ms; }
|
|
77
|
+
@keyframes xeno-ic-minus {
|
|
78
|
+
0% { transform: scaleX(1); }
|
|
79
|
+
34% { transform: scaleX(0.5); }
|
|
80
|
+
54% { transform: scaleX(0.5); }
|
|
81
|
+
80% { transform: scaleX(1.14); }
|
|
82
|
+
100% { transform: scaleX(1); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* x (parts) — the two strokes draw themselves like an X being struck, then a small pop */
|
|
86
|
+
.xeno-element[data-glyph='x'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-x-draw; --part-dur: 520ms; stroke-dasharray: 32; }
|
|
87
|
+
@keyframes xeno-pt-x-draw {
|
|
88
|
+
0% { stroke-dashoffset: 32; transform: scale(1); }
|
|
89
|
+
58% { stroke-dashoffset: 0; transform: scale(1); }
|
|
90
|
+
74% { transform: scale(1.14); }
|
|
91
|
+
100% { stroke-dashoffset: 0; transform: scale(1); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* check (parts) — the tick draws itself heel-to-tip at an EVEN pace, then gives a confirming pop.
|
|
95
|
+
A draw-in needs a steady ease: the library's default ease-out front-loads it and the stroke
|
|
96
|
+
appears half-finished in the first few frames. */
|
|
97
|
+
.xeno-element[data-glyph='check'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
98
|
+
.xeno-element[data-glyph='check'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-check-draw; --part-dur: 900ms; stroke-dasharray: 23; }
|
|
99
|
+
@keyframes xeno-pt-check-draw {
|
|
100
|
+
0% { stroke-dashoffset: 23; transform: scale(1); }
|
|
101
|
+
72% { stroke-dashoffset: 0; transform: scale(1); }
|
|
102
|
+
86% { stroke-dashoffset: 0; transform: scale(1.13); }
|
|
103
|
+
100% { stroke-dashoffset: 0; transform: scale(1); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* chevron-down (whole) — dips down as if to expand */
|
|
107
|
+
.xeno-element[data-glyph='chevron-down'] { --xeno-icon-anim: xeno-ic-chevron-down; --xeno-icon-dur: 440ms; }
|
|
108
|
+
@keyframes xeno-ic-chevron-down {
|
|
109
|
+
0% { transform: translateY(0) scaleY(1); }
|
|
110
|
+
35% { transform: translateY(4px) scaleY(0.9); }
|
|
111
|
+
65% { transform: translateY(-2px) scaleY(1.05); }
|
|
112
|
+
100% { transform: translateY(0) scaleY(1); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* chevron-right (whole) — nudges onward */
|
|
116
|
+
.xeno-element[data-glyph='chevron-right'] { --xeno-icon-anim: xeno-ic-chevron-right; --xeno-icon-dur: 480ms; }
|
|
117
|
+
@keyframes xeno-ic-chevron-right {
|
|
118
|
+
0% { transform: translateX(0); }
|
|
119
|
+
25% { transform: translateX(-2px); }
|
|
120
|
+
55% { transform: translateX(4px); }
|
|
121
|
+
75% { transform: translateX(2px); }
|
|
122
|
+
100% { transform: translateX(0); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* arrow-right (whole) — shoots forward from its tail */
|
|
126
|
+
.xeno-element[data-glyph='arrow-right'] { --xeno-icon-anim: xeno-ic-arrow-right; --xeno-icon-dur: 500ms; transform-origin: left center; }
|
|
127
|
+
@keyframes xeno-ic-arrow-right {
|
|
128
|
+
0% { transform: translateX(0) scaleX(1); }
|
|
129
|
+
30% { transform: translateX(-2px) scaleX(0.9); }
|
|
130
|
+
60% { transform: translateX(5px) scaleX(1.15); }
|
|
131
|
+
100% { transform: translateX(0) scaleX(1); }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* arrow-up (whole) — an immediate leap with a floating double-bob */
|
|
135
|
+
.xeno-element[data-glyph='arrow-up'] { --xeno-icon-anim: xeno-ic-arrow-up; --xeno-icon-dur: 540ms; transform-origin: bottom center; }
|
|
136
|
+
@keyframes xeno-ic-arrow-up {
|
|
137
|
+
0% { transform: translateY(0) scaleY(1); }
|
|
138
|
+
30% { transform: translateY(-6px) scaleY(1.14); }
|
|
139
|
+
56% { transform: translateY(-2px) scaleY(0.96); }
|
|
140
|
+
78% { transform: translateY(-4px) scaleY(1.05); }
|
|
141
|
+
100% { transform: translateY(0) scaleY(1); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* gear (whole) — cranks forward in two ratchet steps */
|
|
145
|
+
.xeno-element[data-glyph='gear'] { --xeno-icon-anim: xeno-ic-gear; --xeno-icon-dur: 620ms; }
|
|
146
|
+
@keyframes xeno-ic-gear {
|
|
147
|
+
0% { transform: rotate(0deg); }
|
|
148
|
+
20% { transform: rotate(20deg); }
|
|
149
|
+
38% { transform: rotate(15deg); }
|
|
150
|
+
58% { transform: rotate(38deg); }
|
|
151
|
+
76% { transform: rotate(33deg); }
|
|
152
|
+
100% { transform: rotate(0deg); }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* bell (parts) — the bell rocks gently from its hang while the clapper at the BOTTOM swings hard and lags, like a real bell */
|
|
156
|
+
.xeno-element[data-glyph='bell'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-bell-body; --part-dur: 760ms; transform-origin: 50% 0%; }
|
|
157
|
+
.xeno-element[data-glyph='bell'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-bell-clap; --part-dur: 760ms; transform-origin: 50% 0%; }
|
|
158
|
+
@keyframes xeno-pt-bell-body {
|
|
159
|
+
0%, 100% { transform: rotate(0deg); }
|
|
160
|
+
22% { transform: rotate(14deg); }
|
|
161
|
+
48% { transform: rotate(-11deg); }
|
|
162
|
+
72% { transform: rotate(5deg); }
|
|
163
|
+
}
|
|
164
|
+
@keyframes xeno-pt-bell-clap {
|
|
165
|
+
0%, 100% { transform: rotate(0deg); }
|
|
166
|
+
16% { transform: rotate(34deg); }
|
|
167
|
+
40% { transform: rotate(-30deg); }
|
|
168
|
+
62% { transform: rotate(18deg); }
|
|
169
|
+
82% { transform: rotate(-7deg); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* mic (parts) — the capsule throbs twice, as if actively catching sound */
|
|
173
|
+
.xeno-element[data-glyph='mic'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-mic-cap; }
|
|
174
|
+
@keyframes xeno-pt-mic-cap {
|
|
175
|
+
0% { transform: scale(1); }
|
|
176
|
+
18% { transform: scale(1.42); }
|
|
177
|
+
36% { transform: scale(0.92); }
|
|
178
|
+
54% { transform: scale(1.24); }
|
|
179
|
+
74% { transform: scale(0.97); }
|
|
180
|
+
100% { transform: scale(1); }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* link (parts) — the two rings tug apart along the chain, then snap back */
|
|
184
|
+
.xeno-element[data-glyph='link'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-link-l; }
|
|
185
|
+
.xeno-element[data-glyph='link'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-link-r; }
|
|
186
|
+
@keyframes xeno-pt-link-l {
|
|
187
|
+
0% { transform: translate(0, 0); }
|
|
188
|
+
30% { transform: translate(-2px, 2px); }
|
|
189
|
+
55% { transform: translate(1px, -1px); }
|
|
190
|
+
100% { transform: translate(0, 0); }
|
|
191
|
+
}
|
|
192
|
+
@keyframes xeno-pt-link-r {
|
|
193
|
+
0% { transform: translate(0, 0); }
|
|
194
|
+
30% { transform: translate(2px, -2px); }
|
|
195
|
+
55% { transform: translate(-1px, 1px); }
|
|
196
|
+
100% { transform: translate(0, 0); }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* lock (parts) — the shackle springs open, hovers, then snaps shut with a latch-click overshoot */
|
|
200
|
+
.xeno-element[data-glyph='lock'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-lock-shackle; transform-origin: bottom center; }
|
|
201
|
+
@keyframes xeno-pt-lock-shackle {
|
|
202
|
+
0% { transform: translateY(0) scaleY(1); }
|
|
203
|
+
18% { transform: translateY(-3px) scaleY(1.09); }
|
|
204
|
+
46% { transform: translateY(-3px) scaleY(1.09); }
|
|
205
|
+
64% { transform: translateY(0.6px) scaleY(0.93); }
|
|
206
|
+
100% { transform: translateY(0) scaleY(1); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* heart (parts) — the solid fill rises from the point upward (the heart "fills up"), then the outline beats */
|
|
210
|
+
.xeno-element[data-glyph='heart'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-heart-beat; --part-dur: 820ms; transform-origin: center; }
|
|
211
|
+
.xeno-element[data-glyph='heart'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-heart-fill; --part-dur: 820ms; transform-origin: 50% 100%; opacity: 0; }
|
|
212
|
+
@keyframes xeno-pt-heart-fill {
|
|
213
|
+
0% { transform: scaleY(0); opacity: 1; }
|
|
214
|
+
55% { transform: scaleY(1); opacity: 1; }
|
|
215
|
+
100% { transform: scaleY(1); opacity: 1; }
|
|
216
|
+
}
|
|
217
|
+
@keyframes xeno-pt-heart-beat {
|
|
218
|
+
0%, 50% { transform: scale(1); }
|
|
219
|
+
64% { transform: scale(1.14); }
|
|
220
|
+
78% { transform: scale(0.97); }
|
|
221
|
+
90% { transform: scale(1.03); }
|
|
222
|
+
100% { transform: scale(1); }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* user (parts) — the head gives a small nod above the shoulders */
|
|
226
|
+
.xeno-element[data-glyph='user'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-user-head; transform-origin: bottom center; }
|
|
227
|
+
@keyframes xeno-pt-user-head {
|
|
228
|
+
0% { transform: translateY(0) rotate(0deg); }
|
|
229
|
+
28% { transform: translateY(-3.2px) rotate(-13deg); }
|
|
230
|
+
55% { transform: translateY(0) rotate(8deg); }
|
|
231
|
+
78% { transform: translateY(0) rotate(-3deg); }
|
|
232
|
+
100% { transform: translateY(0) rotate(0deg); }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* globe (parts) — the world TURNS. Four meridian halves ride the sphere: each one enters at the left limb,
|
|
236
|
+
bulges out, flattens to a straight line as it crosses the poles, bulges the other way and merges into the
|
|
237
|
+
right limb, where it fades and passes behind. Two are on the face at any moment (one, at the quarter turns).
|
|
238
|
+
|
|
239
|
+
The projection is exact, not a fudge. A half-meridian at longitude L projects to the same half-ellipse
|
|
240
|
+
scaled horizontally by sin(L), so scaleX IS the reprojection — sin() drives every stop below, sampled every
|
|
241
|
+
15deg of turn. The limbs are where it pays off: |scaleX| tops out at sqrt(2), which lands the meridian
|
|
242
|
+
exactly on x = 3 / x = 21, ON the frame, so opacity can switch there and the hand-over is invisible. The
|
|
243
|
+
previous version stopped at 7.0 of a 9 radius and vanished in mid-air, which is why only the middle of the
|
|
244
|
+
globe seemed to move.
|
|
245
|
+
|
|
246
|
+
All four share the polar axis as their transform origin, so they turn as one rigid sphere; the equator and
|
|
247
|
+
parallels do not move, because on a real globe they do not. Linear timing, or the spin would visibly slow
|
|
248
|
+
at the ends. NOTE: 3D transforms (rotateY/perspective) do NOT apply to shapes inside an svg — only to the
|
|
249
|
+
svg itself — so the rotation is carried honestly in 2D. `non-scaling-stroke` holds the line weight steady
|
|
250
|
+
while a meridian is stretched 1.41x, for the duration of the hover only. */
|
|
251
|
+
.xeno-element[data-glyph='globe'] { --xeno-icon-ease: linear; }
|
|
252
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='3'],
|
|
253
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='4'],
|
|
254
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='5'],
|
|
255
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='6'] {
|
|
256
|
+
--part-dur: 2800ms; --part-vfx: non-scaling-stroke;
|
|
257
|
+
transform-box: view-box; transform-origin: 12px 12px;
|
|
258
|
+
}
|
|
259
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-globe-m1; }
|
|
260
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='4'] { --part-anim: xeno-pt-globe-m2; }
|
|
261
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='5'] { --part-anim: xeno-pt-globe-m3; }
|
|
262
|
+
.xeno-element[data-glyph='globe'] .xeno-part[data-part='6'] { --part-anim: xeno-pt-globe-m4; }
|
|
263
|
+
/* [3] L=45deg at rest, on the face and heading for the right limb */
|
|
264
|
+
@keyframes xeno-pt-globe-m1 {
|
|
265
|
+
0% { transform: scaleX(1); opacity: 1; }
|
|
266
|
+
4.17% { transform: scaleX(1.22); opacity: 1; }
|
|
267
|
+
8.33% { transform: scaleX(1.37); opacity: 1; }
|
|
268
|
+
12.5% { transform: scaleX(1.41); opacity: 0; }
|
|
269
|
+
16.67% { transform: scaleX(1.37); opacity: 0; }
|
|
270
|
+
20.83% { transform: scaleX(1.22); opacity: 0; }
|
|
271
|
+
25% { transform: scaleX(1); opacity: 0; }
|
|
272
|
+
29.17% { transform: scaleX(0.71); opacity: 0; }
|
|
273
|
+
33.33% { transform: scaleX(0.37); opacity: 0; }
|
|
274
|
+
37.5% { transform: scaleX(0); opacity: 0; }
|
|
275
|
+
41.67% { transform: scaleX(-0.37); opacity: 0; }
|
|
276
|
+
45.83% { transform: scaleX(-0.71); opacity: 0; }
|
|
277
|
+
50% { transform: scaleX(-1); opacity: 0; }
|
|
278
|
+
54.17% { transform: scaleX(-1.22); opacity: 0; }
|
|
279
|
+
58.33% { transform: scaleX(-1.37); opacity: 0; }
|
|
280
|
+
62.5% { transform: scaleX(-1.41); opacity: 0; }
|
|
281
|
+
66.67% { transform: scaleX(-1.37); opacity: 1; }
|
|
282
|
+
70.83% { transform: scaleX(-1.22); opacity: 1; }
|
|
283
|
+
75% { transform: scaleX(-1); opacity: 1; }
|
|
284
|
+
79.17% { transform: scaleX(-0.71); opacity: 1; }
|
|
285
|
+
83.33% { transform: scaleX(-0.37); opacity: 1; }
|
|
286
|
+
87.5% { transform: scaleX(0); opacity: 1; }
|
|
287
|
+
91.67% { transform: scaleX(0.37); opacity: 1; }
|
|
288
|
+
95.83% { transform: scaleX(0.71); opacity: 1; }
|
|
289
|
+
100% { transform: scaleX(1); opacity: 1; }
|
|
290
|
+
}
|
|
291
|
+
/* [4] L=135deg — behind, comes round the left limb at 41.67% */
|
|
292
|
+
@keyframes xeno-pt-globe-m2 {
|
|
293
|
+
0% { transform: scaleX(1); opacity: 0; }
|
|
294
|
+
4.17% { transform: scaleX(0.71); opacity: 0; }
|
|
295
|
+
8.33% { transform: scaleX(0.37); opacity: 0; }
|
|
296
|
+
12.5% { transform: scaleX(0); opacity: 0; }
|
|
297
|
+
16.67% { transform: scaleX(-0.37); opacity: 0; }
|
|
298
|
+
20.83% { transform: scaleX(-0.71); opacity: 0; }
|
|
299
|
+
25% { transform: scaleX(-1); opacity: 0; }
|
|
300
|
+
29.17% { transform: scaleX(-1.22); opacity: 0; }
|
|
301
|
+
33.33% { transform: scaleX(-1.37); opacity: 0; }
|
|
302
|
+
37.5% { transform: scaleX(-1.41); opacity: 0; }
|
|
303
|
+
41.67% { transform: scaleX(-1.37); opacity: 1; }
|
|
304
|
+
45.83% { transform: scaleX(-1.22); opacity: 1; }
|
|
305
|
+
50% { transform: scaleX(-1); opacity: 1; }
|
|
306
|
+
54.17% { transform: scaleX(-0.71); opacity: 1; }
|
|
307
|
+
58.33% { transform: scaleX(-0.37); opacity: 1; }
|
|
308
|
+
62.5% { transform: scaleX(0); opacity: 1; }
|
|
309
|
+
66.67% { transform: scaleX(0.37); opacity: 1; }
|
|
310
|
+
70.83% { transform: scaleX(0.71); opacity: 1; }
|
|
311
|
+
75% { transform: scaleX(1); opacity: 1; }
|
|
312
|
+
79.17% { transform: scaleX(1.22); opacity: 1; }
|
|
313
|
+
83.33% { transform: scaleX(1.37); opacity: 1; }
|
|
314
|
+
87.5% { transform: scaleX(1.41); opacity: 0; }
|
|
315
|
+
91.67% { transform: scaleX(1.37); opacity: 0; }
|
|
316
|
+
95.83% { transform: scaleX(1.22); opacity: 0; }
|
|
317
|
+
100% { transform: scaleX(1); opacity: 0; }
|
|
318
|
+
}
|
|
319
|
+
/* [5] L=225deg — the twin of [3], half a turn behind it */
|
|
320
|
+
@keyframes xeno-pt-globe-m3 {
|
|
321
|
+
0% { transform: scaleX(1); opacity: 0; }
|
|
322
|
+
4.17% { transform: scaleX(1.22); opacity: 0; }
|
|
323
|
+
8.33% { transform: scaleX(1.37); opacity: 0; }
|
|
324
|
+
12.5% { transform: scaleX(1.41); opacity: 0; }
|
|
325
|
+
16.67% { transform: scaleX(1.37); opacity: 1; }
|
|
326
|
+
20.83% { transform: scaleX(1.22); opacity: 1; }
|
|
327
|
+
25% { transform: scaleX(1); opacity: 1; }
|
|
328
|
+
29.17% { transform: scaleX(0.71); opacity: 1; }
|
|
329
|
+
33.33% { transform: scaleX(0.37); opacity: 1; }
|
|
330
|
+
37.5% { transform: scaleX(0); opacity: 1; }
|
|
331
|
+
41.67% { transform: scaleX(-0.37); opacity: 1; }
|
|
332
|
+
45.83% { transform: scaleX(-0.71); opacity: 1; }
|
|
333
|
+
50% { transform: scaleX(-1); opacity: 1; }
|
|
334
|
+
54.17% { transform: scaleX(-1.22); opacity: 1; }
|
|
335
|
+
58.33% { transform: scaleX(-1.37); opacity: 1; }
|
|
336
|
+
62.5% { transform: scaleX(-1.41); opacity: 0; }
|
|
337
|
+
66.67% { transform: scaleX(-1.37); opacity: 0; }
|
|
338
|
+
70.83% { transform: scaleX(-1.22); opacity: 0; }
|
|
339
|
+
75% { transform: scaleX(-1); opacity: 0; }
|
|
340
|
+
79.17% { transform: scaleX(-0.71); opacity: 0; }
|
|
341
|
+
83.33% { transform: scaleX(-0.37); opacity: 0; }
|
|
342
|
+
87.5% { transform: scaleX(0); opacity: 0; }
|
|
343
|
+
91.67% { transform: scaleX(0.37); opacity: 0; }
|
|
344
|
+
95.83% { transform: scaleX(0.71); opacity: 0; }
|
|
345
|
+
100% { transform: scaleX(1); opacity: 0; }
|
|
346
|
+
}
|
|
347
|
+
/* [6] L=315deg — the twin of [4]; at rest it is the left arc of the ellipse */
|
|
348
|
+
@keyframes xeno-pt-globe-m4 {
|
|
349
|
+
0% { transform: scaleX(1); opacity: 1; }
|
|
350
|
+
4.17% { transform: scaleX(0.71); opacity: 1; }
|
|
351
|
+
8.33% { transform: scaleX(0.37); opacity: 1; }
|
|
352
|
+
12.5% { transform: scaleX(0); opacity: 1; }
|
|
353
|
+
16.67% { transform: scaleX(-0.37); opacity: 1; }
|
|
354
|
+
20.83% { transform: scaleX(-0.71); opacity: 1; }
|
|
355
|
+
25% { transform: scaleX(-1); opacity: 1; }
|
|
356
|
+
29.17% { transform: scaleX(-1.22); opacity: 1; }
|
|
357
|
+
33.33% { transform: scaleX(-1.37); opacity: 1; }
|
|
358
|
+
37.5% { transform: scaleX(-1.41); opacity: 0; }
|
|
359
|
+
41.67% { transform: scaleX(-1.37); opacity: 0; }
|
|
360
|
+
45.83% { transform: scaleX(-1.22); opacity: 0; }
|
|
361
|
+
50% { transform: scaleX(-1); opacity: 0; }
|
|
362
|
+
54.17% { transform: scaleX(-0.71); opacity: 0; }
|
|
363
|
+
58.33% { transform: scaleX(-0.37); opacity: 0; }
|
|
364
|
+
62.5% { transform: scaleX(0); opacity: 0; }
|
|
365
|
+
66.67% { transform: scaleX(0.37); opacity: 0; }
|
|
366
|
+
70.83% { transform: scaleX(0.71); opacity: 0; }
|
|
367
|
+
75% { transform: scaleX(1); opacity: 0; }
|
|
368
|
+
79.17% { transform: scaleX(1.22); opacity: 0; }
|
|
369
|
+
83.33% { transform: scaleX(1.37); opacity: 0; }
|
|
370
|
+
87.5% { transform: scaleX(1.41); opacity: 0; }
|
|
371
|
+
91.67% { transform: scaleX(1.37); opacity: 1; }
|
|
372
|
+
95.83% { transform: scaleX(1.22); opacity: 1; }
|
|
373
|
+
100% { transform: scaleX(1); opacity: 1; }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* eye (parts) — a blink of the lid and a glance of the pupil */
|
|
377
|
+
.xeno-element[data-glyph='eye'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-eye-lid; --part-vfx: non-scaling-stroke; }
|
|
378
|
+
.xeno-element[data-glyph='eye'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-eye-pupil; }
|
|
379
|
+
@keyframes xeno-pt-eye-lid {
|
|
380
|
+
0% { transform: scaleY(1); }
|
|
381
|
+
40% { transform: scaleY(1); }
|
|
382
|
+
50% { transform: scaleY(0.15); }
|
|
383
|
+
60% { transform: scaleY(1); }
|
|
384
|
+
100% { transform: scaleY(1); }
|
|
385
|
+
}
|
|
386
|
+
@keyframes xeno-pt-eye-pupil {
|
|
387
|
+
0% { transform: translateX(0); }
|
|
388
|
+
22% { transform: translateX(-2.5px); }
|
|
389
|
+
52% { transform: translateX(2.5px); }
|
|
390
|
+
78% { transform: translateX(-1px); }
|
|
391
|
+
100% { transform: translateX(0); }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* alert (parts) — the exclamation shakes inside a steady frame */
|
|
395
|
+
.xeno-element[data-glyph='alert'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-alert-mark; }
|
|
396
|
+
.xeno-element[data-glyph='alert'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-alert-mark; }
|
|
397
|
+
@keyframes xeno-pt-alert-mark {
|
|
398
|
+
0% { transform: translateX(0); }
|
|
399
|
+
15% { transform: translateX(-3.2px); }
|
|
400
|
+
30% { transform: translateX(3.2px); }
|
|
401
|
+
45% { transform: translateX(-2.5px); }
|
|
402
|
+
60% { transform: translateX(2.1px); }
|
|
403
|
+
78% { transform: translateX(-1px); }
|
|
404
|
+
100% { transform: translateX(0); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* bookmark (whole) — the ribbon swings on its top pin */
|
|
408
|
+
.xeno-element[data-glyph='bookmark'] { --xeno-icon-anim: xeno-ic-bookmark; --xeno-icon-dur: 640ms; transform-origin: top center; }
|
|
409
|
+
@keyframes xeno-ic-bookmark {
|
|
410
|
+
0% { transform: translateY(0) rotate(0deg); }
|
|
411
|
+
22% { transform: translateY(-1px) rotate(-13deg); }
|
|
412
|
+
50% { transform: translateY(0) rotate(9deg); }
|
|
413
|
+
74% { transform: translateY(0) rotate(-4deg); }
|
|
414
|
+
100% { transform: translateY(0) rotate(0deg); }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* star (whole) — a full spin that twinkles at the turns */
|
|
418
|
+
.xeno-element[data-glyph='star'] { --xeno-icon-anim: xeno-ic-star; --xeno-icon-dur: 620ms; }
|
|
419
|
+
@keyframes xeno-ic-star {
|
|
420
|
+
0% { transform: rotate(0deg) scale(1); opacity: 1; }
|
|
421
|
+
30% { transform: rotate(120deg) scale(1.18); opacity: 0.86; }
|
|
422
|
+
55% { transform: rotate(205deg) scale(0.92); opacity: 1; }
|
|
423
|
+
78% { transform: rotate(300deg) scale(1.06); }
|
|
424
|
+
100% { transform: rotate(360deg) scale(1); opacity: 1; }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* image (parts) — a day timelapse: the sun arcs up and across the sky (bigger at its peak) then back down */
|
|
428
|
+
.xeno-element[data-glyph='image'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-image-sun; --part-dur: 1300ms; transform-origin: center; }
|
|
429
|
+
@keyframes xeno-pt-image-sun {
|
|
430
|
+
0% { transform: translate(0, 0) scale(1); }
|
|
431
|
+
22% { transform: translate(2.5px, -3px) scale(1.14); }
|
|
432
|
+
48% { transform: translate(5px, -3.5px) scale(1.2); }
|
|
433
|
+
74% { transform: translate(6.5px, -1px) scale(1.06); }
|
|
434
|
+
100% { transform: translate(0, 0) scale(1); }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* file (parts) — the document's lines of text write themselves in, top to bottom */
|
|
438
|
+
.xeno-element[data-glyph='file'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-file-l0; --part-dur: 860ms; stroke-dasharray: 8; }
|
|
439
|
+
.xeno-element[data-glyph='file'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-file-l1; --part-dur: 860ms; stroke-dasharray: 8; }
|
|
440
|
+
.xeno-element[data-glyph='file'] .xeno-part[data-part='4'] { --part-anim: xeno-pt-file-l2; --part-dur: 860ms; stroke-dasharray: 5; }
|
|
441
|
+
@keyframes xeno-pt-file-l0 { 0%, 12% { stroke-dashoffset: 8; } 42%, 100% { stroke-dashoffset: 0; } }
|
|
442
|
+
@keyframes xeno-pt-file-l1 { 0%, 32% { stroke-dashoffset: 8; } 62%, 100% { stroke-dashoffset: 0; } }
|
|
443
|
+
@keyframes xeno-pt-file-l2 { 0%, 52% { stroke-dashoffset: 5; } 82%, 100% { stroke-dashoffset: 0; } }
|
|
444
|
+
|
|
445
|
+
/* folder (parts) — a document rises OUT of the folder's mouth and drops back in. The sheet and its lines
|
|
446
|
+
share one origin pinned to the folder's mouth (`view-box` lets all three parts use the same absolute
|
|
447
|
+
point) and grow upward from it, so NOTHING is ever drawn inside the folder — no show-through, which a
|
|
448
|
+
plain translate would give away since the folder's outline has no fill to hide behind. */
|
|
449
|
+
.xeno-element[data-glyph='folder'] { --xeno-icon-ease: cubic-bezier(0.34, 1.2, 0.5, 1); }
|
|
450
|
+
.xeno-element[data-glyph='folder'] .xeno-part[data-part='0'],
|
|
451
|
+
.xeno-element[data-glyph='folder'] .xeno-part[data-part='1'],
|
|
452
|
+
.xeno-element[data-glyph='folder'] .xeno-part[data-part='2'] {
|
|
453
|
+
--part-anim: xeno-pt-folder-doc; --part-dur: 900ms;
|
|
454
|
+
transform-box: view-box; transform-origin: 13.4px 7.4px; opacity: 0;
|
|
455
|
+
}
|
|
456
|
+
.xeno-element[data-glyph='folder'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-folder-body; --part-dur: 900ms; transform-origin: 50% 100%; }
|
|
457
|
+
@keyframes xeno-pt-folder-doc {
|
|
458
|
+
0% { transform: scaleY(0); opacity: 1; }
|
|
459
|
+
42% { transform: scaleY(1); opacity: 1; }
|
|
460
|
+
74% { transform: scaleY(1); opacity: 1; }
|
|
461
|
+
100% { transform: scaleY(0); opacity: 1; }
|
|
462
|
+
}
|
|
463
|
+
@keyframes xeno-pt-folder-body {
|
|
464
|
+
0%, 100% { transform: scaleY(1); }
|
|
465
|
+
42% { transform: scaleY(1.03); }
|
|
466
|
+
76% { transform: scaleY(1.03); }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* copy (parts) — the front sheet lifts up-and-away to reveal the duplicate beneath it, then settles back */
|
|
470
|
+
.xeno-element[data-glyph='copy'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-copy-front; --part-dur: 640ms; }
|
|
471
|
+
@keyframes xeno-pt-copy-front {
|
|
472
|
+
0% { transform: translate(0, 0) scale(1); }
|
|
473
|
+
38% { transform: translate(-2.6px, -2.6px) scale(1.05); }
|
|
474
|
+
70% { transform: translate(0.8px, 0.8px) scale(0.99); }
|
|
475
|
+
100% { transform: translate(0, 0) scale(1); }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* edit (parts) — the pencil actually WRITES: it travels to the right, leaving a drawn stroke behind
|
|
479
|
+
its tip, nodding on each press; the stroke then fades and the pencil returns. */
|
|
480
|
+
.xeno-element[data-glyph='edit'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
481
|
+
.xeno-element[data-glyph='edit'] .xeno-part[data-part='0'],
|
|
482
|
+
.xeno-element[data-glyph='edit'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-edit-hand; --part-dur: 1000ms; transform-origin: 18% 88%; }
|
|
483
|
+
.xeno-element[data-glyph='edit'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-edit-stroke; --part-dur: 1000ms; stroke-dasharray: 7.5; opacity: 0; }
|
|
484
|
+
@keyframes xeno-pt-edit-hand {
|
|
485
|
+
0% { transform: translate(0, 0) rotate(0deg); }
|
|
486
|
+
12% { transform: translate(0.3px, 0.5px) rotate(-2deg); }
|
|
487
|
+
30% { transform: translate(2px, 0.2px) rotate(1.5deg); }
|
|
488
|
+
48% { transform: translate(3.6px, 0.5px) rotate(-1.5deg); }
|
|
489
|
+
66% { transform: translate(5.2px, 0.2px) rotate(1.5deg); }
|
|
490
|
+
78% { transform: translate(5.2px, 0) rotate(0deg); }
|
|
491
|
+
100% { transform: translate(0, 0) rotate(0deg); }
|
|
492
|
+
}
|
|
493
|
+
@keyframes xeno-pt-edit-stroke {
|
|
494
|
+
0% { stroke-dashoffset: 7.5; opacity: 0; }
|
|
495
|
+
12% { stroke-dashoffset: 7.5; opacity: 1; }
|
|
496
|
+
70% { stroke-dashoffset: 0; opacity: 1; }
|
|
497
|
+
86% { stroke-dashoffset: 0; opacity: 1; }
|
|
498
|
+
100% { stroke-dashoffset: 0; opacity: 0; }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* trash (parts) — the lid lifts open, the bin gulps (something drops in), the lid drops shut */
|
|
502
|
+
.xeno-element[data-glyph='trash'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-trash-lid; --part-dur: 760ms; transform-origin: 0% 60%; }
|
|
503
|
+
.xeno-element[data-glyph='trash'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-trash-bin; --part-dur: 760ms; transform-origin: center top; }
|
|
504
|
+
@keyframes xeno-pt-trash-lid {
|
|
505
|
+
0% { transform: rotate(0deg) translateY(0); }
|
|
506
|
+
22% { transform: rotate(-26deg) translateY(-1.2px); }
|
|
507
|
+
55% { transform: rotate(-26deg) translateY(-1.2px); }
|
|
508
|
+
82%, 100% { transform: rotate(0deg) translateY(0); }
|
|
509
|
+
}
|
|
510
|
+
@keyframes xeno-pt-trash-bin {
|
|
511
|
+
0%, 30%, 100% { transform: scaleY(1); }
|
|
512
|
+
44% { transform: scaleY(0.9); }
|
|
513
|
+
62% { transform: scaleY(1.02); }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* download (parts) — only the arrow plunges down into the tray, which gives a little bounce (split geometry) */
|
|
517
|
+
.xeno-element[data-glyph='download'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-download-arrow; --part-dur: 680ms; transform-origin: center; }
|
|
518
|
+
.xeno-element[data-glyph='download'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-download-tray; --part-dur: 680ms; transform-origin: center; }
|
|
519
|
+
@keyframes xeno-pt-download-arrow {
|
|
520
|
+
0% { transform: translateY(0) scaleY(1); }
|
|
521
|
+
18% { transform: translateY(-2.5px) scaleY(1.05); }
|
|
522
|
+
50% { transform: translateY(4.5px) scaleY(1.08); }
|
|
523
|
+
66% { transform: translateY(3px) scaleY(0.82); }
|
|
524
|
+
84% { transform: translateY(0) scaleY(1); }
|
|
525
|
+
100% { transform: translateY(0) scaleY(1); }
|
|
526
|
+
}
|
|
527
|
+
@keyframes xeno-pt-download-tray {
|
|
528
|
+
0%, 46%, 100% { transform: translateY(0) scaleX(1); }
|
|
529
|
+
56% { transform: translateY(0.8px) scaleX(1.06); }
|
|
530
|
+
72% { transform: translateY(0) scaleX(1); }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* upload (parts) — the arrow launches up out of its tray, fading at the apex as if sent, then settles */
|
|
534
|
+
.xeno-element[data-glyph='upload'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-upload-arrow; transform-origin: bottom center; }
|
|
535
|
+
@keyframes xeno-pt-upload-arrow {
|
|
536
|
+
0% { transform: translateY(0); opacity: 1; }
|
|
537
|
+
30% { transform: translateY(-4.5px); opacity: 0.68; }
|
|
538
|
+
52% { transform: translateY(-0.5px); opacity: 1; }
|
|
539
|
+
76% { transform: translateY(-2px); }
|
|
540
|
+
100% { transform: translateY(0); opacity: 1; }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* send (whole) — the plane darts off and fades, then returns */
|
|
544
|
+
.xeno-element[data-glyph='send'] { --xeno-icon-anim: xeno-ic-send; --xeno-icon-dur: 640ms; }
|
|
545
|
+
@keyframes xeno-ic-send {
|
|
546
|
+
0% { transform: translate(0, 0) scale(1); opacity: 1; }
|
|
547
|
+
18% { transform: translate(-1.5px, 1.5px) scale(0.92); opacity: 1; }
|
|
548
|
+
52% { transform: translate(4.5px, -4.5px) scale(1.08); opacity: 0.55; }
|
|
549
|
+
60% { transform: translate(5px, -5px) scale(1.02); opacity: 0.5; }
|
|
550
|
+
61% { transform: translate(-3.5px, 3.5px) scale(0.9); opacity: 0.5; }
|
|
551
|
+
100% { transform: translate(0, 0) scale(1); opacity: 1; }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* code (parts) — the two brackets fly apart to open a gap, then settle (the split geometry, animated) */
|
|
555
|
+
.xeno-element[data-glyph='code'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-code-l; --part-dur: 560ms; }
|
|
556
|
+
.xeno-element[data-glyph='code'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-code-r; --part-dur: 560ms; }
|
|
557
|
+
@keyframes xeno-pt-code-l {
|
|
558
|
+
0% { transform: translateX(0); }
|
|
559
|
+
30% { transform: translateX(-2.5px); }
|
|
560
|
+
55% { transform: translateX(0.8px); }
|
|
561
|
+
100% { transform: translateX(0); }
|
|
562
|
+
}
|
|
563
|
+
@keyframes xeno-pt-code-r {
|
|
564
|
+
0% { transform: translateX(0); }
|
|
565
|
+
30% { transform: translateX(2.5px); }
|
|
566
|
+
55% { transform: translateX(-0.8px); }
|
|
567
|
+
100% { transform: translateX(0); }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* terminal (parts) — like a real console, ONLY the cursor blinks; the ">" prompt + window stay still */
|
|
571
|
+
.xeno-element[data-glyph='terminal'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-terminal-cursor; --part-dur: 900ms; }
|
|
572
|
+
@keyframes xeno-pt-terminal-cursor {
|
|
573
|
+
0%, 100% { opacity: 1; }
|
|
574
|
+
25% { opacity: 1; }
|
|
575
|
+
26%, 55% { opacity: 0; }
|
|
576
|
+
56% { opacity: 1; }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* more (parts) — the three dots bounce in sequence */
|
|
580
|
+
.xeno-element[data-glyph='more'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-more-0; }
|
|
581
|
+
.xeno-element[data-glyph='more'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-more-1; }
|
|
582
|
+
.xeno-element[data-glyph='more'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-more-2; }
|
|
583
|
+
@keyframes xeno-pt-more-0 { 0% { transform: translateY(0); } 22% { transform: translateY(-3px); } 46% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
584
|
+
@keyframes xeno-pt-more-1 { 0% { transform: translateY(0); } 14% { transform: translateY(0); } 36% { transform: translateY(-3px); } 60% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
585
|
+
@keyframes xeno-pt-more-2 { 0% { transform: translateY(0); } 28% { transform: translateY(0); } 50% { transform: translateY(-3px); } 74% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
586
|
+
|
|
587
|
+
/* grid (parts) — the four tiles pop on in a diagonal cascade */
|
|
588
|
+
.xeno-element[data-glyph='grid'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-grid-0; }
|
|
589
|
+
.xeno-element[data-glyph='grid'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-grid-1; }
|
|
590
|
+
.xeno-element[data-glyph='grid'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-grid-2; }
|
|
591
|
+
.xeno-element[data-glyph='grid'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-grid-3; }
|
|
592
|
+
@keyframes xeno-pt-grid-0 { 0% { transform: scale(1); } 20% { transform: scale(1.5); } 44% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
593
|
+
@keyframes xeno-pt-grid-1 { 0% { transform: scale(1); } 12% { transform: scale(1); } 32% { transform: scale(1.5); } 56% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
594
|
+
@keyframes xeno-pt-grid-2 { 0% { transform: scale(1); } 24% { transform: scale(1); } 44% { transform: scale(1.5); } 68% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
595
|
+
@keyframes xeno-pt-grid-3 { 0% { transform: scale(1); } 36% { transform: scale(1); } 56% { transform: scale(1.5); } 80% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
596
|
+
|
|
597
|
+
/* list (parts) — each row's bullet pops and its line slides in, top to bottom */
|
|
598
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-list-l0; transform-origin: left center; }
|
|
599
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-list-l1; transform-origin: left center; }
|
|
600
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-list-l2; transform-origin: left center; }
|
|
601
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-list-b0; }
|
|
602
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='4'] { --part-anim: xeno-pt-list-b1; }
|
|
603
|
+
.xeno-element[data-glyph='list'] .xeno-part[data-part='5'] { --part-anim: xeno-pt-list-b2; }
|
|
604
|
+
@keyframes xeno-pt-list-l0 { 0% { transform: translateX(0); } 20% { transform: translateX(2.5px); } 46% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
605
|
+
@keyframes xeno-pt-list-l1 { 0% { transform: translateX(0); } 14% { transform: translateX(0); } 34% { transform: translateX(2.5px); } 60% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
606
|
+
@keyframes xeno-pt-list-l2 { 0% { transform: translateX(0); } 28% { transform: translateX(0); } 48% { transform: translateX(2.5px); } 74% { transform: translateX(0); } 100% { transform: translateX(0); } }
|
|
607
|
+
@keyframes xeno-pt-list-b0 { 0% { transform: scale(1); } 20% { transform: scale(1.3); } 46% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
608
|
+
@keyframes xeno-pt-list-b1 { 0% { transform: scale(1); } 14% { transform: scale(1); } 34% { transform: scale(1.3); } 60% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
609
|
+
@keyframes xeno-pt-list-b2 { 0% { transform: scale(1); } 28% { transform: scale(1); } 48% { transform: scale(1.3); } 74% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
610
|
+
|
|
611
|
+
/* filter (whole) — the funnel pinches narrow then relaxes */
|
|
612
|
+
.xeno-element[data-glyph='filter'] { --xeno-icon-anim: xeno-ic-filter; --xeno-icon-dur: 500ms; transform-origin: top center; }
|
|
613
|
+
@keyframes xeno-ic-filter {
|
|
614
|
+
0% { transform: scaleX(1) scaleY(1); }
|
|
615
|
+
30% { transform: scaleX(0.68) scaleY(1.06); }
|
|
616
|
+
50% { transform: scaleX(0.76) scaleY(1.03); }
|
|
617
|
+
70% { transform: scaleX(1.07) scaleY(0.95); }
|
|
618
|
+
100% { transform: scaleX(1) scaleY(1); }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* sliders (parts) — the two knobs travel their tracks in opposite directions */
|
|
622
|
+
.xeno-element[data-glyph='sliders'] .xeno-part[data-part='4'] { --part-anim: xeno-pt-sliders-k1; }
|
|
623
|
+
.xeno-element[data-glyph='sliders'] .xeno-part[data-part='5'] { --part-anim: xeno-pt-sliders-k2; }
|
|
624
|
+
@keyframes xeno-pt-sliders-k1 {
|
|
625
|
+
0% { transform: translateX(0); }
|
|
626
|
+
30% { transform: translateX(-4px); }
|
|
627
|
+
60% { transform: translateX(1.5px); }
|
|
628
|
+
100% { transform: translateX(0); }
|
|
629
|
+
}
|
|
630
|
+
@keyframes xeno-pt-sliders-k2 {
|
|
631
|
+
0% { transform: translateX(0); }
|
|
632
|
+
30% { transform: translateX(4px); }
|
|
633
|
+
60% { transform: translateX(-1.5px); }
|
|
634
|
+
100% { transform: translateX(0); }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* refresh (whole) — a wind-up then a full clockwise revolution */
|
|
638
|
+
.xeno-element[data-glyph='refresh'] { --xeno-icon-anim: xeno-ic-refresh; --xeno-icon-dur: 680ms; }
|
|
639
|
+
@keyframes xeno-ic-refresh {
|
|
640
|
+
0% { transform: rotate(0deg) scale(1); }
|
|
641
|
+
12% { transform: rotate(-28deg) scale(0.95); }
|
|
642
|
+
60% { transform: rotate(230deg) scale(1.02); }
|
|
643
|
+
100% { transform: rotate(360deg) scale(1); }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* share (parts) — the three nodes pulse along the branch */
|
|
647
|
+
.xeno-element[data-glyph='share'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-share-0; }
|
|
648
|
+
.xeno-element[data-glyph='share'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-share-1; }
|
|
649
|
+
.xeno-element[data-glyph='share'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-share-2; }
|
|
650
|
+
@keyframes xeno-pt-share-0 { 0% { transform: scale(1); } 22% { transform: scale(1.75); } 48% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
651
|
+
@keyframes xeno-pt-share-1 { 0% { transform: scale(1); } 16% { transform: scale(1); } 38% { transform: scale(1.75); } 62% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
652
|
+
@keyframes xeno-pt-share-2 { 0% { transform: scale(1); } 30% { transform: scale(1); } 52% { transform: scale(1.75); } 76% { transform: scale(1); } 100% { transform: scale(1); } }
|
|
653
|
+
|
|
654
|
+
/* clock (parts) — the hands sweep a full clockwise revolution about the centre, like a real clock; the face holds still */
|
|
655
|
+
.xeno-element[data-glyph='clock'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-clock-hands; --part-dur: 900ms; transform-origin: 0% 69%; }
|
|
656
|
+
@keyframes xeno-pt-clock-hands {
|
|
657
|
+
0% { transform: rotate(0deg); }
|
|
658
|
+
100% { transform: rotate(360deg); }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* home (parts) — the house BUILDS itself: the walls rise off the ground, the roof drops on top and lands
|
|
662
|
+
with a small settle, then the door appears last. All three share the ground line as their origin. */
|
|
663
|
+
.xeno-element[data-glyph='home'] { --xeno-icon-ease: cubic-bezier(0.34, 1.3, 0.5, 1); }
|
|
664
|
+
.xeno-element[data-glyph='home'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-home-roof; --part-dur: 1000ms; transform-box: view-box; transform-origin: 12px 20px; }
|
|
665
|
+
.xeno-element[data-glyph='home'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-home-walls; --part-dur: 1000ms; transform-box: view-box; transform-origin: 12px 20px; }
|
|
666
|
+
.xeno-element[data-glyph='home'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-home-door; --part-dur: 1000ms; transform-box: view-box; transform-origin: 12px 20px; }
|
|
667
|
+
@keyframes xeno-pt-home-walls {
|
|
668
|
+
0% { transform: scaleY(0); }
|
|
669
|
+
34% { transform: scaleY(1); }
|
|
670
|
+
100% { transform: scaleY(1); }
|
|
671
|
+
}
|
|
672
|
+
@keyframes xeno-pt-home-roof {
|
|
673
|
+
0%, 22% { transform: translateY(-9px); opacity: 0; }
|
|
674
|
+
30% { transform: translateY(-9px); opacity: 1; }
|
|
675
|
+
56% { transform: translateY(0); opacity: 1; }
|
|
676
|
+
66% { transform: translateY(-1.2px); opacity: 1; }
|
|
677
|
+
78% { transform: translateY(0); opacity: 1; }
|
|
678
|
+
100% { transform: translateY(0); opacity: 1; }
|
|
679
|
+
}
|
|
680
|
+
@keyframes xeno-pt-home-door {
|
|
681
|
+
0%, 70% { transform: scaleY(0); }
|
|
682
|
+
88% { transform: scaleY(1); }
|
|
683
|
+
100% { transform: scaleY(1); }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* info (parts) — only the i-dot lifts and taps back down with a little squash ("gets dotted") */
|
|
687
|
+
.xeno-element[data-glyph='info'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-info-dot; --part-dur: 620ms; }
|
|
688
|
+
@keyframes xeno-pt-info-dot {
|
|
689
|
+
0% { transform: translateY(0) scale(1, 1); }
|
|
690
|
+
35% { transform: translateY(-3px) scale(1, 1); }
|
|
691
|
+
68% { transform: translateY(0.5px) scale(1.18, 0.82); }
|
|
692
|
+
84% { transform: translateY(0) scale(0.94, 1.06); }
|
|
693
|
+
100% { transform: translateY(0) scale(1, 1); }
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* help (parts) — the question mark writes itself: the hook draws in, then the dot drops onto the page */
|
|
697
|
+
.xeno-element[data-glyph='help'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-help-draw; --part-dur: 720ms; stroke-dasharray: 16; }
|
|
698
|
+
.xeno-element[data-glyph='help'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-help-dot; --part-dur: 720ms; transform-origin: center; }
|
|
699
|
+
@keyframes xeno-pt-help-draw {
|
|
700
|
+
0% { stroke-dashoffset: 16; }
|
|
701
|
+
55% { stroke-dashoffset: 0; }
|
|
702
|
+
100% { stroke-dashoffset: 0; }
|
|
703
|
+
}
|
|
704
|
+
@keyframes xeno-pt-help-dot {
|
|
705
|
+
0%, 48% { transform: translateY(-3px) scale(0); opacity: 0; }
|
|
706
|
+
66% { transform: translateY(0) scale(1.3); opacity: 1; }
|
|
707
|
+
82% { transform: translateY(0) scale(0.9); opacity: 1; }
|
|
708
|
+
100% { transform: translateY(0) scale(1); opacity: 1; }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* message (parts) — the bubble nudges from its tail while the text lines write themselves in */
|
|
712
|
+
.xeno-element[data-glyph='message'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-message-pop; --part-dur: 620ms; transform-origin: 0% 100%; }
|
|
713
|
+
.xeno-element[data-glyph='message'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-message-lines; --part-dur: 620ms; stroke-dasharray: 12; }
|
|
714
|
+
@keyframes xeno-pt-message-pop {
|
|
715
|
+
0% { transform: scale(1) rotate(0deg); }
|
|
716
|
+
40% { transform: scale(1.06) rotate(-2deg); }
|
|
717
|
+
72% { transform: scale(0.99) rotate(0.5deg); }
|
|
718
|
+
100% { transform: scale(1) rotate(0deg); }
|
|
719
|
+
}
|
|
720
|
+
@keyframes xeno-pt-message-lines {
|
|
721
|
+
0% { stroke-dashoffset: 12; opacity: 0; }
|
|
722
|
+
35% { stroke-dashoffset: 9; opacity: 1; }
|
|
723
|
+
100% { stroke-dashoffset: 0; opacity: 1; }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* attach (whole) — the clip swings from its top hinge */
|
|
727
|
+
.xeno-element[data-glyph='attach'] { --xeno-icon-anim: xeno-ic-attach; --xeno-icon-dur: 700ms; transform-origin: top center; }
|
|
728
|
+
@keyframes xeno-ic-attach {
|
|
729
|
+
0% { transform: translateY(0) rotate(0deg); }
|
|
730
|
+
15% { transform: translateY(1px) rotate(14deg); }
|
|
731
|
+
38% { transform: translateY(0) rotate(-10deg); }
|
|
732
|
+
58% { transform: translateY(0) rotate(6deg); }
|
|
733
|
+
78% { transform: translateY(0) rotate(-3deg); }
|
|
734
|
+
100% { transform: translateY(0) rotate(0deg); }
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* layers (parts) — the top and bottom sheets fan apart, then re-stack */
|
|
738
|
+
.xeno-element[data-glyph='layers'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-layers-top; }
|
|
739
|
+
.xeno-element[data-glyph='layers'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-layers-bot; }
|
|
740
|
+
@keyframes xeno-pt-layers-top {
|
|
741
|
+
0% { transform: translateY(0); }
|
|
742
|
+
30% { transform: translateY(-2.5px); }
|
|
743
|
+
60% { transform: translateY(0.5px); }
|
|
744
|
+
100% { transform: translateY(0); }
|
|
745
|
+
}
|
|
746
|
+
@keyframes xeno-pt-layers-bot {
|
|
747
|
+
0% { transform: translateY(0); }
|
|
748
|
+
30% { transform: translateY(2px); }
|
|
749
|
+
60% { transform: translateY(-0.5px); }
|
|
750
|
+
100% { transform: translateY(0); }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* sun (parts) — the rays wheel a full turn while the core pulses warm */
|
|
754
|
+
.xeno-element[data-glyph='sun'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-sun-core; }
|
|
755
|
+
.xeno-element[data-glyph='sun'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-sun-rays; --part-dur: 700ms; }
|
|
756
|
+
@keyframes xeno-pt-sun-core {
|
|
757
|
+
0% { transform: scale(1); }
|
|
758
|
+
30% { transform: scale(1.14); }
|
|
759
|
+
60% { transform: scale(0.96); }
|
|
760
|
+
100% { transform: scale(1); }
|
|
761
|
+
}
|
|
762
|
+
@keyframes xeno-pt-sun-rays {
|
|
763
|
+
0% { transform: rotate(0deg); }
|
|
764
|
+
100% { transform: rotate(360deg); }
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* archive (parts) — the lid LIFTS STRAIGHT UP and presses back down, and the box takes the press.
|
|
768
|
+
Deliberately not the trash lid, which hinges open and flaps: archiving is putting something away and
|
|
769
|
+
sealing it, discarding is throwing it out, and the two gestures should not look alike on icons this
|
|
770
|
+
close in meaning. The slot holds still — it is the mark that says things come back out. */
|
|
771
|
+
.xeno-element[data-glyph='archive'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-archive-lid; --part-dur: 620ms; }
|
|
772
|
+
.xeno-element[data-glyph='archive'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-archive-body; --part-dur: 620ms; transform-origin: center bottom; }
|
|
773
|
+
@keyframes xeno-pt-archive-lid {
|
|
774
|
+
0% { transform: translateY(0); }
|
|
775
|
+
30% { transform: translateY(-3px); }
|
|
776
|
+
62% { transform: translateY(0.6px); }
|
|
777
|
+
100% { transform: translateY(0); }
|
|
778
|
+
}
|
|
779
|
+
@keyframes xeno-pt-archive-body {
|
|
780
|
+
0% { transform: scaleY(1); }
|
|
781
|
+
62% { transform: scaleY(0.96); }
|
|
782
|
+
100% { transform: scaleY(1); }
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* user-x (parts) — the cross DRAWS ITSELF, one stroke then the other, while the figure leans away.
|
|
786
|
+
The X is the whole message (this person is not being kept), so it is the thing that performs; the
|
|
787
|
+
figure only acknowledges it. Steady easing, as with `check`: the library's default ease-out
|
|
788
|
+
front-loads a dash and the stroke looks half-finished in the first frames. */
|
|
789
|
+
.xeno-element[data-glyph='user-x'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
790
|
+
.xeno-element[data-glyph='user-x'] .xeno-part[data-part='0'],
|
|
791
|
+
.xeno-element[data-glyph='user-x'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-userx-figure; --part-dur: 640ms; }
|
|
792
|
+
.xeno-element[data-glyph='user-x'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-userx-a; --part-dur: 640ms; stroke-dasharray: 6.4; }
|
|
793
|
+
.xeno-element[data-glyph='user-x'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-userx-b; --part-dur: 640ms; stroke-dasharray: 6.4; }
|
|
794
|
+
@keyframes xeno-pt-userx-figure {
|
|
795
|
+
0% { transform: translateX(0); }
|
|
796
|
+
45% { transform: translateX(-0.8px); }
|
|
797
|
+
100% { transform: translateX(0); }
|
|
798
|
+
}
|
|
799
|
+
@keyframes xeno-pt-userx-a {
|
|
800
|
+
0% { stroke-dashoffset: 6.4; }
|
|
801
|
+
45% { stroke-dashoffset: 0; }
|
|
802
|
+
100% { stroke-dashoffset: 0; }
|
|
803
|
+
}
|
|
804
|
+
@keyframes xeno-pt-userx-b {
|
|
805
|
+
0%,
|
|
806
|
+
45% { stroke-dashoffset: 6.4; }
|
|
807
|
+
90% { stroke-dashoffset: 0; }
|
|
808
|
+
100% { stroke-dashoffset: 0; }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* contrast (whole) — the icon turns half a revolution, so the solid half changes sides. That IS the
|
|
812
|
+
control: a theme toggle swaps which half is dark, and rotating the same shape says it without
|
|
813
|
+
redrawing anything. It stops at 180deg rather than returning, then the animation ends and the icon
|
|
814
|
+
snaps back to rest — so a second hover reads as another flip rather than a rewind. */
|
|
815
|
+
/* contrast (parts) — a TIMELAPSE. Daylight crosses the square: the shadow already covering the left
|
|
816
|
+
half sweeps on across the right until the whole face is dark, and it HOLDS there while you point at
|
|
817
|
+
it. Leave, and it is morning again.
|
|
818
|
+
The hold is the point. `forwards` keeps the last frame instead of letting the icon snap back mid-
|
|
819
|
+
gesture, and removing the animation on mouse-out is what performs the reset — so the two states are
|
|
820
|
+
"pointing at it" and "not", which is what the control actually has.
|
|
821
|
+
|
|
822
|
+
It is the FRAME that fills, not a second shape, and that is what keeps the declaration honest. The
|
|
823
|
+
frame carries `fill: none` from the svg root, so with no stylesheet at all the icon still draws as
|
|
824
|
+
the half-solid square it is meant to be — the sweep exists only in CSS. The alternative, a full-square
|
|
825
|
+
fill sitting in the geometry, would render every no-CSS copy of this glyph as a solid block.
|
|
826
|
+
|
|
827
|
+
`clip-path` starts at the centre line rather than the left edge: the left half is already dark, so
|
|
828
|
+
sweeping from x=3 would spend the first half of the animation re-filling pixels that are not
|
|
829
|
+
changing. And clipping the rect leaves its own rounded corners intact all the way across, which
|
|
830
|
+
scaling a straight-edged half could never do — the corner would have arrived square. */
|
|
831
|
+
.xeno-element[data-glyph='contrast'] .xeno-part[data-part='2'] {
|
|
832
|
+
--part-anim: xeno-pt-contrast-sweep;
|
|
833
|
+
--part-dur: 2400ms;
|
|
834
|
+
/* Symmetric easing and a linear-ish middle: a timelapse that accelerates halfway is a timelapse of
|
|
835
|
+
nothing in particular. */
|
|
836
|
+
--xeno-icon-ease: cubic-bezier(0.45, 0, 0.55, 1);
|
|
837
|
+
animation-iteration-count: infinite;
|
|
838
|
+
}
|
|
839
|
+
/* The resting half is hidden for the whole run: the sweeping layer reproduces it exactly at 0%, so
|
|
840
|
+
leaving both would double-paint the same pixels and, worse, pin a solid left half under an animation
|
|
841
|
+
whose whole point is that the left half empties. */
|
|
842
|
+
.xeno-element[data-glyph='contrast'] .xeno-part[data-part='1'] {
|
|
843
|
+
--part-anim: xeno-pt-contrast-rest;
|
|
844
|
+
--part-dur: 1ms;
|
|
845
|
+
animation-fill-mode: forwards;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/*
|
|
849
|
+
* A whole day, on a loop, and it has to JOIN UP at both ends.
|
|
850
|
+
*
|
|
851
|
+
* It begins on the resting drawing — the left half dark — so starting the loop is invisible: the frame
|
|
852
|
+
* the animation opens with is the frame that was already there.
|
|
853
|
+
*
|
|
854
|
+
* The seam at the end is the interesting part. The cycle finishes with the square empty, and it has to
|
|
855
|
+
* resume with the left half dark, which is not the same picture. The trick is that "empty" can be
|
|
856
|
+
* spelled two ways: clipped away from the LEFT (where the light left it) or from the RIGHT (where the
|
|
857
|
+
* dark will grow back from). Both show nothing, so switching between them costs nothing to look at —
|
|
858
|
+
* and after the switch, closing the right-hand clip grows darkness inward from the left edge, which is
|
|
859
|
+
* exactly how the day started. No jump, and no rewind: the light always travels one way.
|
|
860
|
+
*/
|
|
861
|
+
@keyframes xeno-pt-contrast-sweep {
|
|
862
|
+
/* the resting drawing */
|
|
863
|
+
0% { fill: currentColor; clip-path: inset(0 50% 0 0); }
|
|
864
|
+
/* dusk — the shadow crosses the right half */
|
|
865
|
+
32% { fill: currentColor; clip-path: inset(0 0 0 0); }
|
|
866
|
+
/* midnight — a beat of full dark, so the turn reads as a turn and not a bounce */
|
|
867
|
+
44% { fill: currentColor; clip-path: inset(0 0 0 0); }
|
|
868
|
+
/* dawn — the light sweeps the dark away, from the left */
|
|
869
|
+
74% { fill: currentColor; clip-path: inset(0 0 0 100%); }
|
|
870
|
+
/* the same nothing, re-anchored to the other edge — invisible, because nothing is showing */
|
|
871
|
+
74.01% { fill: currentColor; clip-path: inset(0 100% 0 0); }
|
|
872
|
+
/* a beat of bare border, which is the state worth pausing on */
|
|
873
|
+
82% { fill: currentColor; clip-path: inset(0 100% 0 0); }
|
|
874
|
+
/* dawn's shadow grows back from the left, and lands exactly on the frame it started from */
|
|
875
|
+
100% { fill: currentColor; clip-path: inset(0 50% 0 0); }
|
|
876
|
+
}
|
|
877
|
+
@keyframes xeno-pt-contrast-rest {
|
|
878
|
+
from,
|
|
879
|
+
to { opacity: 0; }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* briefcase (parts) — the case OPENS. The lid hinges up off the clasp line and the handle rides with
|
|
883
|
+
it, because a handle is fixed to a lid and does not stay behind; the base holds still and takes the
|
|
884
|
+
weight.
|
|
885
|
+
|
|
886
|
+
It replaces a version where the whole icon bobbed up and down, which said nothing: a case that is
|
|
887
|
+
lifted is a case doing what any box does. A case that OPENS is the one thing only a case does — and
|
|
888
|
+
the glyph stands for "customize", where the point is getting at what is inside.
|
|
889
|
+
|
|
890
|
+
The hinge is at the LEFT end of the seam, so the right side lifts further than the left and the gap
|
|
891
|
+
is a wedge rather than a slab sliding off. Two degrees of rotation with a small lift under it: at
|
|
892
|
+
24px a wedge is all the eye needs, and any more turns the lid into a flap. */
|
|
893
|
+
.xeno-element[data-glyph='briefcase'] .xeno-part[data-part='0'],
|
|
894
|
+
.xeno-element[data-glyph='briefcase'] .xeno-part[data-part='1'] {
|
|
895
|
+
--part-anim: xeno-pt-briefcase-lid;
|
|
896
|
+
--part-dur: 720ms;
|
|
897
|
+
transform-box: view-box;
|
|
898
|
+
transform-origin: 3px 13px;
|
|
899
|
+
}
|
|
900
|
+
.xeno-element[data-glyph='briefcase'] .xeno-part[data-part='2'] {
|
|
901
|
+
--part-anim: xeno-pt-briefcase-base;
|
|
902
|
+
--part-dur: 720ms;
|
|
903
|
+
transform-box: view-box;
|
|
904
|
+
transform-origin: 12px 19.5px;
|
|
905
|
+
}
|
|
906
|
+
@keyframes xeno-pt-briefcase-lid {
|
|
907
|
+
0% { transform: translateY(0) rotate(0deg); }
|
|
908
|
+
38% { transform: translateY(-3.4px) rotate(-6deg); }
|
|
909
|
+
62% { transform: translateY(-3.4px) rotate(-6deg); }
|
|
910
|
+
100% { transform: translateY(0) rotate(0deg); }
|
|
911
|
+
}
|
|
912
|
+
/* The base only acknowledges it — a hair of squash as the lid comes back down. */
|
|
913
|
+
@keyframes xeno-pt-briefcase-base {
|
|
914
|
+
0% { transform: scaleY(1); }
|
|
915
|
+
62% { transform: scaleY(1); }
|
|
916
|
+
78% { transform: scaleY(0.9); }
|
|
917
|
+
100% { transform: scaleY(1); }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/* building — the windows come on, floor by floor, left to right. The walls hold: a building that rocked
|
|
921
|
+
would be an earthquake, and what this glyph means is that somebody is home.
|
|
922
|
+
|
|
923
|
+
Six parts rather than one, because the whole point is the stagger. `--part-dur` is the same on all six
|
|
924
|
+
and the delay lives in the keyframe percentages instead of in `animation-delay`, so the six finish
|
|
925
|
+
together and the sequence reads as one event with an order rather than six separate ones. */
|
|
926
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-win-1; --part-dur: 620ms; }
|
|
927
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='4'] { --part-anim: xeno-pt-win-2; --part-dur: 620ms; }
|
|
928
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='5'] { --part-anim: xeno-pt-win-3; --part-dur: 620ms; }
|
|
929
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='6'] { --part-anim: xeno-pt-win-4; --part-dur: 620ms; }
|
|
930
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='7'] { --part-anim: xeno-pt-win-5; --part-dur: 620ms; }
|
|
931
|
+
.xeno-element[data-glyph='building'] .xeno-part[data-part='8'] { --part-anim: xeno-pt-win-6; --part-dur: 620ms; }
|
|
932
|
+
@keyframes xeno-pt-win-1 { 0%,4% { opacity: 0.22; } 22%, 100% { opacity: 1; } }
|
|
933
|
+
@keyframes xeno-pt-win-2 { 0%,12% { opacity: 0.22; } 30%, 100% { opacity: 1; } }
|
|
934
|
+
@keyframes xeno-pt-win-3 { 0%,20% { opacity: 0.22; } 38%, 100% { opacity: 1; } }
|
|
935
|
+
@keyframes xeno-pt-win-4 { 0%,28% { opacity: 0.22; } 46%, 100% { opacity: 1; } }
|
|
936
|
+
@keyframes xeno-pt-win-5 { 0%,36% { opacity: 0.22; } 54%, 100% { opacity: 1; } }
|
|
937
|
+
@keyframes xeno-pt-win-6 { 0%,44% { opacity: 0.22; } 62%, 100% { opacity: 1; } }
|
|
938
|
+
|
|
939
|
+
/* next-dismiss — hover motion PER STATE, because the glyph means two different things and a control
|
|
940
|
+
should answer for the one it is showing.
|
|
941
|
+
|
|
942
|
+
Pointing at it while it is an arrow gets `arrow-right`'s own gesture; while it is a cross it gets
|
|
943
|
+
`x`'s pop. Borrowed rather than invented, so hovering this button feels the same as hovering the two
|
|
944
|
+
glyphs it stands in for.
|
|
945
|
+
|
|
946
|
+
It is a whole-icon animation on `transform`, and the morph between the states runs on `d`. Different
|
|
947
|
+
properties, so the two never fight: you can hover mid-morph and both keep going. */
|
|
948
|
+
.xeno-element[data-glyph='next-dismiss'][data-selection='off'] {
|
|
949
|
+
--xeno-icon-anim: xeno-ic-arrow-right;
|
|
950
|
+
--xeno-icon-dur: 500ms;
|
|
951
|
+
transform-origin: left center;
|
|
952
|
+
}
|
|
953
|
+
.xeno-element[data-glyph='next-dismiss'][data-selection='on'] {
|
|
954
|
+
--xeno-icon-anim: xeno-ic-nextdismiss-x;
|
|
955
|
+
--xeno-icon-dur: 420ms;
|
|
956
|
+
}
|
|
957
|
+
@keyframes xeno-ic-nextdismiss-x {
|
|
958
|
+
0% { transform: scale(1); }
|
|
959
|
+
46% { transform: scale(1.14); }
|
|
960
|
+
100% { transform: scale(1); }
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* ---- Triggers: play on icon-hover or any interactive host ------------------- */
|
|
964
|
+
|
|
965
|
+
/* There are TWO triggers, not one.
|
|
966
|
+
*
|
|
967
|
+
* Hover is the obvious one: point at a control and its glyph stirs. The second is SELECTION — a host
|
|
968
|
+
* carrying `data-selection="on"` plays the same animation once, at the moment it becomes selected.
|
|
969
|
+
*
|
|
970
|
+
* That second trigger exists because a real product kept hand-rolling it. A row of options where picking
|
|
971
|
+
* one makes its icon respond is a common, meaningful pattern: the motion is not decoration there, it is
|
|
972
|
+
* the confirmation that the choice landed. Without a selection trigger the only way to get it was to
|
|
973
|
+
* abandon the declaration and hand-write an animated SVG per option — which is exactly what the chat's
|
|
974
|
+
* share dialog had done, three times over, in a stroke weight that did not match the rest of the set.
|
|
975
|
+
*
|
|
976
|
+
* CSS gives this for free and it is worth saying why: a rule that only applies while the attribute is on
|
|
977
|
+
* starts its animation when the attribute flips, and stops existing when it flips back. No listener, no
|
|
978
|
+
* state, no replay logic. Flipping to `off` and back to `on` plays it again.
|
|
979
|
+
*
|
|
980
|
+
* Both trigger lists stay at specificity ZERO (`:where`), so any consumer rule still overrides them.
|
|
981
|
+
*
|
|
982
|
+
* There is also a SCOPE, `.xeno-icon-hosts`, and it is the third trigger. Inside an element carrying it,
|
|
983
|
+
* every button, link, menu item, tab and option is a host without saying so.
|
|
984
|
+
*
|
|
985
|
+
* That looks like it contradicts the closed list, and it is the closed list's escape hatch rather than a
|
|
986
|
+
* hole in it. The list exists so an icon does not stir on a stray hover — a table full of glyphs that all
|
|
987
|
+
* twitch as the pointer crosses them is noise. But a product whose every icon button IS a XENO glyph pays
|
|
988
|
+
* that tax as a per-button opt-in it has to remember, forever, on every button anyone adds. Measured on
|
|
989
|
+
* one: thirty-four controls carried a glyph and none of them was a host, and the way that surfaced was a
|
|
990
|
+
* user noticing two buttons out of six that sat still.
|
|
991
|
+
*
|
|
992
|
+
* The scope answers to SELECTION as well as hover, and it did not at first — which turned out to matter
|
|
993
|
+
* more than it sounds. A copy button that swaps its glyph for a check on click had the check draw
|
|
994
|
+
* itself perfectly, so long as the pointer stayed put; move the mouse as you click, as people do, and
|
|
995
|
+
* the check simply appeared, already finished. The draw only existed while a hover trigger matched.
|
|
996
|
+
* Selection is the trigger that does not need a pointer, and a product that has declared its scope
|
|
997
|
+
* should not then have to add `xeno-icon-hover` to one button to reach it.
|
|
998
|
+
*
|
|
999
|
+
* A scope is a decision made once, in the open, by the consumer — which is the difference between an
|
|
1000
|
+
* opt-in and a default.
|
|
1001
|
+
*/
|
|
1002
|
+
|
|
1003
|
+
/* whole-icon animations */
|
|
1004
|
+
:where(
|
|
1005
|
+
.xeno-element[data-glyph]:hover,
|
|
1006
|
+
.xeno-btn:hover .xeno-element[data-glyph],
|
|
1007
|
+
.xeno-chip:hover .xeno-element[data-glyph],
|
|
1008
|
+
.xeno-toggle:hover .xeno-element[data-glyph],
|
|
1009
|
+
.xeno-list-row:hover .xeno-element[data-glyph],
|
|
1010
|
+
.xeno-menu-item:hover .xeno-element[data-glyph],
|
|
1011
|
+
.xeno-sidebar-item:hover .xeno-element[data-glyph],
|
|
1012
|
+
.xeno-tab:hover .xeno-element[data-glyph],
|
|
1013
|
+
.xeno-segmented-option:hover .xeno-element[data-glyph],
|
|
1014
|
+
.xeno-field-trigger:hover .xeno-element[data-glyph],
|
|
1015
|
+
.xeno-icon-hover:hover .xeno-element[data-glyph],
|
|
1016
|
+
.xeno-icon-hosts :is(button, a, [role='menuitem'], [role='tab'], [role='option'], [role='radio'], label):hover .xeno-element[data-glyph],
|
|
1017
|
+
.xeno-icon-hover[data-selection='on'] .xeno-element[data-glyph],
|
|
1018
|
+
.xeno-icon-hosts :is(button, a, [role='menuitem'], [role='tab'], [role='option'], [role='radio'], label)[data-selection='on'] .xeno-element[data-glyph],
|
|
1019
|
+
.xeno-list-row[data-selection='on'] .xeno-element[data-glyph],
|
|
1020
|
+
.xeno-menu-item[data-selection='on'] .xeno-element[data-glyph],
|
|
1021
|
+
.xeno-sidebar-item[data-selection='on'] .xeno-element[data-glyph],
|
|
1022
|
+
.xeno-tab[data-selection='on'] .xeno-element[data-glyph],
|
|
1023
|
+
.xeno-segmented-option[data-selection='on'] .xeno-element[data-glyph]
|
|
1024
|
+
) { animation: var(--xeno-icon-anim) var(--xeno-icon-dur) var(--xeno-icon-ease); }
|
|
1025
|
+
|
|
1026
|
+
/* per-part animations */
|
|
1027
|
+
:where(
|
|
1028
|
+
.xeno-element[data-glyph]:hover,
|
|
1029
|
+
.xeno-btn:hover .xeno-element[data-glyph],
|
|
1030
|
+
.xeno-chip:hover .xeno-element[data-glyph],
|
|
1031
|
+
.xeno-toggle:hover .xeno-element[data-glyph],
|
|
1032
|
+
.xeno-list-row:hover .xeno-element[data-glyph],
|
|
1033
|
+
.xeno-menu-item:hover .xeno-element[data-glyph],
|
|
1034
|
+
.xeno-sidebar-item:hover .xeno-element[data-glyph],
|
|
1035
|
+
.xeno-tab:hover .xeno-element[data-glyph],
|
|
1036
|
+
.xeno-segmented-option:hover .xeno-element[data-glyph],
|
|
1037
|
+
.xeno-field-trigger:hover .xeno-element[data-glyph],
|
|
1038
|
+
.xeno-icon-hover:hover .xeno-element[data-glyph],
|
|
1039
|
+
.xeno-icon-hosts :is(button, a, [role='menuitem'], [role='tab'], [role='option'], [role='radio'], label):hover .xeno-element[data-glyph],
|
|
1040
|
+
.xeno-icon-hover[data-selection='on'] .xeno-element[data-glyph],
|
|
1041
|
+
.xeno-icon-hosts :is(button, a, [role='menuitem'], [role='tab'], [role='option'], [role='radio'], label)[data-selection='on'] .xeno-element[data-glyph],
|
|
1042
|
+
.xeno-list-row[data-selection='on'] .xeno-element[data-glyph],
|
|
1043
|
+
.xeno-menu-item[data-selection='on'] .xeno-element[data-glyph],
|
|
1044
|
+
.xeno-sidebar-item[data-selection='on'] .xeno-element[data-glyph],
|
|
1045
|
+
.xeno-tab[data-selection='on'] .xeno-element[data-glyph],
|
|
1046
|
+
.xeno-segmented-option[data-selection='on'] .xeno-element[data-glyph]
|
|
1047
|
+
) .xeno-part {
|
|
1048
|
+
animation: var(--part-anim) var(--part-dur) var(--xeno-icon-ease);
|
|
1049
|
+
/* A part that is scaled hard (globe's meridians) opts into a constant line weight for the DURATION of the
|
|
1050
|
+
hover only: `non-scaling-stroke` pins the stroke to screen pixels, which would otherwise leave the icon
|
|
1051
|
+
under-weighted whenever it is drawn larger than 24. At rest the stroke scales with `size`, as it must. */
|
|
1052
|
+
vector-effect: var(--part-vfx, none);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/* ── The transport set: play / pause / stop ──────────────────────────────────────────
|
|
1056
|
+
One control in three states, so the three animations are one idea told three ways: play LEANS the way
|
|
1057
|
+
it is going, pause takes the weight on both feet, stop lands. Same 460ms for all three, because they
|
|
1058
|
+
replace each other in place and a state that took longer would read as a slower machine. */
|
|
1059
|
+
|
|
1060
|
+
/* play — the wedge presses forward and settles. It is the only one of the three with a direction, so it
|
|
1061
|
+
is the only one that travels. */
|
|
1062
|
+
.xeno-element[data-glyph='play'] { --xeno-icon-anim: xeno-play-go; --xeno-icon-dur: 460ms; }
|
|
1063
|
+
@keyframes xeno-play-go {
|
|
1064
|
+
0% { transform: translateX(0); }
|
|
1065
|
+
34% { transform: translateX(1.4px); }
|
|
1066
|
+
100% { transform: translateX(0); }
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/* pause — the bars part a hair and come back. Offset by a beat so it reads as two objects rather than one
|
|
1070
|
+
shape scaling: a pause is two things standing still, and things that move in perfect lockstep look
|
|
1071
|
+
welded. */
|
|
1072
|
+
.xeno-element[data-glyph='pause'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-pause-a; --part-dur: 460ms; }
|
|
1073
|
+
.xeno-element[data-glyph='pause'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-pause-b; --part-dur: 460ms; }
|
|
1074
|
+
@keyframes xeno-pt-pause-a {
|
|
1075
|
+
0% { transform: translateX(0); }
|
|
1076
|
+
38% { transform: translateX(-2.4px); }
|
|
1077
|
+
100% { transform: translateX(0); }
|
|
1078
|
+
}
|
|
1079
|
+
@keyframes xeno-pt-pause-b {
|
|
1080
|
+
0% { transform: translateX(0); }
|
|
1081
|
+
48% { transform: translateX(2.4px); }
|
|
1082
|
+
100% { transform: translateX(0); }
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/* stop — one contraction, nothing else. A stop is the absence of travel, so anything that moved sideways
|
|
1086
|
+
here would argue with the glyph. */
|
|
1087
|
+
.xeno-element[data-glyph='stop'] { --xeno-icon-anim: xeno-stop-land; --xeno-icon-dur: 460ms; }
|
|
1088
|
+
@keyframes xeno-stop-land {
|
|
1089
|
+
0% { transform: scale(1); }
|
|
1090
|
+
38% { transform: scale(0.72); }
|
|
1091
|
+
70% { transform: scale(1.07); }
|
|
1092
|
+
100% { transform: scale(1); }
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/* ── The panel set: the chevron goes where the panel is about to go ──────────────────
|
|
1096
|
+
Four glyphs, one gesture, four directions. Only the chevron moves — the frame is the window and the
|
|
1097
|
+
rail is the panel's edge, and a window that shifted every time you pointed at it would be a bug. The
|
|
1098
|
+
travel is 1.6px: enough to read as intent at 24px, small enough that the chevron never leaves the
|
|
1099
|
+
frame it belongs to. */
|
|
1100
|
+
.xeno-element[data-glyph='panel-left'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-panel-e; --part-dur: 500ms; }
|
|
1101
|
+
.xeno-element[data-glyph='panel-right-close'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-panel-e; --part-dur: 500ms; }
|
|
1102
|
+
.xeno-element[data-glyph='panel-left-close'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-panel-w; --part-dur: 500ms; }
|
|
1103
|
+
.xeno-element[data-glyph='panel-right'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-panel-w; --part-dur: 500ms; }
|
|
1104
|
+
@keyframes xeno-pt-panel-e {
|
|
1105
|
+
0% { transform: translateX(0); }
|
|
1106
|
+
36% { transform: translateX(3.4px); }
|
|
1107
|
+
100% { transform: translateX(0); }
|
|
1108
|
+
}
|
|
1109
|
+
@keyframes xeno-pt-panel-w {
|
|
1110
|
+
0% { transform: translateX(0); }
|
|
1111
|
+
36% { transform: translateX(-3.4px); }
|
|
1112
|
+
100% { transform: translateX(0); }
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/* ── maximize / minimize — the same gesture with the sign flipped ────────────────────
|
|
1116
|
+
Each corner moves along its OWN diagonal, away from the centre for maximize and toward it for
|
|
1117
|
+
minimize. Four separate keyframes per glyph rather than a scale on the whole icon: scaling would take
|
|
1118
|
+
the stroke weight with it, and a corner bracket that thickens as it travels stops looking like the
|
|
1119
|
+
same bracket. */
|
|
1120
|
+
.xeno-element[data-glyph='maximize'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-max-tl; --part-dur: 520ms; }
|
|
1121
|
+
.xeno-element[data-glyph='maximize'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-max-tr; --part-dur: 520ms; }
|
|
1122
|
+
.xeno-element[data-glyph='maximize'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-max-br; --part-dur: 520ms; }
|
|
1123
|
+
.xeno-element[data-glyph='maximize'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-max-bl; --part-dur: 520ms; }
|
|
1124
|
+
@keyframes xeno-pt-max-tl { 0% { transform: translate(0, 0); } 38% { transform: translate(-2.9px, -2.9px); } 100% { transform: translate(0, 0); } }
|
|
1125
|
+
@keyframes xeno-pt-max-tr { 0% { transform: translate(0, 0); } 38% { transform: translate(2.9px, -2.9px); } 100% { transform: translate(0, 0); } }
|
|
1126
|
+
@keyframes xeno-pt-max-br { 0% { transform: translate(0, 0); } 38% { transform: translate(2.9px, 2.9px); } 100% { transform: translate(0, 0); } }
|
|
1127
|
+
@keyframes xeno-pt-max-bl { 0% { transform: translate(0, 0); } 38% { transform: translate(-2.9px, 2.9px); } 100% { transform: translate(0, 0); } }
|
|
1128
|
+
|
|
1129
|
+
.xeno-element[data-glyph='minimize'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-min-tl; --part-dur: 520ms; }
|
|
1130
|
+
.xeno-element[data-glyph='minimize'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-min-tr; --part-dur: 520ms; }
|
|
1131
|
+
.xeno-element[data-glyph='minimize'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-min-br; --part-dur: 520ms; }
|
|
1132
|
+
.xeno-element[data-glyph='minimize'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-min-bl; --part-dur: 520ms; }
|
|
1133
|
+
@keyframes xeno-pt-min-tl { 0% { transform: translate(0, 0); } 38% { transform: translate(1.3px, 1.3px); } 100% { transform: translate(0, 0); } }
|
|
1134
|
+
@keyframes xeno-pt-min-tr { 0% { transform: translate(0, 0); } 38% { transform: translate(-1.3px, 1.3px); } 100% { transform: translate(0, 0); } }
|
|
1135
|
+
@keyframes xeno-pt-min-br { 0% { transform: translate(0, 0); } 38% { transform: translate(-1.3px, -1.3px); } 100% { transform: translate(0, 0); } }
|
|
1136
|
+
@keyframes xeno-pt-min-bl { 0% { transform: translate(0, 0); } 38% { transform: translate(1.3px, -1.3px); } 100% { transform: translate(0, 0); } }
|
|
1137
|
+
|
|
1138
|
+
/* ── calendar — the posts tap down, the page takes it ────────────────────────────────
|
|
1139
|
+
The binder posts are what hold a calendar to the wall, so they are what moves; the header rule and the
|
|
1140
|
+
page stay put. It is a small motion on purpose — a calendar is usually a label for a date field, not a
|
|
1141
|
+
button, and a page that swung would pull the eye off the value beside it. */
|
|
1142
|
+
.xeno-element[data-glyph='calendar'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-cal-post-a; --part-dur: 540ms; }
|
|
1143
|
+
.xeno-element[data-glyph='calendar'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-cal-post-b; --part-dur: 540ms; }
|
|
1144
|
+
.xeno-element[data-glyph='calendar'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-cal-page; --part-dur: 540ms; transform-box: view-box; transform-origin: 12px 20.5px; }
|
|
1145
|
+
@keyframes xeno-pt-cal-post-a {
|
|
1146
|
+
0% { transform: translateY(0); }
|
|
1147
|
+
30% { transform: translateY(-3.1px); }
|
|
1148
|
+
100% { transform: translateY(0); }
|
|
1149
|
+
}
|
|
1150
|
+
@keyframes xeno-pt-cal-post-b {
|
|
1151
|
+
0% { transform: translateY(0); }
|
|
1152
|
+
42% { transform: translateY(-3.1px); }
|
|
1153
|
+
100% { transform: translateY(0); }
|
|
1154
|
+
}
|
|
1155
|
+
@keyframes xeno-pt-cal-page {
|
|
1156
|
+
0% { transform: scaleY(1); }
|
|
1157
|
+
46% { transform: scaleY(0.9); }
|
|
1158
|
+
100% { transform: scaleY(1); }
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/* ── The negation set: eye-off / mic-off / timer-off ─────────────────────────────────
|
|
1162
|
+
The slash DRAWS ITSELF, corner to corner, and the thing being denied gives way underneath. One mark,
|
|
1163
|
+
one gesture, across every glyph that negates something — and it is the drawing that carries it: a
|
|
1164
|
+
slash that simply appeared would be a state, while a slash that is drawn is an act.
|
|
1165
|
+
|
|
1166
|
+
`stroke-dasharray: 22.1` is the stroke's own length (4.2,4.2 → 19.8,19.8 = sqrt(2) x 15.6), so the
|
|
1167
|
+
dash offset runs it exactly once. Steady easing, as with `check`: the default ease-out front-loads
|
|
1168
|
+
the dash and the line looks half-finished for most of the animation. */
|
|
1169
|
+
.xeno-element[data-glyph='eye-off'],
|
|
1170
|
+
.xeno-element[data-glyph='mic-off'],
|
|
1171
|
+
.xeno-element[data-glyph='timer-off'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
1172
|
+
|
|
1173
|
+
.xeno-element[data-glyph='eye-off'] .xeno-part[data-part='2'],
|
|
1174
|
+
.xeno-element[data-glyph='mic-off'] .xeno-part[data-part='3'],
|
|
1175
|
+
.xeno-element[data-glyph='timer-off'] .xeno-part[data-part='2'] {
|
|
1176
|
+
--part-anim: xeno-pt-slash;
|
|
1177
|
+
--part-dur: 560ms;
|
|
1178
|
+
stroke-dasharray: 22.1;
|
|
1179
|
+
}
|
|
1180
|
+
@keyframes xeno-pt-slash {
|
|
1181
|
+
0% { stroke-dashoffset: 22.1; }
|
|
1182
|
+
70% { stroke-dashoffset: 0; }
|
|
1183
|
+
100% { stroke-dashoffset: 0; }
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/* What each one gives up as the slash arrives. Not the same part in each: the eye loses its pupil (it
|
|
1187
|
+
stops seeing), the mic's capsule shrinks back into its cradle (it stops listening), the clock's hands
|
|
1188
|
+
fall still (it stops counting). Same mark, three different things being denied — a shared animation on
|
|
1189
|
+
"whatever is part 1" would have said nothing about any of them. */
|
|
1190
|
+
.xeno-element[data-glyph='eye-off'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-eyeoff-pupil; --part-dur: 560ms; transform-box: view-box; transform-origin: 12px 12px; }
|
|
1191
|
+
@keyframes xeno-pt-eyeoff-pupil {
|
|
1192
|
+
0% { transform: scale(1); opacity: 1; }
|
|
1193
|
+
62% { transform: scale(0.45); opacity: 0.25; }
|
|
1194
|
+
100% { transform: scale(1); opacity: 1; }
|
|
1195
|
+
}
|
|
1196
|
+
.xeno-element[data-glyph='mic-off'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-micoff-capsule; --part-dur: 560ms; }
|
|
1197
|
+
@keyframes xeno-pt-micoff-capsule {
|
|
1198
|
+
0% { transform: translateY(0); }
|
|
1199
|
+
62% { transform: translateY(2.2px); }
|
|
1200
|
+
100% { transform: translateY(0); }
|
|
1201
|
+
}
|
|
1202
|
+
.xeno-element[data-glyph='timer-off'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-timeroff-hands; --part-dur: 560ms; transform-box: view-box; transform-origin: 12px 12px; }
|
|
1203
|
+
@keyframes xeno-pt-timeroff-hands {
|
|
1204
|
+
0% { transform: rotate(0deg); }
|
|
1205
|
+
26% { transform: rotate(26deg); }
|
|
1206
|
+
62% { transform: rotate(26deg); }
|
|
1207
|
+
100% { transform: rotate(0deg); }
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/* ── The marked set: the mark performs, the object holds ─────────────────────────────
|
|
1211
|
+
file-x, message-x — the cross draws itself one stroke then the other, the way `user-x` does. Reusing
|
|
1212
|
+
that timing on purpose: a cross means the same thing on all three, so it should arrive the same way.
|
|
1213
|
+
Stroke length is 7.35 (5.2 x sqrt(2)) for the file, 6.5 for the smaller one in the bubble. */
|
|
1214
|
+
.xeno-element[data-glyph='file-x'],
|
|
1215
|
+
.xeno-element[data-glyph='message-x'],
|
|
1216
|
+
.xeno-element[data-glyph='message-plus'],
|
|
1217
|
+
.xeno-element[data-glyph='file-out'],
|
|
1218
|
+
.xeno-element[data-glyph='folder-up'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
1219
|
+
|
|
1220
|
+
.xeno-element[data-glyph='file-x'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-mark-a; --part-dur: 600ms; stroke-dasharray: 7.35; }
|
|
1221
|
+
.xeno-element[data-glyph='file-x'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-mark-b; --part-dur: 600ms; stroke-dasharray: 7.35; }
|
|
1222
|
+
.xeno-element[data-glyph='message-x'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-mark-a; --part-dur: 600ms; stroke-dasharray: 6.5; }
|
|
1223
|
+
.xeno-element[data-glyph='message-x'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-mark-b; --part-dur: 600ms; stroke-dasharray: 6.5; }
|
|
1224
|
+
@keyframes xeno-pt-mark-a {
|
|
1225
|
+
0% { stroke-dashoffset: 8; }
|
|
1226
|
+
46% { stroke-dashoffset: 0; }
|
|
1227
|
+
100% { stroke-dashoffset: 0; }
|
|
1228
|
+
}
|
|
1229
|
+
@keyframes xeno-pt-mark-b {
|
|
1230
|
+
0% { stroke-dashoffset: 8; }
|
|
1231
|
+
30% { stroke-dashoffset: 8; }
|
|
1232
|
+
76% { stroke-dashoffset: 0; }
|
|
1233
|
+
100% { stroke-dashoffset: 0; }
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/* message-plus — the two strokes meet in the middle rather than being drawn end to end. A plus has no
|
|
1237
|
+
reading order; a cross does, which is why the two glyphs animate differently despite sharing a bubble. */
|
|
1238
|
+
.xeno-element[data-glyph='message-plus'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-plus-h; --part-dur: 600ms; transform-box: view-box; transform-origin: 12.4px 10.5px; }
|
|
1239
|
+
.xeno-element[data-glyph='message-plus'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-plus-v; --part-dur: 600ms; transform-box: view-box; transform-origin: 12.4px 10.5px; }
|
|
1240
|
+
@keyframes xeno-pt-plus-h {
|
|
1241
|
+
0% { transform: scaleX(0.2); }
|
|
1242
|
+
56% { transform: scaleX(1); }
|
|
1243
|
+
100% { transform: scaleX(1); }
|
|
1244
|
+
}
|
|
1245
|
+
@keyframes xeno-pt-plus-v {
|
|
1246
|
+
0% { transform: scaleY(0.2); }
|
|
1247
|
+
70% { transform: scaleY(1); }
|
|
1248
|
+
100% { transform: scaleY(1); }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/* file-out — the arrow LEAVES. Head and shaft travel together and come back: an export is one thing
|
|
1252
|
+
going one way, and a head that outran its own shaft would be two things. */
|
|
1253
|
+
.xeno-element[data-glyph='file-out'] .xeno-part[data-part='2'],
|
|
1254
|
+
.xeno-element[data-glyph='file-out'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-fileout; --part-dur: 620ms; }
|
|
1255
|
+
@keyframes xeno-pt-fileout {
|
|
1256
|
+
0% { transform: translateX(0); }
|
|
1257
|
+
40% { transform: translateX(2.2px); }
|
|
1258
|
+
100% { transform: translateX(0); }
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/* folder-up — the arrow rises and the folder takes it, the same beat `archive` uses for its lid. The
|
|
1262
|
+
two glyphs are neighbours in meaning (things going into a container), so they move alike. */
|
|
1263
|
+
.xeno-element[data-glyph='folder-up'] .xeno-part[data-part='1'],
|
|
1264
|
+
.xeno-element[data-glyph='folder-up'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-folderup-arrow; --part-dur: 620ms; }
|
|
1265
|
+
.xeno-element[data-glyph='folder-up'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-folderup-body; --part-dur: 620ms; transform-box: view-box; transform-origin: 10.4px 18.4px; }
|
|
1266
|
+
@keyframes xeno-pt-folderup-arrow {
|
|
1267
|
+
0% { transform: translateY(0); }
|
|
1268
|
+
34% { transform: translateY(-2.4px); }
|
|
1269
|
+
100% { transform: translateY(0); }
|
|
1270
|
+
}
|
|
1271
|
+
@keyframes xeno-pt-folderup-body {
|
|
1272
|
+
0% { transform: scaleY(1); }
|
|
1273
|
+
50% { transform: scaleY(0.97); }
|
|
1274
|
+
100% { transform: scaleY(1); }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/* ── The arrow family — one head, so one gesture ─────────────────────────────────────
|
|
1278
|
+
Every arrow in the set travels along its own shaft and comes back. Distance is 1.6px, the same as the
|
|
1279
|
+
panel chevrons: an arrow is usually a label on a button, and one that lunged would pull the eye off
|
|
1280
|
+
the word beside it. */
|
|
1281
|
+
.xeno-element[data-glyph='arrow-up-right'] { --xeno-icon-anim: xeno-arrow-ne; --xeno-icon-dur: 480ms; }
|
|
1282
|
+
@keyframes xeno-arrow-ne {
|
|
1283
|
+
0% { transform: translate(0, 0); }
|
|
1284
|
+
36% { transform: translate(1.2px, -1.2px); }
|
|
1285
|
+
100% { transform: translate(0, 0); }
|
|
1286
|
+
}
|
|
1287
|
+
/* The swap arrows pass EACH OTHER: same distance, opposite directions, and the lower one starts a beat
|
|
1288
|
+
late. Two arrows moving as one block would be a single object sliding, which is not what a swap is. */
|
|
1289
|
+
.xeno-element[data-glyph='arrow-left-right'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-swap-e; --part-dur: 520ms; }
|
|
1290
|
+
.xeno-element[data-glyph='arrow-left-right'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-swap-w; --part-dur: 520ms; }
|
|
1291
|
+
@keyframes xeno-pt-swap-e {
|
|
1292
|
+
0% { transform: translateX(0); }
|
|
1293
|
+
38% { transform: translateX(3.4px); }
|
|
1294
|
+
100% { transform: translateX(0); }
|
|
1295
|
+
}
|
|
1296
|
+
@keyframes xeno-pt-swap-w {
|
|
1297
|
+
0% { transform: translateX(0); }
|
|
1298
|
+
16% { transform: translateX(0); }
|
|
1299
|
+
50% { transform: translateX(-3.4px); }
|
|
1300
|
+
100% { transform: translateX(0); }
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/* trending-up — the line DRAWS itself and the head arrives at the end of it, which is the only order
|
|
1304
|
+
that tells the truth: a trend is a thing that happened over time, so the glyph should take time.
|
|
1305
|
+
Dash length 24 covers the polyline (7.8 + 4.8 + 10.7 ≈ 23.3) with a margin. */
|
|
1306
|
+
.xeno-element[data-glyph='trending-up'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
1307
|
+
.xeno-element[data-glyph='trending-up'] .xeno-part[data-part='0'] {
|
|
1308
|
+
--part-anim: xeno-pt-trend-line;
|
|
1309
|
+
--part-dur: 720ms;
|
|
1310
|
+
stroke-dasharray: 24;
|
|
1311
|
+
}
|
|
1312
|
+
.xeno-element[data-glyph='trending-up'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-trend-head; --part-dur: 720ms; }
|
|
1313
|
+
@keyframes xeno-pt-trend-line {
|
|
1314
|
+
0% { stroke-dashoffset: 24; }
|
|
1315
|
+
62% { stroke-dashoffset: 0; }
|
|
1316
|
+
100% { stroke-dashoffset: 0; }
|
|
1317
|
+
}
|
|
1318
|
+
@keyframes xeno-pt-trend-head {
|
|
1319
|
+
0% { opacity: 0; transform: translate(-1.6px, 1.6px); }
|
|
1320
|
+
56% { opacity: 0; transform: translate(-1.6px, 1.6px); }
|
|
1321
|
+
78% { opacity: 1; transform: translate(0, 0); }
|
|
1322
|
+
100% { opacity: 1; transform: translate(0, 0); }
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/* check-square — the tick draws itself inside a frame that holds still, the same way `check` does alone.
|
|
1326
|
+
The frame is the constant in this family (`stop`, `contrast`, `check-square`); if it moved, the three
|
|
1327
|
+
would stop reading as the same enclosure. Dash 12.6 is the tick's own length. */
|
|
1328
|
+
.xeno-element[data-glyph='check-square'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
1329
|
+
.xeno-element[data-glyph='check-square'] .xeno-part[data-part='1'] {
|
|
1330
|
+
--part-anim: xeno-pt-checksq;
|
|
1331
|
+
--part-dur: 540ms;
|
|
1332
|
+
stroke-dasharray: 12.6;
|
|
1333
|
+
}
|
|
1334
|
+
@keyframes xeno-pt-checksq {
|
|
1335
|
+
0% { stroke-dashoffset: 12.6; }
|
|
1336
|
+
72% { stroke-dashoffset: 0; }
|
|
1337
|
+
100% { stroke-dashoffset: 0; }
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/* save — the shutter presses into the shell and the shell takes it. A disk is a thing you push in, and
|
|
1341
|
+
that is the only motion the object has ever had. */
|
|
1342
|
+
.xeno-element[data-glyph='save'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-save-shutter; --part-dur: 520ms; }
|
|
1343
|
+
.xeno-element[data-glyph='save'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-save-shell; --part-dur: 520ms; transform-box: view-box; transform-origin: 12px 19.6px; }
|
|
1344
|
+
@keyframes xeno-pt-save-shutter {
|
|
1345
|
+
0% { transform: translateY(0); }
|
|
1346
|
+
36% { transform: translateY(3.1px); }
|
|
1347
|
+
100% { transform: translateY(0); }
|
|
1348
|
+
}
|
|
1349
|
+
@keyframes xeno-pt-save-shell {
|
|
1350
|
+
0% { transform: scaleY(1); }
|
|
1351
|
+
46% { transform: scaleY(0.9); }
|
|
1352
|
+
100% { transform: scaleY(1); }
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/* quote — the second mark follows the first, the way a quotation is read. Nothing else moves, because
|
|
1356
|
+
the marks ARE the glyph. */
|
|
1357
|
+
.xeno-element[data-glyph='quote'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-quote-a; --part-dur: 620ms; }
|
|
1358
|
+
.xeno-element[data-glyph='quote'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-quote-b; --part-dur: 620ms; }
|
|
1359
|
+
@keyframes xeno-pt-quote-a {
|
|
1360
|
+
0% { transform: translateY(0); }
|
|
1361
|
+
32% { transform: translateY(-3.1px); }
|
|
1362
|
+
100% { transform: translateY(0); }
|
|
1363
|
+
}
|
|
1364
|
+
@keyframes xeno-pt-quote-b {
|
|
1365
|
+
0% { transform: translateY(0); }
|
|
1366
|
+
14% { transform: translateY(0); }
|
|
1367
|
+
46% { transform: translateY(-3.1px); }
|
|
1368
|
+
100% { transform: translateY(0); }
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/* monitor / app-window — the screen wakes. A screen is the one part of either glyph that has a state,
|
|
1372
|
+
so it is the part that changes; the stand and the title bar are furniture and hold still. */
|
|
1373
|
+
.xeno-element[data-glyph='monitor'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-screen-wake; --part-dur: 560ms; transform-box: view-box; transform-origin: 12px 10.2px; }
|
|
1374
|
+
.xeno-element[data-glyph='app-window'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-screen-wake; --part-dur: 560ms; transform-box: view-box; transform-origin: 12px 12px; }
|
|
1375
|
+
/* The screen blinks rather than only shrinking. Scale alone is what this was, and a screen cannot be
|
|
1376
|
+
scaled far without ceasing to be the same monitor — 14% is about the limit, and 14% of a ten-pixel
|
|
1377
|
+
screen is a pixel and a half of edge. Dimming has no such ceiling: it is the one thing a display can
|
|
1378
|
+
do to its whole surface at once, and it is also what waking actually looks like. */
|
|
1379
|
+
@keyframes xeno-pt-screen-wake {
|
|
1380
|
+
0% { transform: scale(1); opacity: 1; }
|
|
1381
|
+
30% { transform: scale(0.88); opacity: 0.25; }
|
|
1382
|
+
62% { transform: scale(1.04); opacity: 1; }
|
|
1383
|
+
100% { transform: scale(1); opacity: 1; }
|
|
1384
|
+
}
|
|
1385
|
+
/* The window's two marks blink in sequence behind it — the smallest thing in the glyph, doing the least. */
|
|
1386
|
+
.xeno-element[data-glyph='app-window'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-win-dot-a; --part-dur: 560ms; }
|
|
1387
|
+
.xeno-element[data-glyph='app-window'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-win-dot-b; --part-dur: 560ms; }
|
|
1388
|
+
@keyframes xeno-pt-win-dot-a { 0% { opacity: 1; } 30% { opacity: 0.3; } 60% { opacity: 1; } 100% { opacity: 1; } }
|
|
1389
|
+
@keyframes xeno-pt-win-dot-b { 0% { opacity: 1; } 16% { opacity: 1; } 46% { opacity: 0.3; } 76% { opacity: 1; } 100% { opacity: 1; } }
|
|
1390
|
+
|
|
1391
|
+
/* zap — one hard flash and a snap back, no travel. The glyph means "instant", and anything that eased
|
|
1392
|
+
in would be arguing with the word. Steps easing on the way in for the same reason. */
|
|
1393
|
+
.xeno-element[data-glyph='zap'] { --xeno-icon-anim: xeno-zap-strike; --xeno-icon-dur: 440ms; --xeno-icon-ease: cubic-bezier(0.2, 0, 0, 1); }
|
|
1394
|
+
@keyframes xeno-zap-strike {
|
|
1395
|
+
0% { transform: scale(1); opacity: 1; }
|
|
1396
|
+
18% { transform: scale(1.32); opacity: 0.3; }
|
|
1397
|
+
34% { transform: scale(0.9); opacity: 1; }
|
|
1398
|
+
100% { transform: scale(1); opacity: 1; }
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/* wrap-text — the hook is drawn, then its head arrives, so the line is seen running out of room before
|
|
1402
|
+
it is seen turning back. Dash 12 covers the hook's run and its arc. */
|
|
1403
|
+
.xeno-element[data-glyph='wrap-text'] { --xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1); }
|
|
1404
|
+
.xeno-element[data-glyph='wrap-text'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-wrap-hook; --part-dur: 660ms; stroke-dasharray: 16; }
|
|
1405
|
+
.xeno-element[data-glyph='wrap-text'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-wrap-head; --part-dur: 660ms; }
|
|
1406
|
+
@keyframes xeno-pt-wrap-hook {
|
|
1407
|
+
0% { stroke-dashoffset: 16; }
|
|
1408
|
+
60% { stroke-dashoffset: 0; }
|
|
1409
|
+
100% { stroke-dashoffset: 0; }
|
|
1410
|
+
}
|
|
1411
|
+
@keyframes xeno-pt-wrap-head {
|
|
1412
|
+
0% { opacity: 0; transform: translateX(1.6px); }
|
|
1413
|
+
54% { opacity: 0; transform: translateX(1.6px); }
|
|
1414
|
+
76% { opacity: 1; transform: translateX(0); }
|
|
1415
|
+
100% { opacity: 1; transform: translateX(0); }
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/* thumbs-up / thumbs-down — the WHOLE hand goes the way it points and comes back. Up for a like, down
|
|
1419
|
+
for a dislike: the gesture and the glyph say the same thing, which a rotation from the wrist did not
|
|
1420
|
+
— that read as a hand waving, and a wave means neither approval nor its opposite.
|
|
1421
|
+
|
|
1422
|
+
The whole icon rather than the palm alone, because a hand that leaves its own cuff behind is a hand
|
|
1423
|
+
coming off at the wrist. Mirrored exactly, as the geometry is: the pair sits under every answer, and a
|
|
1424
|
+
like that travelled further than a dislike would read as the product having an opinion about which one
|
|
1425
|
+
you should press. */
|
|
1426
|
+
.xeno-element[data-glyph='thumbs-up'] { --xeno-icon-anim: xeno-ic-thumb-up; --xeno-icon-dur: 520ms; }
|
|
1427
|
+
.xeno-element[data-glyph='thumbs-down'] { --xeno-icon-anim: xeno-ic-thumb-down; --xeno-icon-dur: 520ms; }
|
|
1428
|
+
@keyframes xeno-ic-thumb-up {
|
|
1429
|
+
0% { transform: translateY(0); }
|
|
1430
|
+
34% { transform: translateY(-2.4px); }
|
|
1431
|
+
100% { transform: translateY(0); }
|
|
1432
|
+
}
|
|
1433
|
+
@keyframes xeno-ic-thumb-down {
|
|
1434
|
+
0% { transform: translateY(0); }
|
|
1435
|
+
34% { transform: translateY(2.4px); }
|
|
1436
|
+
100% { transform: translateY(0); }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/* library — the leaning book straightens and falls back. It is the only part that can move without the
|
|
1440
|
+
shelf becoming a different shelf, and it is also the part that carries the meaning: three upright bars
|
|
1441
|
+
are a chart, and the one out of line is what says these are objects standing up. */
|
|
1442
|
+
.xeno-element[data-glyph='library'] .xeno-part[data-part='2'] {
|
|
1443
|
+
--part-anim: xeno-pt-library-lean;
|
|
1444
|
+
--part-dur: 620ms;
|
|
1445
|
+
transform-box: view-box;
|
|
1446
|
+
transform-origin: 15px 19.2px;
|
|
1447
|
+
}
|
|
1448
|
+
/* Seven degrees on a book five units tall is a third of a pixel at the 13px this is drawn at in the
|
|
1449
|
+
agent rail. It straightens the whole way now, and past vertical, so the lean visibly resolves and
|
|
1450
|
+
comes back — a book being pushed upright rather than nudged. */
|
|
1451
|
+
@keyframes xeno-pt-library-lean {
|
|
1452
|
+
0% { transform: rotate(0deg); }
|
|
1453
|
+
36% { transform: rotate(-18deg); }
|
|
1454
|
+
68% { transform: rotate(4deg); }
|
|
1455
|
+
100% { transform: rotate(0deg); }
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/* store — the awning takes a breath and the front holds. An awning is cloth on a frame; it is the only
|
|
1459
|
+
soft thing in the glyph, so it is the only thing that gives. */
|
|
1460
|
+
.xeno-element[data-glyph='store'] .xeno-part[data-part='0'] {
|
|
1461
|
+
--part-anim: xeno-pt-store-awning;
|
|
1462
|
+
--part-dur: 560ms;
|
|
1463
|
+
transform-box: view-box;
|
|
1464
|
+
transform-origin: 12px 9.4px;
|
|
1465
|
+
}
|
|
1466
|
+
/* The weakest motion in the whole set as measured: 0.36px, because 12% of a four-unit awning at 13px is
|
|
1467
|
+
a quarter of a pixel. It billows properly now — half again its depth, and drawn back under itself on
|
|
1468
|
+
the way out, which is what cloth on a frame does when the wind drops. */
|
|
1469
|
+
@keyframes xeno-pt-store-awning {
|
|
1470
|
+
0% { transform: scaleY(1); }
|
|
1471
|
+
36% { transform: scaleY(1.55); }
|
|
1472
|
+
70% { transform: scaleY(0.88); }
|
|
1473
|
+
100% { transform: scaleY(1); }
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/* navigation — the pointer travels along the way it points, which is the only direction it is allowed to
|
|
1477
|
+
move: a location marker that drifted sideways would be pointing at one thing and going to another. */
|
|
1478
|
+
.xeno-element[data-glyph='navigation'] { --xeno-icon-anim: xeno-nav-go; --xeno-icon-dur: 520ms; }
|
|
1479
|
+
@keyframes xeno-nav-go {
|
|
1480
|
+
0% { transform: translateY(0); }
|
|
1481
|
+
34% { transform: translateY(-1.8px); }
|
|
1482
|
+
100% { transform: translateY(0); }
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/* lightbulb — it comes ON. The glass brightens and swells a fraction while the collar and contact hold
|
|
1486
|
+
still, because the socket does not light up. This is the one glyph in the set whose animation IS its
|
|
1487
|
+
meaning rather than a comment on it: an idea is the moment something starts. */
|
|
1488
|
+
.xeno-element[data-glyph='lightbulb'] .xeno-part[data-part='0'] {
|
|
1489
|
+
--part-anim: xeno-pt-bulb-on;
|
|
1490
|
+
--part-dur: 680ms;
|
|
1491
|
+
transform-box: view-box;
|
|
1492
|
+
transform-origin: 12px 15px;
|
|
1493
|
+
}
|
|
1494
|
+
@keyframes xeno-pt-bulb-on {
|
|
1495
|
+
0% { transform: scale(1); opacity: 0.45; }
|
|
1496
|
+
30% { transform: scale(1.28); opacity: 1; }
|
|
1497
|
+
62% { transform: scale(1.05); opacity: 1; }
|
|
1498
|
+
100% { transform: scale(1); opacity: 0.45; }
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/* scan-eye — the frame closes in and the eye looks. Corners inward, not outward: this is `maximize`'s
|
|
1502
|
+
geometry with the opposite intent, and the direction is the entire difference between framing
|
|
1503
|
+
something to enlarge it and framing something to examine it. The pupil tracks a beat behind the
|
|
1504
|
+
corners, because the frame arrives first and the looking follows. */
|
|
1505
|
+
.xeno-element[data-glyph='scan-eye'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-scan-tl; --part-dur: 720ms; }
|
|
1506
|
+
.xeno-element[data-glyph='scan-eye'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-scan-tr; --part-dur: 720ms; }
|
|
1507
|
+
.xeno-element[data-glyph='scan-eye'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-scan-br; --part-dur: 720ms; }
|
|
1508
|
+
.xeno-element[data-glyph='scan-eye'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-scan-bl; --part-dur: 720ms; }
|
|
1509
|
+
.xeno-element[data-glyph='scan-eye'] .xeno-part[data-part='5'] { --part-anim: xeno-pt-scan-pupil; --part-dur: 720ms; }
|
|
1510
|
+
@keyframes xeno-pt-scan-tl { 0% { transform: translate(0,0); } 34% { transform: translate(2.4px, 2.4px); } 100% { transform: translate(0,0); } }
|
|
1511
|
+
@keyframes xeno-pt-scan-tr { 0% { transform: translate(0,0); } 34% { transform: translate(-2.4px, 2.4px); } 100% { transform: translate(0,0); } }
|
|
1512
|
+
@keyframes xeno-pt-scan-br { 0% { transform: translate(0,0); } 34% { transform: translate(-2.4px, -2.4px); } 100% { transform: translate(0,0); } }
|
|
1513
|
+
@keyframes xeno-pt-scan-bl { 0% { transform: translate(0,0); } 34% { transform: translate(2.4px, -2.4px); } 100% { transform: translate(0,0); } }
|
|
1514
|
+
@keyframes xeno-pt-scan-pupil {
|
|
1515
|
+
0% { transform: translateX(0); }
|
|
1516
|
+
30% { transform: translateX(0); }
|
|
1517
|
+
54% { transform: translateX(2.1px); }
|
|
1518
|
+
78% { transform: translateX(-2.1px); }
|
|
1519
|
+
100% { transform: translateX(0); }
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/* shapes — the square that sits in front slides clear and comes back, so the overlap is briefly opened.
|
|
1523
|
+
Only the one in front moves: what the glyph is about is that there are two objects, and the way to say
|
|
1524
|
+
that is to let one of them pass over the other. */
|
|
1525
|
+
.xeno-element[data-glyph='shapes'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-shapes-square; --part-dur: 600ms; }
|
|
1526
|
+
@keyframes xeno-pt-shapes-square {
|
|
1527
|
+
0% { transform: translate(0, 0); }
|
|
1528
|
+
38% { transform: translate(3.1px, 3.1px); }
|
|
1529
|
+
100% { transform: translate(0, 0); }
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/* waves — the three roll, each a beat behind the one above it. The glyph is drawn with them in phase so
|
|
1533
|
+
the motion has somewhere to go: staggered at rest, it would already look like it was moving and the
|
|
1534
|
+
hover would have nothing left to add. Half a wavelength each way, so the crest lands where the trough
|
|
1535
|
+
was and nothing appears to slide off the edge. */
|
|
1536
|
+
.xeno-element[data-glyph='waves'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-wave-a; --part-dur: 900ms; }
|
|
1537
|
+
.xeno-element[data-glyph='waves'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-wave-b; --part-dur: 900ms; }
|
|
1538
|
+
.xeno-element[data-glyph='waves'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-wave-c; --part-dur: 900ms; }
|
|
1539
|
+
@keyframes xeno-pt-wave-a { 0% { transform: translateX(0); } 50% { transform: translateX(2.4px); } 100% { transform: translateX(0); } }
|
|
1540
|
+
@keyframes xeno-pt-wave-b { 0% { transform: translateX(0); } 14% { transform: translateX(0); } 60% { transform: translateX(2.4px); } 100% { transform: translateX(0); } }
|
|
1541
|
+
@keyframes xeno-pt-wave-c { 0% { transform: translateX(0); } 28% { transform: translateX(0); } 70% { transform: translateX(2.4px); } 100% { transform: translateX(0); } }
|
|
1542
|
+
|
|
1543
|
+
/* feather — the quill dips from its point, the way a pen is put to paper. The pivot is the tip, because
|
|
1544
|
+
that is the part that stays put when you write. */
|
|
1545
|
+
/* Percentages, for the reason spelled out on `brain`: this is a whole-icon rule, so px would be CSS
|
|
1546
|
+
pixels off the root element's own box and the tip would only be the tip at size 24. 4.2/24, 19.8/24. */
|
|
1547
|
+
.xeno-element[data-glyph='feather'] {
|
|
1548
|
+
--xeno-icon-anim: xeno-feather-dip;
|
|
1549
|
+
--xeno-icon-dur: 620ms;
|
|
1550
|
+
transform-origin: 17.5% 82.5%;
|
|
1551
|
+
}
|
|
1552
|
+
@keyframes xeno-feather-dip {
|
|
1553
|
+
0% { transform: rotate(0deg); }
|
|
1554
|
+
36% { transform: rotate(-7deg); }
|
|
1555
|
+
100% { transform: rotate(0deg); }
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/* hand — the fingers ripple, thumb to little finger. Four bars rather than one outline is what buys
|
|
1559
|
+
this: a scalloped silhouette can only wave as a whole, and a hand that waves as a whole is a paddle. */
|
|
1560
|
+
.xeno-element[data-glyph='hand'] .xeno-part[data-part='0'] { --part-anim: xeno-pt-finger-a; --part-dur: 720ms; }
|
|
1561
|
+
.xeno-element[data-glyph='hand'] .xeno-part[data-part='1'] { --part-anim: xeno-pt-finger-b; --part-dur: 720ms; }
|
|
1562
|
+
.xeno-element[data-glyph='hand'] .xeno-part[data-part='2'] { --part-anim: xeno-pt-finger-c; --part-dur: 720ms; }
|
|
1563
|
+
.xeno-element[data-glyph='hand'] .xeno-part[data-part='3'] { --part-anim: xeno-pt-finger-d; --part-dur: 720ms; }
|
|
1564
|
+
@keyframes xeno-pt-finger-a { 0% { transform: translateY(0); } 26% { transform: translateY(-3.1px); } 60% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
1565
|
+
@keyframes xeno-pt-finger-b { 0% { transform: translateY(0); } 12% { transform: translateY(0); } 38% { transform: translateY(-3.1px); } 72% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
1566
|
+
@keyframes xeno-pt-finger-c { 0% { transform: translateY(0); } 24% { transform: translateY(0); } 50% { transform: translateY(-3.1px); } 84% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
1567
|
+
@keyframes xeno-pt-finger-d { 0% { transform: translateY(0); } 36% { transform: translateY(0); } 62% { transform: translateY(-3.1px); } 96% { transform: translateY(0); } 100% { transform: translateY(0); } }
|
|
1568
|
+
|
|
1569
|
+
/* file-clock — the hands sweep a full turn and the sheet holds. One turn, not a nudge: this glyph means
|
|
1570
|
+
history, and the only honest way to say "time passed" is to let some. */
|
|
1571
|
+
.xeno-element[data-glyph='file-clock'] .xeno-part[data-part='3'] {
|
|
1572
|
+
--part-anim: xeno-pt-fileclock;
|
|
1573
|
+
--part-dur: 900ms;
|
|
1574
|
+
--xeno-icon-ease: cubic-bezier(0.65, 0, 0.35, 1);
|
|
1575
|
+
transform-box: view-box;
|
|
1576
|
+
transform-origin: 11.7px 15.3px;
|
|
1577
|
+
}
|
|
1578
|
+
@keyframes xeno-pt-fileclock {
|
|
1579
|
+
0% { transform: rotate(0deg); }
|
|
1580
|
+
100% { transform: rotate(360deg); }
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/* ---------------------------------------------------------------------------
|
|
1584
|
+
A NOTE ON AMPLITUDE, which the seven glyphs below were all calibrated wrong for
|
|
1585
|
+
the first time round.
|
|
1586
|
+
|
|
1587
|
+
Every keyframe here is written in the 24-unit viewBox, and it is natural to judge
|
|
1588
|
+
it in the preview, where icons are drawn at 96 or 48. The chat draws them at 13
|
|
1589
|
+
to 16. At 14px a unit is 0.58 CSS pixels, so a motion that looks like a confident
|
|
1590
|
+
three units on the design grid arrives as 1.7 pixels — and a rotation of a
|
|
1591
|
+
near-symmetric shape arrives as nothing at all, because there is no edge whose
|
|
1592
|
+
travel the eye can follow.
|
|
1593
|
+
|
|
1594
|
+
Measured on the running chat with the host's :hover forced and the part rects
|
|
1595
|
+
sampled through the animation: brain-circuit moved 0.30px, smile 0.43, bot 0.49,
|
|
1596
|
+
target 0.58. All seven read as broken, and none of them was — the triggers fired
|
|
1597
|
+
every time. The rule the set already followed without writing it down is that the
|
|
1598
|
+
motions which work at small sizes are the LARGE ones: refresh spins a full turn
|
|
1599
|
+
(8.1px), chevron travels (4.7px), plus scales from zero (3.7px).
|
|
1600
|
+
|
|
1601
|
+
So: at these sizes, scale from zero and travel. Rotate only what has a long arm.
|
|
1602
|
+
--------------------------------------------------------------------------- */
|
|
1603
|
+
|
|
1604
|
+
/* brain — it tilts, the way a head does when someone is working something out. There is nothing else in
|
|
1605
|
+
this glyph that CAN move: two strokes, one of them the outline and the other the line that divides it,
|
|
1606
|
+
and a sulcus that slid would be a crack rather than a fold. So the motion is the whole object, and the
|
|
1607
|
+
gesture it borrows is the one people actually make when they think.
|
|
1608
|
+
|
|
1609
|
+
The pivot is the base of the brain rather than its centre — a head tilts on a neck, and rotating about
|
|
1610
|
+
the middle reads as a wobble.
|
|
1611
|
+
|
|
1612
|
+
It tips fourteen degrees and comes back through five, rather than the six it started life with. A blob
|
|
1613
|
+
is the hardest thing in this set to rotate legibly — it has no corner to watch — so it needs both the
|
|
1614
|
+
larger angle and the overshoot on the way back, which is the part the eye actually catches. */
|
|
1615
|
+
/* The pivot is a PERCENTAGE, and that is the whole of a bug this shipped with.
|
|
1616
|
+
*
|
|
1617
|
+
* A WHOLE-icon rule targets the root `<svg>`, which is a replaced element with a CSS box — so a
|
|
1618
|
+
* `transform-origin` written in px is CSS pixels measured from that box's corner, not viewBox units.
|
|
1619
|
+
* `12px 19.4px` is only the base of the brain when the icon happens to be drawn at 24. The chat draws
|
|
1620
|
+
* it at 14, where that pivot sits five pixels BELOW the glyph and two from its right edge, and a
|
|
1621
|
+
* fourteen-degree rotation about a point outside the icon is not a tilt — it is the drawing being
|
|
1622
|
+
* thrown sideways. Reported as "it doesn't look like a brain any more", which is what it looked like.
|
|
1623
|
+
*
|
|
1624
|
+
* `transform-box: view-box` looks like the fix and is a no-op here: on the root element the view box
|
|
1625
|
+
* and the fill box are the same rectangle, so the lengths stay CSS pixels either way. It only converts
|
|
1626
|
+
* to user units on a CHILD, which is why the per-part rules in this file (file-clock, library, the
|
|
1627
|
+
* nodes below) can and must use it. A whole-icon rule has to speak in percentages: 12/24 and 19.4/24.
|
|
1628
|
+
*
|
|
1629
|
+
* `feather` had the same defect and got away with it — seven degrees moves the drawing about a pixel,
|
|
1630
|
+
* which reads as slightly odd easing rather than as a bug. */
|
|
1631
|
+
.xeno-element[data-glyph='brain'] {
|
|
1632
|
+
--xeno-icon-anim: xeno-ic-brain-tilt;
|
|
1633
|
+
--xeno-icon-dur: 720ms;
|
|
1634
|
+
transform-origin: 50% 81%;
|
|
1635
|
+
}
|
|
1636
|
+
@keyframes xeno-ic-brain-tilt {
|
|
1637
|
+
0% { transform: rotate(0deg); }
|
|
1638
|
+
36% { transform: rotate(-14deg); }
|
|
1639
|
+
68% { transform: rotate(5deg); }
|
|
1640
|
+
100% { transform: rotate(0deg); }
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/* brain-circuit — the signal leaves the tissue and arrives at a node, twice, upper then lower. The order
|
|
1644
|
+
is the point; both at once would be a pulse, and a pulse is a heartbeat.
|
|
1645
|
+
|
|
1646
|
+
The trace DRAWS, from the sulcus outward, and the node then arrives from nothing. That is the second
|
|
1647
|
+
version: the first swelled the nodes 32% in place, which on a 2.8-unit square at 16px is four tenths
|
|
1648
|
+
of a pixel — the animation ran, and no one could have seen it. Growing from zero moves the same square
|
|
1649
|
+
by its whole width, and the trace's far end travels its whole length. Same idea, an order of magnitude
|
|
1650
|
+
more of it.
|
|
1651
|
+
|
|
1652
|
+
`scaleX` from the sulcus end for the trace, so it extends rather than growing from its middle — a
|
|
1653
|
+
signal has a direction. */
|
|
1654
|
+
.xeno-element[data-glyph='brain-circuit'] .xeno-part[data-part='2'] {
|
|
1655
|
+
--part-anim: xeno-pt-trace-a;
|
|
1656
|
+
--part-dur: 780ms;
|
|
1657
|
+
transform-box: view-box;
|
|
1658
|
+
transform-origin: 12px 9.3px;
|
|
1659
|
+
}
|
|
1660
|
+
.xeno-element[data-glyph='brain-circuit'] .xeno-part[data-part='3'] {
|
|
1661
|
+
--part-anim: xeno-pt-trace-b;
|
|
1662
|
+
--part-dur: 780ms;
|
|
1663
|
+
transform-box: view-box;
|
|
1664
|
+
transform-origin: 12px 15.3px;
|
|
1665
|
+
}
|
|
1666
|
+
.xeno-element[data-glyph='brain-circuit'] .xeno-part[data-part='4'] {
|
|
1667
|
+
--part-anim: xeno-pt-node-fire-a;
|
|
1668
|
+
--part-dur: 780ms;
|
|
1669
|
+
transform-box: view-box;
|
|
1670
|
+
transform-origin: 17px 9.3px;
|
|
1671
|
+
}
|
|
1672
|
+
.xeno-element[data-glyph='brain-circuit'] .xeno-part[data-part='5'] {
|
|
1673
|
+
--part-anim: xeno-pt-node-fire-b;
|
|
1674
|
+
--part-dur: 780ms;
|
|
1675
|
+
transform-box: view-box;
|
|
1676
|
+
transform-origin: 17px 15.3px;
|
|
1677
|
+
}
|
|
1678
|
+
@keyframes xeno-pt-trace-a {
|
|
1679
|
+
0% { transform: scaleX(0); }
|
|
1680
|
+
30% { transform: scaleX(1); }
|
|
1681
|
+
100% { transform: scaleX(1); }
|
|
1682
|
+
}
|
|
1683
|
+
@keyframes xeno-pt-trace-b {
|
|
1684
|
+
0% { transform: scaleX(0); }
|
|
1685
|
+
26% { transform: scaleX(0); }
|
|
1686
|
+
56% { transform: scaleX(1); }
|
|
1687
|
+
100% { transform: scaleX(1); }
|
|
1688
|
+
}
|
|
1689
|
+
@keyframes xeno-pt-node-fire-a {
|
|
1690
|
+
0% { transform: scale(0); }
|
|
1691
|
+
30% { transform: scale(0); }
|
|
1692
|
+
48% { transform: scale(1.25); }
|
|
1693
|
+
62% { transform: scale(1); }
|
|
1694
|
+
100% { transform: scale(1); }
|
|
1695
|
+
}
|
|
1696
|
+
@keyframes xeno-pt-node-fire-b {
|
|
1697
|
+
0% { transform: scale(0); }
|
|
1698
|
+
56% { transform: scale(0); }
|
|
1699
|
+
74% { transform: scale(1.25); }
|
|
1700
|
+
88% { transform: scale(1); }
|
|
1701
|
+
100% { transform: scale(1); }
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/* bot — the antenna bobs and it blinks. The eyes are two-unit vertical STROKES, which is what makes the
|
|
1705
|
+
blink possible at all: a segment can close to nothing along one axis and open again. A dot can only
|
|
1706
|
+
shrink towards its own centre, which is a fade, and a robot whose eyes fade is powering down.
|
|
1707
|
+
|
|
1708
|
+
Both eyes on the same frame. Staggered, it would be a wink, and a wink from a piece of software is a
|
|
1709
|
+
different personality than this product has.
|
|
1710
|
+
|
|
1711
|
+
The antenna was added because the blink alone is not enough here. A blink is a real event — the eye is
|
|
1712
|
+
there and then it is not — but at 13px, which is what the empty state's tabs draw, the eye is barely a
|
|
1713
|
+
pixel tall to begin with, so its disappearing is a flicker rather than a gesture. The antenna EXTENDS,
|
|
1714
|
+
which gives the eye something to follow while the blink happens underneath it.
|
|
1715
|
+
|
|
1716
|
+
The stem stretches by exactly what the node travels, anchored at the head. Moving the node alone was
|
|
1717
|
+
the obvious version and it is wrong: a node that leaves its stem behind is a part coming off. */
|
|
1718
|
+
.xeno-element[data-glyph='bot'] .xeno-part[data-part='0'] {
|
|
1719
|
+
--part-anim: xeno-pt-bot-antenna;
|
|
1720
|
+
--part-dur: 700ms;
|
|
1721
|
+
transform-box: view-box;
|
|
1722
|
+
transform-origin: 12px 4px;
|
|
1723
|
+
}
|
|
1724
|
+
.xeno-element[data-glyph='bot'] .xeno-part[data-part='1'] {
|
|
1725
|
+
--part-anim: xeno-pt-bot-stem;
|
|
1726
|
+
--part-dur: 700ms;
|
|
1727
|
+
transform-box: view-box;
|
|
1728
|
+
transform-origin: 12px 8px;
|
|
1729
|
+
}
|
|
1730
|
+
.xeno-element[data-glyph='bot'] .xeno-part[data-part='3'] {
|
|
1731
|
+
--part-anim: xeno-pt-bot-blink;
|
|
1732
|
+
--part-dur: 700ms;
|
|
1733
|
+
transform-box: view-box;
|
|
1734
|
+
transform-origin: 8.8px 13.6px;
|
|
1735
|
+
}
|
|
1736
|
+
.xeno-element[data-glyph='bot'] .xeno-part[data-part='4'] {
|
|
1737
|
+
--part-anim: xeno-pt-bot-blink;
|
|
1738
|
+
--part-dur: 700ms;
|
|
1739
|
+
transform-box: view-box;
|
|
1740
|
+
transform-origin: 15.2px 13.6px;
|
|
1741
|
+
}
|
|
1742
|
+
@keyframes xeno-pt-bot-antenna {
|
|
1743
|
+
0% { transform: translateY(0); }
|
|
1744
|
+
30% { transform: translateY(-2.6px); }
|
|
1745
|
+
62% { transform: translateY(0.4px); }
|
|
1746
|
+
100% { transform: translateY(0); }
|
|
1747
|
+
}
|
|
1748
|
+
/* 2.6 units of travel on a 2.6-unit stem is exactly double its length, and 0.4 back is 1.15. */
|
|
1749
|
+
@keyframes xeno-pt-bot-stem {
|
|
1750
|
+
0% { transform: scaleY(1); }
|
|
1751
|
+
30% { transform: scaleY(2); }
|
|
1752
|
+
62% { transform: scaleY(0.85); }
|
|
1753
|
+
100% { transform: scaleY(1); }
|
|
1754
|
+
}
|
|
1755
|
+
@keyframes xeno-pt-bot-blink {
|
|
1756
|
+
0% { transform: scaleY(1); }
|
|
1757
|
+
40% { transform: scaleY(1); }
|
|
1758
|
+
54% { transform: scaleY(0.04); }
|
|
1759
|
+
70% { transform: scaleY(1); }
|
|
1760
|
+
100% { transform: scaleY(1); }
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/* sparkles — the two twinkle out of phase, the small one following the large. That order is the reason
|
|
1764
|
+
there are two: a single star can only pulse, and a pulse has no direction, while a second one arriving
|
|
1765
|
+
late reads as light catching on something.
|
|
1766
|
+
|
|
1767
|
+
A quarter turn goes with the scale. A four-pointed star rotated 90° is the same star, so the turn
|
|
1768
|
+
costs nothing at the ends of the animation and is visible for every frame in between — which is
|
|
1769
|
+
exactly the property a twinkle wants. */
|
|
1770
|
+
.xeno-element[data-glyph='sparkles'] .xeno-part[data-part='0'] {
|
|
1771
|
+
--part-anim: xeno-pt-twinkle-a;
|
|
1772
|
+
--part-dur: 820ms;
|
|
1773
|
+
transform-box: view-box;
|
|
1774
|
+
transform-origin: 9.6px 9.6px;
|
|
1775
|
+
}
|
|
1776
|
+
.xeno-element[data-glyph='sparkles'] .xeno-part[data-part='1'] {
|
|
1777
|
+
--part-anim: xeno-pt-twinkle-b;
|
|
1778
|
+
--part-dur: 820ms;
|
|
1779
|
+
transform-box: view-box;
|
|
1780
|
+
transform-origin: 17.8px 17.4px;
|
|
1781
|
+
}
|
|
1782
|
+
@keyframes xeno-pt-twinkle-a {
|
|
1783
|
+
0% { transform: rotate(0deg) scale(1); }
|
|
1784
|
+
46% { transform: rotate(45deg) scale(0.78); }
|
|
1785
|
+
100% { transform: rotate(90deg) scale(1); }
|
|
1786
|
+
}
|
|
1787
|
+
@keyframes xeno-pt-twinkle-b {
|
|
1788
|
+
0% { transform: rotate(0deg) scale(1); }
|
|
1789
|
+
24% { transform: rotate(0deg) scale(1); }
|
|
1790
|
+
62% { transform: rotate(45deg) scale(0.72); }
|
|
1791
|
+
100% { transform: rotate(90deg) scale(1); }
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/* wand-sparkles — the wand flicks and the star answers. Cause and effect, in that order and with a gap
|
|
1795
|
+
between them: the shaft and its joint swing together about the handle, and only after they have turned
|
|
1796
|
+
does the star swell. Simultaneous, the two would look like one object stretching.
|
|
1797
|
+
|
|
1798
|
+
The pivot is the butt of the handle, because that is where a hand would be, and the whole point of the
|
|
1799
|
+
joint being drawn at the lower third is to say so. */
|
|
1800
|
+
.xeno-element[data-glyph='wand-sparkles'] .xeno-part[data-part='0'],
|
|
1801
|
+
.xeno-element[data-glyph='wand-sparkles'] .xeno-part[data-part='1'] {
|
|
1802
|
+
--part-anim: xeno-pt-wand-flick;
|
|
1803
|
+
--part-dur: 720ms;
|
|
1804
|
+
transform-box: view-box;
|
|
1805
|
+
transform-origin: 3.8px 20.2px;
|
|
1806
|
+
}
|
|
1807
|
+
.xeno-element[data-glyph='wand-sparkles'] .xeno-part[data-part='2'] {
|
|
1808
|
+
--part-anim: xeno-pt-wand-star;
|
|
1809
|
+
--part-dur: 720ms;
|
|
1810
|
+
transform-box: view-box;
|
|
1811
|
+
transform-origin: 18.2px 6.4px;
|
|
1812
|
+
}
|
|
1813
|
+
/* Eighteen degrees, not nine. The shaft has a long arm — its tip is fifteen units from the handle — so
|
|
1814
|
+
this is the one glyph in the group where rotation was already the right instrument and simply needed
|
|
1815
|
+
twice as much of it. The star arrives from nothing rather than swelling a quarter, for the same reason
|
|
1816
|
+
the circuit's nodes do. */
|
|
1817
|
+
@keyframes xeno-pt-wand-flick {
|
|
1818
|
+
0% { transform: rotate(0deg); }
|
|
1819
|
+
30% { transform: rotate(-18deg); }
|
|
1820
|
+
64% { transform: rotate(4deg); }
|
|
1821
|
+
100% { transform: rotate(0deg); }
|
|
1822
|
+
}
|
|
1823
|
+
@keyframes xeno-pt-wand-star {
|
|
1824
|
+
0% { transform: rotate(0deg) scale(0.2); }
|
|
1825
|
+
32% { transform: rotate(0deg) scale(0.2); }
|
|
1826
|
+
58% { transform: rotate(45deg) scale(1.35); }
|
|
1827
|
+
78% { transform: rotate(70deg) scale(0.94); }
|
|
1828
|
+
100% { transform: rotate(90deg) scale(1); }
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/* target — the rings close on the centre and open again. Inner first, then outer, so the tightening
|
|
1832
|
+
reads as one movement travelling INWARD rather than the whole glyph shrinking. The centre mark holds
|
|
1833
|
+
still, because it is the thing being converged on; a bullseye that moved would be the wrong idea
|
|
1834
|
+
entirely. */
|
|
1835
|
+
.xeno-element[data-glyph='target'] .xeno-part[data-part='0'] {
|
|
1836
|
+
--part-anim: xeno-pt-target-outer;
|
|
1837
|
+
--part-dur: 680ms;
|
|
1838
|
+
transform-box: view-box;
|
|
1839
|
+
transform-origin: 12px 12px;
|
|
1840
|
+
}
|
|
1841
|
+
.xeno-element[data-glyph='target'] .xeno-part[data-part='1'] {
|
|
1842
|
+
--part-anim: xeno-pt-target-inner;
|
|
1843
|
+
--part-dur: 680ms;
|
|
1844
|
+
transform-box: view-box;
|
|
1845
|
+
transform-origin: 12px 12px;
|
|
1846
|
+
}
|
|
1847
|
+
/* The rings close much harder than they first did — 0.9 and 0.76 were a tenth and a quarter of rings
|
|
1848
|
+
that are ten and five pixels wide on screen, so the whole convergence happened inside a pixel. The
|
|
1849
|
+
inner ring now collapses most of the way to the centre mark, which is the reading anyway: it is not
|
|
1850
|
+
that the rings shrink a little, it is that they close ON something. */
|
|
1851
|
+
@keyframes xeno-pt-target-outer {
|
|
1852
|
+
0% { transform: scale(1); }
|
|
1853
|
+
40% { transform: scale(0.8); }
|
|
1854
|
+
72% { transform: scale(1.04); }
|
|
1855
|
+
100% { transform: scale(1); }
|
|
1856
|
+
}
|
|
1857
|
+
@keyframes xeno-pt-target-inner {
|
|
1858
|
+
0% { transform: scale(1); }
|
|
1859
|
+
32% { transform: scale(0.42); }
|
|
1860
|
+
70% { transform: scale(1.08); }
|
|
1861
|
+
100% { transform: scale(1); }
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/* smile — the eyes narrow and the mouth widens, together. A smile is not a mouth shape; it is what the
|
|
1865
|
+
eyes do at the same time, which is why the eyes are strokes here and not dots. A face where only the
|
|
1866
|
+
mouth moves is a face being polite about something.
|
|
1867
|
+
|
|
1868
|
+
The mouth widens rather than curving deeper, because the curve is geometry and widening is a
|
|
1869
|
+
transform — and a smile that got WIDER is the same reading as one that got deeper, for free. */
|
|
1870
|
+
.xeno-element[data-glyph='smile'] .xeno-part[data-part='1'] {
|
|
1871
|
+
--part-anim: xeno-pt-smile-eye;
|
|
1872
|
+
--part-dur: 620ms;
|
|
1873
|
+
transform-box: view-box;
|
|
1874
|
+
transform-origin: 9.2px 10.9px;
|
|
1875
|
+
}
|
|
1876
|
+
.xeno-element[data-glyph='smile'] .xeno-part[data-part='2'] {
|
|
1877
|
+
--part-anim: xeno-pt-smile-eye;
|
|
1878
|
+
--part-dur: 620ms;
|
|
1879
|
+
transform-box: view-box;
|
|
1880
|
+
transform-origin: 14.8px 10.9px;
|
|
1881
|
+
}
|
|
1882
|
+
.xeno-element[data-glyph='smile'] .xeno-part[data-part='3'] {
|
|
1883
|
+
--part-anim: xeno-pt-smile-mouth;
|
|
1884
|
+
--part-dur: 620ms;
|
|
1885
|
+
transform-box: view-box;
|
|
1886
|
+
transform-origin: 12px 15.4px;
|
|
1887
|
+
}
|
|
1888
|
+
/* The eyes close ALL the way rather than halfway, and the mouth takes a third more width instead of a
|
|
1889
|
+
seventh. Halfway on a 1.8-unit stroke is nine tenths of a unit — half a pixel where this glyph is
|
|
1890
|
+
actually drawn — so the first version was a smile no one could see happen. Squeezed shut, the eye is
|
|
1891
|
+
simply gone for a moment, which is an event at any size. */
|
|
1892
|
+
@keyframes xeno-pt-smile-eye {
|
|
1893
|
+
0% { transform: scaleY(1); }
|
|
1894
|
+
38% { transform: scaleY(0.05) translateY(0.5px); }
|
|
1895
|
+
72% { transform: scaleY(1.1); }
|
|
1896
|
+
100% { transform: scaleY(1); }
|
|
1897
|
+
}
|
|
1898
|
+
@keyframes xeno-pt-smile-mouth {
|
|
1899
|
+
0% { transform: scale(1, 1); }
|
|
1900
|
+
38% { transform: scale(1.34, 1.45); }
|
|
1901
|
+
72% { transform: scale(0.96, 0.94); }
|
|
1902
|
+
100% { transform: scale(1, 1); }
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/* ---- Accessibility: honor reduced-motion ----------------------------------- */
|
|
1906
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1907
|
+
.xeno-element[data-glyph],
|
|
1908
|
+
.xeno-element[data-glyph] .xeno-part { animation: none !important; }
|
|
1909
|
+
}
|