neba 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +27 -16
  2. package/dist/components/avatar/Avatar.d.ts +100 -0
  3. package/dist/components/avatar/Avatar.js +1 -0
  4. package/dist/components/avatar/index.d.ts +2 -0
  5. package/dist/components/avatar/index.js +1 -0
  6. package/dist/components/blockquote/Blockquote.d.ts +65 -0
  7. package/dist/components/blockquote/Blockquote.js +1 -0
  8. package/dist/components/blockquote/index.d.ts +2 -0
  9. package/dist/components/blockquote/index.js +1 -0
  10. package/dist/components/breadcrumb/Breadcrumb.d.ts +92 -0
  11. package/dist/components/breadcrumb/Breadcrumb.js +1 -0
  12. package/dist/components/breadcrumb/index.d.ts +2 -0
  13. package/dist/components/breadcrumb/index.js +1 -0
  14. package/dist/components/carousel/Carousel.d.ts +74 -0
  15. package/dist/components/carousel/Carousel.js +1 -0
  16. package/dist/components/carousel/index.d.ts +2 -0
  17. package/dist/components/carousel/index.js +1 -0
  18. package/dist/components/chat-bubble/ChatBubble.d.ts +114 -0
  19. package/dist/components/chat-bubble/ChatBubble.js +1 -0
  20. package/dist/components/chat-bubble/index.d.ts +2 -0
  21. package/dist/components/chat-bubble/index.js +1 -0
  22. package/dist/components/checkbox/Checkbox.js +1 -1
  23. package/dist/components/combobox/Combobox.js +1 -1
  24. package/dist/components/container/Container.d.ts +55 -0
  25. package/dist/components/container/Container.js +1 -0
  26. package/dist/components/container/index.d.ts +2 -0
  27. package/dist/components/container/index.js +1 -0
  28. package/dist/components/date-picker/DatePicker.d.ts +75 -0
  29. package/dist/components/date-picker/DatePicker.js +1 -0
  30. package/dist/components/date-picker/index.d.ts +2 -0
  31. package/dist/components/date-picker/index.js +1 -0
  32. package/dist/components/date-range-picker/DateRangePicker.d.ts +81 -0
  33. package/dist/components/date-range-picker/DateRangePicker.js +1 -0
  34. package/dist/components/date-range-picker/index.d.ts +2 -0
  35. package/dist/components/date-range-picker/index.js +1 -0
  36. package/dist/components/date-time-picker/DateTimePicker.d.ts +68 -0
  37. package/dist/components/date-time-picker/DateTimePicker.js +1 -0
  38. package/dist/components/date-time-picker/index.d.ts +2 -0
  39. package/dist/components/date-time-picker/index.js +1 -0
  40. package/dist/components/dialog/Dialog.js +1 -1
  41. package/dist/components/divider/Divider.d.ts +19 -0
  42. package/dist/components/divider/Divider.js +1 -1
  43. package/dist/components/grid/Grid.d.ts +50 -0
  44. package/dist/components/grid/Grid.js +1 -0
  45. package/dist/components/grid/index.d.ts +2 -0
  46. package/dist/components/grid/index.js +1 -0
  47. package/dist/components/grid-container/GridContainer.d.ts +79 -0
  48. package/dist/components/grid-container/GridContainer.js +1 -0
  49. package/dist/components/grid-container/index.d.ts +2 -0
  50. package/dist/components/grid-container/index.js +1 -0
  51. package/dist/components/highlight/Highlight.d.ts +84 -0
  52. package/dist/components/highlight/Highlight.js +1 -0
  53. package/dist/components/highlight/index.d.ts +2 -0
  54. package/dist/components/highlight/index.js +1 -0
  55. package/dist/components/icon/Icon.d.ts +58 -0
  56. package/dist/components/icon/Icon.js +1 -0
  57. package/dist/components/icon/index.d.ts +2 -0
  58. package/dist/components/icon/index.js +1 -0
  59. package/dist/components/icon-button/IconButton.d.ts +39 -0
  60. package/dist/components/icon-button/IconButton.js +1 -0
  61. package/dist/components/icon-button/index.d.ts +2 -0
  62. package/dist/components/icon-button/index.js +1 -0
  63. package/dist/components/otp-field/OtpField.d.ts +85 -0
  64. package/dist/components/otp-field/OtpField.js +1 -0
  65. package/dist/components/otp-field/index.d.ts +2 -0
  66. package/dist/components/otp-field/index.js +1 -0
  67. package/dist/components/pagination/Pagination.js +1 -1
  68. package/dist/components/panes/Panes.d.ts +78 -0
  69. package/dist/components/panes/Panes.js +1 -0
  70. package/dist/components/panes/index.d.ts +2 -0
  71. package/dist/components/panes/index.js +1 -0
  72. package/dist/components/pill/Pill.d.ts +80 -0
  73. package/dist/components/pill/Pill.js +1 -0
  74. package/dist/components/pill/index.d.ts +2 -0
  75. package/dist/components/pill/index.js +1 -0
  76. package/dist/components/radio-group/RadioGroup.js +1 -1
  77. package/dist/components/segmented-button/SegmentedButton.d.ts +77 -0
  78. package/dist/components/segmented-button/SegmentedButton.js +1 -0
  79. package/dist/components/segmented-button/index.d.ts +2 -0
  80. package/dist/components/segmented-button/index.js +1 -0
  81. package/dist/components/select/Select.js +1 -1
  82. package/dist/components/shortcut/Shortcut.d.ts +60 -0
  83. package/dist/components/shortcut/Shortcut.js +1 -0
  84. package/dist/components/shortcut/index.d.ts +2 -0
  85. package/dist/components/shortcut/index.js +1 -0
  86. package/dist/components/spoiler/Spoiler.d.ts +95 -0
  87. package/dist/components/spoiler/Spoiler.js +1 -0
  88. package/dist/components/spoiler/index.d.ts +2 -0
  89. package/dist/components/spoiler/index.js +1 -0
  90. package/dist/components/statistic/Statistic.d.ts +91 -0
  91. package/dist/components/statistic/Statistic.js +1 -0
  92. package/dist/components/statistic/index.d.ts +2 -0
  93. package/dist/components/statistic/index.js +1 -0
  94. package/dist/components/switch/Switch.js +1 -1
  95. package/dist/components/text-link/TextLink.d.ts +82 -0
  96. package/dist/components/text-link/TextLink.js +1 -0
  97. package/dist/components/text-link/index.d.ts +2 -0
  98. package/dist/components/text-link/index.js +1 -0
  99. package/dist/components/time-picker/TimePicker.d.ts +76 -0
  100. package/dist/components/time-picker/TimePicker.js +1 -0
  101. package/dist/components/time-picker/index.d.ts +2 -0
  102. package/dist/components/time-picker/index.js +1 -0
  103. package/dist/components/timeline/Timeline.d.ts +98 -0
  104. package/dist/components/timeline/Timeline.js +1 -0
  105. package/dist/components/timeline/index.d.ts +2 -0
  106. package/dist/components/timeline/index.js +1 -0
  107. package/dist/components/toolbar/Toolbar.d.ts +67 -0
  108. package/dist/components/toolbar/Toolbar.js +1 -0
  109. package/dist/components/toolbar/index.d.ts +2 -0
  110. package/dist/components/toolbar/index.js +1 -0
  111. package/dist/components/tree-view/TreeView.d.ts +115 -0
  112. package/dist/components/tree-view/TreeView.js +1 -0
  113. package/dist/components/tree-view/index.d.ts +2 -0
  114. package/dist/components/tree-view/index.js +1 -0
  115. package/dist/index.d.ts +26 -0
  116. package/dist/index.js +1 -1
  117. package/dist/internal/calendar.d.ts +149 -0
  118. package/dist/internal/calendar.js +1 -0
  119. package/dist/internal/date.d.ts +170 -0
  120. package/dist/internal/date.js +1 -0
  121. package/dist/internal/grid.d.ts +69 -0
  122. package/dist/internal/grid.js +1 -0
  123. package/dist/internal/i18n.d.ts +78 -0
  124. package/dist/internal/i18n.js +1 -0
  125. package/dist/internal/icons.d.ts +54 -0
  126. package/dist/internal/icons.js +1 -1
  127. package/dist/internal/picker.d.ts +111 -0
  128. package/dist/internal/picker.js +1 -0
  129. package/dist/internal/sizer.d.ts +33 -0
  130. package/dist/internal/sizer.js +1 -0
  131. package/dist/internal/styles.d.ts +33 -0
  132. package/dist/internal/styles.js +1 -1
  133. package/dist/styles.css +2 -758
  134. package/dist/tailwind.css +1017 -0
  135. package/dist/types.d.ts +65 -0
  136. package/package.json +25 -12
@@ -0,0 +1,1017 @@
1
+ /**
2
+ * Neba design tokens.
3
+ *
4
+ * This file ships as `neba/tailwind.css`, for a project that runs Tailwind
5
+ * itself and wants the components' utilities generated in the same pass as its
6
+ * own:
7
+ *
8
+ * @import 'tailwindcss';
9
+ * @import 'neba/tailwind.css';
10
+ *
11
+ * Two lines, and no `@source` of their own — the `@source` below does that job.
12
+ *
13
+ * A project with no Tailwind imports `neba/styles.css` instead, which is this
14
+ * file plus a reset plus the compiled utilities, built by
15
+ * `scripts/build-styles.mjs` from `src/standalone.css`. The tokens below are
16
+ * the same in both.
17
+ *
18
+ * Components never hardcode a color; they map a `color` prop onto the `--n-*`
19
+ * local slots and let the cascade resolve the rest.
20
+ *
21
+ * Dark mode responds to `prefers-color-scheme`, and can be forced either way
22
+ * with `.dark` / `[data-theme='dark'|'light']` on any ancestor.
23
+ *
24
+ * The surface model is a sheet of acrylic, not a moulded plastic key: a
25
+ * slightly translucent fill, a blurred backdrop, and a hairline edge catching
26
+ * the light. Depth is not part of the surface — it is opt-in, via `elevation`.
27
+ */
28
+
29
+ /*
30
+ * The components' classes are Tailwind utilities, so Tailwind has to see the
31
+ * files that spell them out — otherwise every component renders unstyled.
32
+ *
33
+ * `@source` resolves relative to the file it is written in, not to whatever
34
+ * imported it. In a consumer's install that is `node_modules/neba/dist/`, next
35
+ * to the compiled components; in this repository it is `src/`, next to the
36
+ * `.tsx`. One line covers both, and an explicitly registered source is scanned
37
+ * even inside `node_modules`, which automatic detection skips.
38
+ *
39
+ * This is why consumers do not need an `@source '../node_modules/neba'` of
40
+ * their own — a path that would depend on where their CSS file happens to sit.
41
+ *
42
+ * The same line is what the standalone build scans: `src/standalone.css` pulls
43
+ * this file in from `src/`, so `.` means the components' `.tsx` there. One
44
+ * declaration, three contexts.
45
+ */
46
+ @source '.';
47
+
48
+ /* ---------------------------------------------------------------------------
49
+ * Light theme (default)
50
+ * ------------------------------------------------------------------------- */
51
+
52
+ :root {
53
+ /* Neutral surfaces and content */
54
+ --neba-surface: oklch(100% 0 0);
55
+ --neba-fg: oklch(24% 0.015 262);
56
+ --neba-muted-fg: oklch(56% 0.014 262);
57
+ --neba-border: oklch(91% 0.008 262);
58
+
59
+ /* Inactive controls: muted neutral rather than a faded copy of the color.
60
+ A greyed-out surface reads as "unavailable"; 50% opacity just reads blurry. */
61
+ --neba-disabled-bg: oklch(94.5% 0.005 262);
62
+ --neba-disabled-fg: oklch(73% 0.012 262);
63
+ --neba-disabled-border: oklch(90% 0.008 262);
64
+
65
+ /* Acrylic -------------------------------------------------------------- */
66
+
67
+ /* How opaque a filled surface is, and how hard the backdrop is blurred.
68
+ The blur radius is what decides whether anything behind is *legible*: at
69
+ 16px a grid line behind the button smears into flat colour and the surface
70
+ reads opaque no matter how low the alpha goes. Held at 9px so the backdrop
71
+ is present but never resolves into something you could read.
72
+ 88% is the floor for 4.5:1 white-on-fill over a white page; fill lightness
73
+ is picked against this number, not against opaque. */
74
+ --neba-fill-alpha: 88%;
75
+ --neba-blur: blur(9px) saturate(1.5);
76
+
77
+ /* Frosted, not polished. A single tile of greyscale fractal noise laid over
78
+ the fill in `overlay` is what separates sandblasted acrylic from a pane of
79
+ glass — and it is the one thing a flat fill can never fake. Kept at 12% in
80
+ the tile and blended, so it reads as tooth rather than as dirt.
81
+ One 120px tile, decoded once, reused by every surface on the page. */
82
+ --neba-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.12'/%3E%3C/svg%3E");
83
+
84
+ /* The hairline that reads as the cut edge of an acrylic sheet, plus the
85
+ brighter top edge where the light lands. There is deliberately no dark
86
+ bottom bevel — that is what made the surface read as moulded plastic. */
87
+ --neba-plate-edge: rgb(255 255 255 / 0.18);
88
+ --neba-plate-top: rgb(255 255 255 / 0.26);
89
+ --neba-plate-solid: inset 0 1px 0 var(--neba-plate-top), inset 0 0 0 1px var(--neba-plate-edge);
90
+ --neba-plate-glass: inset 0 1px 0 var(--neba-plate-top);
91
+ /* Off-vertical on purpose: light falling across a sheet at an angle, rather
92
+ than the symmetric top-down gradient every framework ships. */
93
+ --neba-sheen: linear-gradient(148deg, rgb(255 255 255 / 0.13), rgb(255 255 255 / 0) 52%);
94
+
95
+ /* The acrylic with no dye in it — what a colored panel is mixed *into*.
96
+
97
+ At 42% the sheet was letting through more of the page than of itself, so on
98
+ any backdrop that is not pure white it settled into grey. Raising it to 66%
99
+ is the one place alpha is the right lever rather than the blur: the blur
100
+ decides whether the backdrop is *legible*, and 9px still hides it. This
101
+ decides whether the sheet reads as white, and it has to. */
102
+ --neba-glass-bg: rgb(255 255 255 / 0.66);
103
+
104
+ /* The container surface, in the three strengths a panel-shaped component
105
+ needs — Box, Card and TextField read these directly.
106
+
107
+ Undyed on purpose, and this is the one place the color family deliberately
108
+ stops at the edge. A container holds other people's content, which arrives
109
+ with its own colors; tinting the sheet under it puts every one of them on a
110
+ background they were not chosen against. So the family shows up in the
111
+ hairline, the focus ring and the caret, and the sheet stays white.
112
+
113
+ Controls are the opposite case and keep their tinted `--neba-{color}-panel`:
114
+ a Button's surface *is* the thing being colored.
115
+
116
+ The ladder is opacity, not lightness — the sheet catches more light as it
117
+ is engaged, rather than turning grey. */
118
+ --neba-panel: rgb(255 255 255 / 0.66);
119
+ --neba-panel-hover: rgb(255 255 255 / 0.82);
120
+ --neba-panel-press: rgb(255 255 255 / 0.92);
121
+
122
+ /* Pointer spotlight and release afterglow, over a filled surface. Tinted
123
+ variants use their own accent instead — see the derived block. The flash is
124
+ only a shade brighter than the spotlight it replaces: the press is carried
125
+ by the fill going dark, and this is the highlight riding on top of it, not
126
+ the effect itself. */
127
+ --neba-glow-on-fill: rgb(255 255 255 / 0.16);
128
+ --neba-flash-on-fill: rgb(255 255 255 / 0.22);
129
+
130
+ /* The scrim behind a modal surface. Neutral rather than tinted, and dim
131
+ rather than opaque: what a dialog needs is for the page to stop being
132
+ reachable, not for it to stop existing. */
133
+ --neba-scrim: rgb(16 24 40 / 0.34);
134
+
135
+ /* Elevation ------------------------------------------------------------
136
+ Neutral, never tinted with the control's own color: a colored glow is the
137
+ loudest thing a small control can do, and the job here is to place a
138
+ surface in space, not to advertise it. Level 4 is only reachable by
139
+ hovering a level-3 surface. */
140
+ --neba-shadow-ambient: rgb(16 24 40 / 0.13);
141
+ --neba-shadow-0: 0 0 #0000;
142
+ --neba-shadow-1: 0 1px 2px -1px var(--neba-shadow-ambient);
143
+ --neba-shadow-2:
144
+ 0 2px 4px -2px var(--neba-shadow-ambient), 0 5px 12px -7px var(--neba-shadow-ambient);
145
+ --neba-shadow-3:
146
+ 0 4px 8px -3px var(--neba-shadow-ambient), 0 12px 24px -11px var(--neba-shadow-ambient);
147
+ --neba-shadow-4:
148
+ 0 6px 12px -4px var(--neba-shadow-ambient), 0 20px 40px -15px var(--neba-shadow-ambient);
149
+
150
+ /* Corner radius: ~45% of the control height at every step. Held just short of
151
+ the 50% that would make it a pill — the flat run along the top and bottom
152
+ edge is what still reads as a sheet with corners cut off it. */
153
+ --neba-radius-xs: 0.625rem;
154
+ --neba-radius-sm: 0.75rem;
155
+ --neba-radius-md: 0.875rem;
156
+ --neba-radius-lg: 1.125rem;
157
+ --neba-radius-xl: 1.375rem;
158
+
159
+ /* Motion. Controls answer in color and depth, never by moving: a transform on
160
+ a control drags its label with it, and text that shifts under the cursor is
161
+ the thing that reads as cheap. */
162
+ --neba-ease: cubic-bezier(0.32, 0.72, 0, 1);
163
+ --neba-duration: 160ms;
164
+ /* Fills settle more slowly than edges and shadows. Pressing overrides this to
165
+ 0ms, so the colour lands on the frame of the click and then takes this long
166
+ to drain back out — the button itself holds the press, not just the bloom
167
+ under the cursor. */
168
+ --neba-duration-fill: 340ms;
169
+
170
+ /* Color families -------------------------------------------------------
171
+ Chroma sits just under the sRGB gamut boundary for each family's lightness
172
+ — around 90% of the maximum, close enough to be vivid and far enough that
173
+ the browser never has to clip. The old values used about two thirds of what
174
+ was available, which is what made the palette read muddy: the fills were
175
+ not too dark, they were too grey.
176
+
177
+ Lightness is not the free variable it looks like. `on-solid` is white, the
178
+ fill is only 88% opaque, and the pair has to clear 4.5:1 over a white page
179
+ — which pins every family with white text to the high 40s / low 50s. A
180
+ brighter *fill* means a dark ink, which is why `warning` is the one family
181
+ that already goes there. Verified per step, including hover and active.
182
+
183
+ `success` moved 152 → 148 and `info` 218 → 223. Both are hue regions where
184
+ sRGB is starved at mid lightness, and a few degrees toward the gamut's wide
185
+ side buys chroma that no amount of tuning at the original hue could. */
186
+
187
+ --neba-primary-solid: oklch(50% 0.198 262);
188
+ --neba-primary-solid-hover: oklch(45.5% 0.196 262);
189
+ --neba-primary-solid-active: oklch(38% 0.172 262);
190
+ --neba-primary-on-solid: oklch(99% 0.004 262);
191
+ --neba-primary-accent: oklch(55% 0.222 262);
192
+
193
+ --neba-secondary-solid: oklch(48% 0.038 262);
194
+ --neba-secondary-solid-hover: oklch(43.5% 0.036 262);
195
+ --neba-secondary-solid-active: oklch(36% 0.03 262);
196
+ --neba-secondary-on-solid: oklch(99% 0.003 262);
197
+ --neba-secondary-accent: oklch(54% 0.045 262);
198
+
199
+ --neba-success-solid: oklch(48.5% 0.136 148);
200
+ --neba-success-solid-hover: oklch(44% 0.126 148);
201
+ --neba-success-solid-active: oklch(36.5% 0.106 148);
202
+ --neba-success-on-solid: oklch(99% 0.004 148);
203
+ --neba-success-accent: oklch(52.5% 0.143 148);
204
+
205
+ --neba-warning-solid: oklch(80% 0.146 80);
206
+ --neba-warning-solid-hover: oklch(75.5% 0.148 78);
207
+ --neba-warning-solid-active: oklch(66.5% 0.14 74);
208
+ --neba-warning-on-solid: oklch(28% 0.06 70);
209
+ --neba-warning-accent: oklch(54.5% 0.111 68);
210
+
211
+ --neba-danger-solid: oklch(52.5% 0.194 30);
212
+ --neba-danger-solid-hover: oklch(48% 0.186 30);
213
+ --neba-danger-solid-active: oklch(40.5% 0.162 30);
214
+ --neba-danger-on-solid: oklch(99% 0.004 30);
215
+ --neba-danger-accent: oklch(56.5% 0.212 30);
216
+
217
+ --neba-info-solid: oklch(48.5% 0.088 223);
218
+ --neba-info-solid-hover: oklch(44% 0.081 223);
219
+ --neba-info-solid-active: oklch(36.5% 0.068 223);
220
+ --neba-info-on-solid: oklch(99% 0.004 223);
221
+ --neba-info-accent: oklch(53.5% 0.096 223);
222
+ }
223
+
224
+ /* ---------------------------------------------------------------------------
225
+ * Dark theme
226
+ *
227
+ * Fills stay mid-tone rather than pale, so a filled button on a dark surface
228
+ * does not glare; the readable-on-surface `accent` is what lightens.
229
+ * ------------------------------------------------------------------------- */
230
+
231
+ @media (prefers-color-scheme: dark) {
232
+ :root:not(.light):not([data-theme='light']) {
233
+ --neba-surface: oklch(19% 0.012 262);
234
+ --neba-fg: oklch(95% 0.008 262);
235
+ --neba-muted-fg: oklch(66% 0.014 262);
236
+ --neba-border: oklch(30% 0.012 262);
237
+
238
+ --neba-disabled-bg: oklch(27% 0.012 262);
239
+ --neba-disabled-fg: oklch(48% 0.012 262);
240
+ --neba-disabled-border: oklch(32% 0.012 262);
241
+
242
+ --neba-fill-alpha: 87%;
243
+ --neba-blur: blur(9px) saturate(1.3);
244
+
245
+ --neba-plate-edge: rgb(255 255 255 / 0.12);
246
+ --neba-plate-top: rgb(255 255 255 / 0.16);
247
+ --neba-sheen: linear-gradient(148deg, rgb(255 255 255 / 0.09), rgb(255 255 255 / 0) 52%);
248
+
249
+ --neba-glass-bg: rgb(255 255 255 / 0.07);
250
+
251
+ --neba-panel: rgb(255 255 255 / 0.07);
252
+ --neba-panel-hover: rgb(255 255 255 / 0.1);
253
+ --neba-panel-press: rgb(255 255 255 / 0.13);
254
+
255
+ --neba-glow-on-fill: rgb(255 255 255 / 0.13);
256
+ --neba-flash-on-fill: rgb(255 255 255 / 0.18);
257
+
258
+ --neba-scrim: rgb(0 0 0 / 0.58);
259
+
260
+ --neba-shadow-ambient: rgb(0 0 0 / 0.55);
261
+
262
+ --neba-primary-solid: oklch(57% 0.208 262);
263
+ --neba-primary-solid-hover: oklch(62% 0.181 262);
264
+ --neba-primary-solid-active: oklch(48% 0.196 262);
265
+ --neba-primary-on-solid: oklch(99% 0.004 262);
266
+ --neba-primary-accent: oklch(78% 0.103 262);
267
+
268
+ --neba-secondary-solid: oklch(50% 0.036 262);
269
+ --neba-secondary-solid-hover: oklch(55% 0.036 262);
270
+ --neba-secondary-solid-active: oklch(41% 0.03 262);
271
+ --neba-secondary-on-solid: oklch(99% 0.003 262);
272
+ --neba-secondary-accent: oklch(78% 0.026 262);
273
+
274
+ --neba-success-solid: oklch(54% 0.147 148);
275
+ --neba-success-solid-hover: oklch(59% 0.161 148);
276
+ --neba-success-solid-active: oklch(45% 0.123 148);
277
+ --neba-success-on-solid: oklch(99% 0.004 148);
278
+ --neba-success-accent: oklch(78% 0.17 148);
279
+
280
+ --neba-warning-solid: oklch(76% 0.145 80);
281
+ --neba-warning-solid-hover: oklch(81% 0.154 80);
282
+ --neba-warning-solid-active: oklch(66% 0.126 80);
283
+ --neba-warning-on-solid: oklch(25% 0.06 70);
284
+ --neba-warning-accent: oklch(82% 0.14 80);
285
+
286
+ --neba-danger-solid: oklch(55% 0.207 30);
287
+ --neba-danger-solid-hover: oklch(60% 0.226 30);
288
+ --neba-danger-solid-active: oklch(46% 0.174 30);
289
+ --neba-danger-on-solid: oklch(99% 0.004 30);
290
+ --neba-danger-accent: oklch(76% 0.136 32);
291
+
292
+ --neba-info-solid: oklch(55% 0.098 223);
293
+ --neba-info-solid-hover: oklch(60% 0.107 223);
294
+ --neba-info-solid-active: oklch(46% 0.082 223);
295
+ --neba-info-on-solid: oklch(99% 0.004 223);
296
+ --neba-info-accent: oklch(79% 0.11 223);
297
+ }
298
+ }
299
+
300
+ .dark,
301
+ [data-theme='dark'] {
302
+ --neba-surface: oklch(19% 0.012 262);
303
+ --neba-fg: oklch(95% 0.008 262);
304
+ --neba-muted-fg: oklch(66% 0.014 262);
305
+ --neba-border: oklch(30% 0.012 262);
306
+
307
+ --neba-disabled-bg: oklch(27% 0.012 262);
308
+ --neba-disabled-fg: oklch(48% 0.012 262);
309
+ --neba-disabled-border: oklch(32% 0.012 262);
310
+
311
+ --neba-fill-alpha: 87%;
312
+ --neba-blur: blur(9px) saturate(1.3);
313
+
314
+ --neba-plate-edge: rgb(255 255 255 / 0.12);
315
+ --neba-plate-top: rgb(255 255 255 / 0.16);
316
+ --neba-sheen: linear-gradient(148deg, rgb(255 255 255 / 0.09), rgb(255 255 255 / 0) 52%);
317
+
318
+ --neba-glass-bg: rgb(255 255 255 / 0.07);
319
+
320
+ --neba-panel: rgb(255 255 255 / 0.07);
321
+ --neba-panel-hover: rgb(255 255 255 / 0.1);
322
+ --neba-panel-press: rgb(255 255 255 / 0.13);
323
+
324
+ --neba-glow-on-fill: rgb(255 255 255 / 0.13);
325
+ --neba-flash-on-fill: rgb(255 255 255 / 0.18);
326
+
327
+ --neba-scrim: rgb(0 0 0 / 0.58);
328
+
329
+ --neba-shadow-ambient: rgb(0 0 0 / 0.55);
330
+
331
+ --neba-primary-solid: oklch(57% 0.208 262);
332
+ --neba-primary-solid-hover: oklch(62% 0.181 262);
333
+ --neba-primary-solid-active: oklch(48% 0.196 262);
334
+ --neba-primary-on-solid: oklch(99% 0.004 262);
335
+ --neba-primary-accent: oklch(78% 0.103 262);
336
+
337
+ --neba-secondary-solid: oklch(50% 0.036 262);
338
+ --neba-secondary-solid-hover: oklch(55% 0.036 262);
339
+ --neba-secondary-solid-active: oklch(41% 0.03 262);
340
+ --neba-secondary-on-solid: oklch(99% 0.003 262);
341
+ --neba-secondary-accent: oklch(78% 0.026 262);
342
+
343
+ --neba-success-solid: oklch(54% 0.147 148);
344
+ --neba-success-solid-hover: oklch(59% 0.161 148);
345
+ --neba-success-solid-active: oklch(45% 0.123 148);
346
+ --neba-success-on-solid: oklch(99% 0.004 148);
347
+ --neba-success-accent: oklch(78% 0.17 148);
348
+
349
+ --neba-warning-solid: oklch(76% 0.145 80);
350
+ --neba-warning-solid-hover: oklch(81% 0.154 80);
351
+ --neba-warning-solid-active: oklch(66% 0.126 80);
352
+ --neba-warning-on-solid: oklch(25% 0.06 70);
353
+ --neba-warning-accent: oklch(82% 0.14 80);
354
+
355
+ --neba-danger-solid: oklch(55% 0.207 30);
356
+ --neba-danger-solid-hover: oklch(60% 0.226 30);
357
+ --neba-danger-solid-active: oklch(46% 0.174 30);
358
+ --neba-danger-on-solid: oklch(99% 0.004 30);
359
+ --neba-danger-accent: oklch(76% 0.136 32);
360
+
361
+ --neba-info-solid: oklch(55% 0.098 223);
362
+ --neba-info-solid-hover: oklch(60% 0.107 223);
363
+ --neba-info-solid-active: oklch(46% 0.082 223);
364
+ --neba-info-on-solid: oklch(99% 0.004 223);
365
+ --neba-info-accent: oklch(79% 0.11 223);
366
+ }
367
+
368
+ /* ---------------------------------------------------------------------------
369
+ * Derived tokens
370
+ *
371
+ * Fills, tints, hairlines and focus rings are all one base color at different
372
+ * strengths, so they are computed rather than hand-picked per theme. This block
373
+ * is repeated for every theme root because a custom property resolves its
374
+ * `var()`s on the element that declares it — declaring these only on `:root`
375
+ * would freeze them to the light values inside a `.dark` subtree.
376
+ * ------------------------------------------------------------------------- */
377
+
378
+ :root,
379
+ .dark,
380
+ .light,
381
+ [data-theme='dark'],
382
+ [data-theme='light'] {
383
+ --neba-primary-fill: color-mix(
384
+ in srgb,
385
+ var(--neba-primary-solid) var(--neba-fill-alpha),
386
+ transparent
387
+ );
388
+ --neba-primary-fill-hover: color-mix(
389
+ in srgb,
390
+ var(--neba-primary-solid-hover) var(--neba-fill-alpha),
391
+ transparent
392
+ );
393
+ --neba-primary-fill-active: color-mix(
394
+ in srgb,
395
+ var(--neba-primary-solid-active) var(--neba-fill-alpha),
396
+ transparent
397
+ );
398
+ --neba-primary-soft: color-mix(in srgb, var(--neba-primary-accent) 10%, transparent);
399
+ --neba-primary-soft-hover: color-mix(in srgb, var(--neba-primary-accent) 17%, transparent);
400
+ --neba-primary-soft-press: color-mix(in srgb, var(--neba-primary-accent) 25%, transparent);
401
+ --neba-primary-panel: color-mix(in srgb, var(--neba-primary-accent) 8%, var(--neba-glass-bg));
402
+ --neba-primary-panel-hover: color-mix(
403
+ in srgb,
404
+ var(--neba-primary-accent) 16%,
405
+ var(--neba-glass-bg)
406
+ );
407
+ --neba-primary-panel-press: color-mix(
408
+ in srgb,
409
+ var(--neba-primary-accent) 24%,
410
+ var(--neba-glass-bg)
411
+ );
412
+ --neba-primary-line: color-mix(in srgb, var(--neba-primary-accent) 22%, transparent);
413
+ --neba-primary-line-hover: color-mix(in srgb, var(--neba-primary-accent) 40%, transparent);
414
+ --neba-primary-ring: color-mix(in srgb, var(--neba-primary-accent) 55%, transparent);
415
+
416
+ --neba-secondary-fill: color-mix(
417
+ in srgb,
418
+ var(--neba-secondary-solid) var(--neba-fill-alpha),
419
+ transparent
420
+ );
421
+ --neba-secondary-fill-hover: color-mix(
422
+ in srgb,
423
+ var(--neba-secondary-solid-hover) var(--neba-fill-alpha),
424
+ transparent
425
+ );
426
+ --neba-secondary-fill-active: color-mix(
427
+ in srgb,
428
+ var(--neba-secondary-solid-active) var(--neba-fill-alpha),
429
+ transparent
430
+ );
431
+ --neba-secondary-soft: color-mix(in srgb, var(--neba-secondary-accent) 10%, transparent);
432
+ --neba-secondary-soft-hover: color-mix(in srgb, var(--neba-secondary-accent) 17%, transparent);
433
+ --neba-secondary-soft-press: color-mix(in srgb, var(--neba-secondary-accent) 25%, transparent);
434
+ --neba-secondary-panel: color-mix(in srgb, var(--neba-secondary-accent) 8%, var(--neba-glass-bg));
435
+ --neba-secondary-panel-hover: color-mix(
436
+ in srgb,
437
+ var(--neba-secondary-accent) 16%,
438
+ var(--neba-glass-bg)
439
+ );
440
+ --neba-secondary-panel-press: color-mix(
441
+ in srgb,
442
+ var(--neba-secondary-accent) 24%,
443
+ var(--neba-glass-bg)
444
+ );
445
+ --neba-secondary-line: color-mix(in srgb, var(--neba-secondary-accent) 22%, transparent);
446
+ --neba-secondary-line-hover: color-mix(in srgb, var(--neba-secondary-accent) 40%, transparent);
447
+ --neba-secondary-ring: color-mix(in srgb, var(--neba-secondary-accent) 55%, transparent);
448
+
449
+ --neba-success-fill: color-mix(
450
+ in srgb,
451
+ var(--neba-success-solid) var(--neba-fill-alpha),
452
+ transparent
453
+ );
454
+ --neba-success-fill-hover: color-mix(
455
+ in srgb,
456
+ var(--neba-success-solid-hover) var(--neba-fill-alpha),
457
+ transparent
458
+ );
459
+ --neba-success-fill-active: color-mix(
460
+ in srgb,
461
+ var(--neba-success-solid-active) var(--neba-fill-alpha),
462
+ transparent
463
+ );
464
+ --neba-success-soft: color-mix(in srgb, var(--neba-success-accent) 10%, transparent);
465
+ --neba-success-soft-hover: color-mix(in srgb, var(--neba-success-accent) 17%, transparent);
466
+ --neba-success-soft-press: color-mix(in srgb, var(--neba-success-accent) 25%, transparent);
467
+ --neba-success-panel: color-mix(in srgb, var(--neba-success-accent) 8%, var(--neba-glass-bg));
468
+ --neba-success-panel-hover: color-mix(
469
+ in srgb,
470
+ var(--neba-success-accent) 16%,
471
+ var(--neba-glass-bg)
472
+ );
473
+ --neba-success-panel-press: color-mix(
474
+ in srgb,
475
+ var(--neba-success-accent) 24%,
476
+ var(--neba-glass-bg)
477
+ );
478
+ --neba-success-line: color-mix(in srgb, var(--neba-success-accent) 22%, transparent);
479
+ --neba-success-line-hover: color-mix(in srgb, var(--neba-success-accent) 40%, transparent);
480
+ --neba-success-ring: color-mix(in srgb, var(--neba-success-accent) 55%, transparent);
481
+
482
+ --neba-warning-fill: color-mix(
483
+ in srgb,
484
+ var(--neba-warning-solid) var(--neba-fill-alpha),
485
+ transparent
486
+ );
487
+ --neba-warning-fill-hover: color-mix(
488
+ in srgb,
489
+ var(--neba-warning-solid-hover) var(--neba-fill-alpha),
490
+ transparent
491
+ );
492
+ --neba-warning-fill-active: color-mix(
493
+ in srgb,
494
+ var(--neba-warning-solid-active) var(--neba-fill-alpha),
495
+ transparent
496
+ );
497
+ --neba-warning-soft: color-mix(in srgb, var(--neba-warning-accent) 12%, transparent);
498
+ --neba-warning-soft-hover: color-mix(in srgb, var(--neba-warning-accent) 20%, transparent);
499
+ --neba-warning-soft-press: color-mix(in srgb, var(--neba-warning-accent) 28%, transparent);
500
+ --neba-warning-panel: color-mix(in srgb, var(--neba-warning-accent) 8%, var(--neba-glass-bg));
501
+ --neba-warning-panel-hover: color-mix(
502
+ in srgb,
503
+ var(--neba-warning-accent) 16%,
504
+ var(--neba-glass-bg)
505
+ );
506
+ --neba-warning-panel-press: color-mix(
507
+ in srgb,
508
+ var(--neba-warning-accent) 24%,
509
+ var(--neba-glass-bg)
510
+ );
511
+ --neba-warning-line: color-mix(in srgb, var(--neba-warning-accent) 26%, transparent);
512
+ --neba-warning-line-hover: color-mix(in srgb, var(--neba-warning-accent) 44%, transparent);
513
+ --neba-warning-ring: color-mix(in srgb, var(--neba-warning-accent) 55%, transparent);
514
+
515
+ --neba-danger-fill: color-mix(
516
+ in srgb,
517
+ var(--neba-danger-solid) var(--neba-fill-alpha),
518
+ transparent
519
+ );
520
+ --neba-danger-fill-hover: color-mix(
521
+ in srgb,
522
+ var(--neba-danger-solid-hover) var(--neba-fill-alpha),
523
+ transparent
524
+ );
525
+ --neba-danger-fill-active: color-mix(
526
+ in srgb,
527
+ var(--neba-danger-solid-active) var(--neba-fill-alpha),
528
+ transparent
529
+ );
530
+ --neba-danger-soft: color-mix(in srgb, var(--neba-danger-accent) 10%, transparent);
531
+ --neba-danger-soft-hover: color-mix(in srgb, var(--neba-danger-accent) 17%, transparent);
532
+ --neba-danger-soft-press: color-mix(in srgb, var(--neba-danger-accent) 25%, transparent);
533
+ --neba-danger-panel: color-mix(in srgb, var(--neba-danger-accent) 8%, var(--neba-glass-bg));
534
+ --neba-danger-panel-hover: color-mix(
535
+ in srgb,
536
+ var(--neba-danger-accent) 16%,
537
+ var(--neba-glass-bg)
538
+ );
539
+ --neba-danger-panel-press: color-mix(
540
+ in srgb,
541
+ var(--neba-danger-accent) 24%,
542
+ var(--neba-glass-bg)
543
+ );
544
+ --neba-danger-line: color-mix(in srgb, var(--neba-danger-accent) 22%, transparent);
545
+ --neba-danger-line-hover: color-mix(in srgb, var(--neba-danger-accent) 40%, transparent);
546
+ --neba-danger-ring: color-mix(in srgb, var(--neba-danger-accent) 55%, transparent);
547
+
548
+ --neba-info-fill: color-mix(in srgb, var(--neba-info-solid) var(--neba-fill-alpha), transparent);
549
+ --neba-info-fill-hover: color-mix(
550
+ in srgb,
551
+ var(--neba-info-solid-hover) var(--neba-fill-alpha),
552
+ transparent
553
+ );
554
+ --neba-info-fill-active: color-mix(
555
+ in srgb,
556
+ var(--neba-info-solid-active) var(--neba-fill-alpha),
557
+ transparent
558
+ );
559
+ --neba-info-soft: color-mix(in srgb, var(--neba-info-accent) 10%, transparent);
560
+ --neba-info-soft-hover: color-mix(in srgb, var(--neba-info-accent) 17%, transparent);
561
+ --neba-info-soft-press: color-mix(in srgb, var(--neba-info-accent) 25%, transparent);
562
+ --neba-info-panel: color-mix(in srgb, var(--neba-info-accent) 8%, var(--neba-glass-bg));
563
+ --neba-info-panel-hover: color-mix(in srgb, var(--neba-info-accent) 16%, var(--neba-glass-bg));
564
+ --neba-info-panel-press: color-mix(in srgb, var(--neba-info-accent) 24%, var(--neba-glass-bg));
565
+ --neba-info-line: color-mix(in srgb, var(--neba-info-accent) 22%, transparent);
566
+ --neba-info-line-hover: color-mix(in srgb, var(--neba-info-accent) 40%, transparent);
567
+ --neba-info-ring: color-mix(in srgb, var(--neba-info-accent) 55%, transparent);
568
+ }
569
+
570
+ /* ---------------------------------------------------------------------------
571
+ * Sheet titles
572
+ *
573
+ * A `title` is styled by the element the component renders around it — Card,
574
+ * Alert and Dialog all do this — so a plain string needs nothing. But a sheet
575
+ * that belongs in the document outline has to be given a real heading,
576
+ * `title={<h2>…</h2>}`, and that heading arrives with the browser's own 1.5em
577
+ * bold and its margins, which would ignore the type scale just set.
578
+ *
579
+ * This is in CSS rather than in Tailwind because the selector is the point:
580
+ * `[&_:where(h1,h2,h3,h4,h5,h6)]:[font:inherit]` is expressible and nobody
581
+ * would want to read it twice.
582
+ * ------------------------------------------------------------------------- */
583
+
584
+ .neba-title :where(h1, h2, h3, h4, h5, h6) {
585
+ font: inherit;
586
+ margin: 0;
587
+ }
588
+
589
+ /* ---------------------------------------------------------------------------
590
+ * Links
591
+ *
592
+ * `<a>` is, with `<td>`, one of the two tags a host stylesheet still styles by
593
+ * name — Tailwind Typography's `.prose a`, VitePress's own `.vp-doc a`, every
594
+ * CSS framework ever written — and always as a class plus a type, which is a
595
+ * specificity a one-class utility cannot reach. Table answered this by moving
596
+ * its cell padding inline. A link cannot: half of what it draws only exists on
597
+ * `:hover`, and inline styles have no `:hover`.
598
+ *
599
+ * So the selector is doubled instead. `.neba-link.neba-link` is two classes and
600
+ * matches nothing extra, which puts these declarations above a host's `a` rule
601
+ * and still below anything a caller writes with two classes of their own. Only
602
+ * the invariants are here — the parts of a TextLink that no prop changes. The
603
+ * colour and whether the line is drawn at all stay in the component, written
604
+ * through the same doubled class.
605
+ *
606
+ * The thickness has a `max()` floor because a hairline that lands between two
607
+ * device pixels is antialiased into a grey smudge: 0.055em is a real line from
608
+ * about 18px up, and 1px below that.
609
+ * ------------------------------------------------------------------------- */
610
+
611
+ .neba-link.neba-link {
612
+ font-weight: inherit;
613
+ text-decoration-thickness: max(1px, 0.055em);
614
+ text-underline-offset: 0.2em;
615
+ text-underline-position: from-font;
616
+ text-decoration-color: var(--n-underline);
617
+ }
618
+
619
+ .neba-link.neba-link:hover {
620
+ text-decoration-color: var(--n-underline-hover);
621
+ }
622
+
623
+ /* ---------------------------------------------------------------------------
624
+ * Grid
625
+ *
626
+ * A column is `(100% + gap) * span / columns - gap` wide, and an offset shifts
627
+ * it by `(100% + gap) * offset / columns`. Both are exact rather than
628
+ * approximate: the `+ gap` is what pays for the gutter the item does not have
629
+ * on its trailing edge, so twelve `span={1}` items and one `span={12}` end on
630
+ * the same pixel.
631
+ *
632
+ * This is here rather than in Tailwind for the reason the rest of this file is:
633
+ * `columns` is a number a caller picks and `span` changes at four widths, so
634
+ * the class name would have to be assembled at runtime — and a class name that
635
+ * is assembled at runtime is a class name Tailwind never sees. The per-instance
636
+ * numbers arrive as inline `--n-*` slots, exactly as colours do, and the
637
+ * arithmetic lives here once.
638
+ *
639
+ * `min(span, columns)` is the clamp: a `span={99}` fills its row instead of
640
+ * overflowing the page, which is what the caller meant.
641
+ *
642
+ * Every slot cascades upward — each breakpoint falls back through the ones
643
+ * below it — so `span={{ md: 6 }}` sets one custom property and not five. The
644
+ * chains get long at `xl`; that is the cost of keeping the DOM lean, and it is
645
+ * paid once here rather than on every grid item in a consumer's app.
646
+ *
647
+ * The widths are Tailwind's own breakpoints, so a Neba grid and a `md:` utility
648
+ * change at the same moment.
649
+ * ------------------------------------------------------------------------- */
650
+
651
+ .neba-grid {
652
+ --n-cols: var(--n-cols-xs, 12);
653
+ --n-gap-x: var(--n-gap-x-xs, 0px);
654
+ --n-gap-y: var(--n-gap-y-xs, 0px);
655
+ }
656
+
657
+ .neba-grid-item {
658
+ --n-span: var(--n-span-xs, var(--n-cols, 12));
659
+ --n-offset: var(--n-offset-xs, 0);
660
+
661
+ /* One column plus one gutter. `--n-cols` and `--n-gap-x` are inherited from
662
+ the container, so this re-resolves on its own at every breakpoint. */
663
+ --n-track: calc((100% + var(--n-gap-x, 0px)) / var(--n-cols, 12));
664
+
665
+ /* Inseparable from the width above it: `flex` stops the row from resizing
666
+ the item, `min-width` stops a long unbroken word from doing the same, and
667
+ `box-sizing` is what makes the percentage mean the whole item. */
668
+ box-sizing: border-box;
669
+ flex: 0 0 auto;
670
+ min-width: 0;
671
+ width: calc(var(--n-track) * min(var(--n-span), var(--n-cols, 12)) - var(--n-gap-x, 0px));
672
+ margin-inline-start: calc(var(--n-track) * min(var(--n-offset), var(--n-cols, 12)));
673
+ }
674
+
675
+ @media (width >= 40rem) {
676
+ .neba-grid {
677
+ --n-cols: var(--n-cols-sm, var(--n-cols-xs, 12));
678
+ --n-gap-x: var(--n-gap-x-sm, var(--n-gap-x-xs, 0px));
679
+ --n-gap-y: var(--n-gap-y-sm, var(--n-gap-y-xs, 0px));
680
+ }
681
+
682
+ .neba-grid-item {
683
+ --n-span: var(--n-span-sm, var(--n-span-xs, var(--n-cols, 12)));
684
+ --n-offset: var(--n-offset-sm, var(--n-offset-xs, 0));
685
+ }
686
+ }
687
+
688
+ @media (width >= 48rem) {
689
+ .neba-grid {
690
+ --n-cols: var(--n-cols-md, var(--n-cols-sm, var(--n-cols-xs, 12)));
691
+ --n-gap-x: var(--n-gap-x-md, var(--n-gap-x-sm, var(--n-gap-x-xs, 0px)));
692
+ --n-gap-y: var(--n-gap-y-md, var(--n-gap-y-sm, var(--n-gap-y-xs, 0px)));
693
+ }
694
+
695
+ .neba-grid-item {
696
+ --n-span: var(--n-span-md, var(--n-span-sm, var(--n-span-xs, var(--n-cols, 12))));
697
+ --n-offset: var(--n-offset-md, var(--n-offset-sm, var(--n-offset-xs, 0)));
698
+ }
699
+ }
700
+
701
+ @media (width >= 64rem) {
702
+ .neba-grid {
703
+ --n-cols: var(--n-cols-lg, var(--n-cols-md, var(--n-cols-sm, var(--n-cols-xs, 12))));
704
+ --n-gap-x: var(--n-gap-x-lg, var(--n-gap-x-md, var(--n-gap-x-sm, var(--n-gap-x-xs, 0px))));
705
+ --n-gap-y: var(--n-gap-y-lg, var(--n-gap-y-md, var(--n-gap-y-sm, var(--n-gap-y-xs, 0px))));
706
+ }
707
+
708
+ .neba-grid-item {
709
+ --n-span: var(
710
+ --n-span-lg,
711
+ var(--n-span-md, var(--n-span-sm, var(--n-span-xs, var(--n-cols, 12))))
712
+ );
713
+ --n-offset: var(--n-offset-lg, var(--n-offset-md, var(--n-offset-sm, var(--n-offset-xs, 0))));
714
+ }
715
+ }
716
+
717
+ @media (width >= 80rem) {
718
+ .neba-grid {
719
+ --n-cols: var(
720
+ --n-cols-xl,
721
+ var(--n-cols-lg, var(--n-cols-md, var(--n-cols-sm, var(--n-cols-xs, 12))))
722
+ );
723
+ --n-gap-x: var(
724
+ --n-gap-x-xl,
725
+ var(--n-gap-x-lg, var(--n-gap-x-md, var(--n-gap-x-sm, var(--n-gap-x-xs, 0px))))
726
+ );
727
+ --n-gap-y: var(
728
+ --n-gap-y-xl,
729
+ var(--n-gap-y-lg, var(--n-gap-y-md, var(--n-gap-y-sm, var(--n-gap-y-xs, 0px))))
730
+ );
731
+ }
732
+
733
+ .neba-grid-item {
734
+ --n-span: var(
735
+ --n-span-xl,
736
+ var(--n-span-lg, var(--n-span-md, var(--n-span-sm, var(--n-span-xs, var(--n-cols, 12)))))
737
+ );
738
+ --n-offset: var(
739
+ --n-offset-xl,
740
+ var(--n-offset-lg, var(--n-offset-md, var(--n-offset-sm, var(--n-offset-xs, 0))))
741
+ );
742
+ }
743
+ }
744
+
745
+ /* ---------------------------------------------------------------------------
746
+ * Tree guides
747
+ *
748
+ * The rail down a level of a TreeView, and — in `folder` — the elbow from that
749
+ * rail into every row on it.
750
+ *
751
+ * Both are drawn on the `<li>` rather than on the row inside it, and that is the
752
+ * whole trick: an `<li>` in a tree contains its own branch, so a rail that runs
753
+ * its full height keeps running past an expanded subtree and reaches the next
754
+ * sibling, exactly as a file manager draws it. The rail under a *last* child
755
+ * stops at that row's middle, which is the only thing telling the eye a branch
756
+ * has ended.
757
+ *
758
+ * `--n-tree-indent` and `--n-tree-row` are set inline by TreeView from its
759
+ * `size`. The rail sits half an indent outside the row, in the column the
760
+ * group's own padding opened up.
761
+ *
762
+ * This is CSS rather than Tailwind for the reason the grid is: the geometry is
763
+ * arithmetic on two per-instance custom properties, and the last child's rail is
764
+ * a different height from every other one. `[&:last-child::before]:[block-size:
765
+ * calc(var(--n-tree-row)/2)]` is expressible and unreadable.
766
+ *
767
+ * Everything is logical — `inset-inline-start`, `border-inline-start` — so the
768
+ * whole drawing mirrors under RTL without a second rule.
769
+ * ------------------------------------------------------------------------- */
770
+
771
+ .neba-tree-simple :where(ul[role='group']) > li::before,
772
+ .neba-tree-folder :where(ul[role='group']) > li::before {
773
+ content: '';
774
+ position: absolute;
775
+ inset-block-start: 0;
776
+ inset-inline-start: calc(var(--n-tree-indent, 1.25rem) / -2);
777
+ block-size: 100%;
778
+ inline-size: 0;
779
+ border-inline-start: 1px solid var(--neba-border);
780
+ pointer-events: none;
781
+ }
782
+
783
+ .neba-tree-folder :where(ul[role='group']) > li:last-child::before {
784
+ block-size: calc(var(--n-tree-row, 2rem) / 2);
785
+ }
786
+
787
+ /* The elbow. It stops 2px short of the row so the corner reads as a turn rather
788
+ than as a line running into the label. */
789
+ .neba-tree-folder :where(ul[role='group']) > li::after {
790
+ content: '';
791
+ position: absolute;
792
+ inset-block-start: calc(var(--n-tree-row, 2rem) / 2);
793
+ inset-inline-start: calc(var(--n-tree-indent, 1.25rem) / -2);
794
+ block-size: 0;
795
+ inline-size: calc(var(--n-tree-indent, 1.25rem) / 2 - 2px);
796
+ border-block-start: 1px solid var(--neba-border);
797
+ pointer-events: none;
798
+ }
799
+
800
+ /* ---------------------------------------------------------------------------
801
+ * Interaction light
802
+ *
803
+ * Two stacked layers on an interactive surface, both reading `--n-mx`/`--n-my`
804
+ * — the pointer position, written straight to the element's inline style on
805
+ * `pointermove` — and both no-ops until those slots have colour in them.
806
+ *
807
+ * This lives in CSS rather than in Tailwind arbitrary variants because the
808
+ * selectors are pseudo-elements carrying gradients: `[&::before]:[background:
809
+ * radial-gradient(...)]` is technically expressible and completely unreadable.
810
+ *
811
+ * Cost per hovered button: two composited layers and a gradient repaint of one
812
+ * small box, on frames the pointer already moved in. Nothing reflows, and React
813
+ * never re-renders — the component writes the two custom properties directly.
814
+ * ------------------------------------------------------------------------- */
815
+
816
+ .neba-glow::before,
817
+ .neba-glow::after {
818
+ content: '';
819
+ position: absolute;
820
+ inset: 0;
821
+ border-radius: inherit;
822
+ pointer-events: none;
823
+ opacity: 0;
824
+ }
825
+
826
+ /* Pointer spotlight: a soft bloom trailing the cursor across the surface. */
827
+ .neba-glow::before {
828
+ background: radial-gradient(
829
+ 5.5rem circle at var(--n-mx, 50%) var(--n-my, 50%),
830
+ var(--n-glow, transparent),
831
+ transparent 70%
832
+ );
833
+ transition: opacity 240ms var(--neba-ease);
834
+ }
835
+
836
+ @media (hover: hover) {
837
+ .neba-glow:hover::before {
838
+ opacity: 1;
839
+ }
840
+ }
841
+
842
+ /*
843
+ * Release afterglow. The asymmetry is the whole trick: 0ms in, ~900ms out, so
844
+ * the flash lands on the frame of the press and is still visibly draining a
845
+ * beat after the finger lifts. No JavaScript, no ripple element, no timers.
846
+ */
847
+ .neba-glow::after {
848
+ background: radial-gradient(
849
+ 4.5rem circle at var(--n-mx, 50%) var(--n-my, 50%),
850
+ var(--n-flash, transparent),
851
+ transparent 72%
852
+ );
853
+ transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1);
854
+ }
855
+
856
+ .neba-glow:active::after {
857
+ opacity: 1;
858
+ transition-duration: 0ms;
859
+ }
860
+
861
+ /* ---------------------------------------------------------------------------
862
+ * Loading
863
+ *
864
+ * The progress indicators are the one place in the library where something
865
+ * moves on its own, and the only place it is allowed to: an indeterminate
866
+ * indicator that does not move is a decoration.
867
+ *
868
+ * The rule against transforms still holds, because none of these moves anything
869
+ * with text in it. The sweep slides an empty box along a 4px groove, the ring
870
+ * turns a glyph — the same allowance the Select chevron takes — and the plates
871
+ * only change colour.
872
+ *
873
+ * They are real CSS rather than Tailwind arbitrary variants for the reason
874
+ * everything else in this file is: keyframes are not a variant, so there is
875
+ * nothing to express them with.
876
+ * ------------------------------------------------------------------------- */
877
+
878
+ /*
879
+ * The indeterminate bar: a short segment crossing the groove.
880
+ *
881
+ * `inset-inline-start` rather than a transform, so it runs the other way under
882
+ * RTL without being told. The cost is a layout pass per frame, confined to a
883
+ * box a few pixels tall — which is the trade that makes the rule affordable
884
+ * here rather than an exception to it.
885
+ */
886
+ @keyframes neba-sweep {
887
+ from {
888
+ inset-inline-start: -45%;
889
+ }
890
+ to {
891
+ inset-inline-start: 100%;
892
+ }
893
+ }
894
+
895
+ .neba-sweep {
896
+ position: absolute;
897
+ inset-block: 0;
898
+ inline-size: 45%;
899
+ animation: neba-sweep 1.15s var(--neba-ease) infinite;
900
+ }
901
+
902
+ @keyframes neba-ring-spin {
903
+ to {
904
+ rotate: 360deg;
905
+ }
906
+ }
907
+
908
+ .neba-ring-spin {
909
+ transform-origin: center;
910
+ animation: neba-ring-spin 900ms linear infinite;
911
+ }
912
+
913
+ /*
914
+ * The plates light in sequence, each one held back by its own index. Colour
915
+ * only — the plates never move, so a row of them reads as a surface being
916
+ * written to rather than as something bouncing.
917
+ */
918
+ @keyframes neba-plate-wave {
919
+ 0%,
920
+ 70%,
921
+ 100% {
922
+ background-color: var(--n-soft);
923
+ box-shadow: var(--neba-plate-glass);
924
+ }
925
+ 35% {
926
+ background-color: var(--n-fill);
927
+ box-shadow: var(--neba-plate-solid);
928
+ }
929
+ }
930
+
931
+ .neba-plate-wave {
932
+ animation: neba-plate-wave 1.2s var(--neba-ease) infinite;
933
+ animation-delay: calc(var(--n-i, 0) * 120ms);
934
+ }
935
+
936
+ /*
937
+ * The three dots under a message somebody is still typing.
938
+ *
939
+ * The same idea as the plates and held to the same rule: colour only, delayed
940
+ * per dot by its own index. Dots that bounce are what every other chat UI does,
941
+ * and a thread is read while this is running — something jumping in the corner
942
+ * of the page you are reading is the one kind of motion the design language has
943
+ * no time for.
944
+ *
945
+ * `currentColor` rather than a token, because a bubble's ink is white on
946
+ * `solid` and the page's own on the other two.
947
+ */
948
+ @keyframes neba-typing {
949
+ 0%,
950
+ 60%,
951
+ 100% {
952
+ background-color: color-mix(in oklab, currentColor 28%, transparent);
953
+ }
954
+ 30% {
955
+ background-color: currentColor;
956
+ }
957
+ }
958
+
959
+ .neba-typing-dot {
960
+ background-color: color-mix(in oklab, currentColor 28%, transparent);
961
+ animation: neba-typing 1.25s var(--neba-ease) infinite;
962
+ animation-delay: calc(var(--n-i, 0) * 160ms);
963
+ }
964
+
965
+ /* The reduced-motion stand-in for the sweep: the segment stops travelling and
966
+ the whole groove pulses instead. Colour, which is the axis every other state
967
+ in the library already uses. */
968
+ @keyframes neba-progress-pulse {
969
+ 0%,
970
+ 100% {
971
+ background-color: var(--n-soft-press);
972
+ }
973
+ 50% {
974
+ background-color: var(--n-fill);
975
+ }
976
+ }
977
+
978
+ /* Respect a reduced-motion preference. Both layers track the pointer, so they
979
+ are motion in the sense that matters here — drop them entirely. */
980
+ @media (prefers-reduced-motion: reduce) {
981
+ :root {
982
+ --neba-duration: 0ms;
983
+ --neba-duration-fill: 0ms;
984
+ }
985
+
986
+ .neba-glow::before,
987
+ .neba-glow::after {
988
+ display: none;
989
+ }
990
+
991
+ /*
992
+ * The indicators are not switched off, because an indeterminate one that
993
+ * holds still says the opposite of what it is for. The travelling segment
994
+ * becomes a colour pulse, and the two that genuinely have to keep turning or
995
+ * cycling are slowed to where they stop reading as motion.
996
+ */
997
+ .neba-sweep {
998
+ inset-inline-start: 0;
999
+ inline-size: 100%;
1000
+ animation: neba-progress-pulse 1.8s var(--neba-ease) infinite;
1001
+ }
1002
+
1003
+ .neba-ring-spin {
1004
+ animation-duration: 2.4s;
1005
+ }
1006
+
1007
+ .neba-plate-wave {
1008
+ animation-duration: 2.4s;
1009
+ }
1010
+
1011
+ /* Slowed rather than stopped, for the reason the indicators are: three dots
1012
+ holding still say the message has stalled, which is the opposite of what
1013
+ they are there for. */
1014
+ .neba-typing-dot {
1015
+ animation-duration: 2.6s;
1016
+ }
1017
+ }